fix(all/googledrive): Fix single items, add support for search, small… (#2216)

… code refactor, fix video extractor (#2216)
This commit is contained in:
Secozzi 2023-09-20 06:50:01 +00:00 committed by GitHub
parent b5fdaf0b9f
commit b50e3a0b75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 198 additions and 194 deletions

View File

@ -39,8 +39,18 @@ class GoogleDriveExtractor(private val client: OkHttpClient, private val headers
add("Cookie", getCookie(itemUrl))
}
val document = client.newCall(GET(itemUrl, itemHeaders)).execute()
.use { it.asJsoup() }
val documentResp = noRedirectClient.newCall(
GET(itemUrl, itemHeaders)
).execute()
if (documentResp.isRedirect) {
val newUrl = documentResp.use { it.headers["location"] }
?: return listOf(Video(itemUrl, videoName, itemUrl, itemHeaders))
return videoFromRedirect(newUrl, itemUrl, videoName)
}
val document = documentResp.use { it.asJsoup() }
val itemSize = document.selectFirst("span.uc-name-size")
?.let { " ${it.ownText().trim()} " }
@ -59,6 +69,15 @@ class GoogleDriveExtractor(private val client: OkHttpClient, private val headers
val redirected = response.use { it.headers["location"] }
?: return listOf(Video(url, videoName + itemSize, url))
return videoFromRedirect(redirected, url, videoName, itemSize)
}
private fun videoFromRedirect(
redirected: String,
fallbackUrl: String,
videoName: String,
itemSize: String = ""
): List<Video> {
val redirectedHeaders = headersBuilder {
set("Host", redirected.toHttpUrl().host)
}
@ -69,7 +88,7 @@ class GoogleDriveExtractor(private val client: OkHttpClient, private val headers
val authCookie = redirectedResponseHeaders.firstOrNull {
it.first == "set-cookie" && it.second.startsWith("AUTH_")
}?.second?.substringBefore(";") ?: return listOf(Video(url, videoName + itemSize, url))
}?.second?.substringBefore(";") ?: return listOf(Video(fallbackUrl, videoName + itemSize, fallbackUrl))
val newRedirected = redirectedResponseHeaders["location"]
?: return listOf(Video(redirected, videoName + itemSize, redirected))

View File

@ -1,12 +1,14 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext {
extName = 'Google Drive'
pkgNameSuffix = 'all.googledrive'
extClass = '.GoogleDrive'
extVersionCode = 7
extVersionCode = 8
libVersion = '13'
}

View File

@ -31,10 +31,12 @@ import okhttp3.ProtocolException
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.security.MessageDigest
class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
@ -52,8 +54,6 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
override val lang = "all"
private var nextPageToken: String? = ""
override val supportsLatest = false
private val json: Json by injectLazy()
@ -62,11 +62,22 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client: OkHttpClient = network.cloudflareClient
override val client: OkHttpClient = network.client
// Overriding headersBuilder() seems to cause issues with webview
private val getHeaders = headers.newBuilder().apply {
add("Accept", "*/*")
add("Connection", "keep-alive")
add("Cookie", getCookie("https://drive.google.com"))
add("Host", "drive.google.com")
}.build()
private var nextPageToken: String? = ""
// ============================== Popular ===============================
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> = Observable.just(parsePage(popularAnimeRequest(page), page))
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> =
Observable.just(parsePage(popularAnimeRequest(page), page))
override fun popularAnimeRequest(page: Int): Request {
require(!baseUrlInternal.isNullOrEmpty()) { "Enter drive path(s) in extension settings." }
@ -75,14 +86,11 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
val folderId = match.groups["id"]!!.value
val recurDepth = match.groups["depth"]?.value ?: ""
baseUrl = "https://drive.google.com/drive/folders/$folderId"
val driveHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Connection", "keep-alive")
.add("Cookie", getCookie("https://drive.google.com"))
.add("Host", "drive.google.com")
.build()
return GET("https://drive.google.com/drive/folders/$folderId$recurDepth", headers = driveHeaders)
return GET(
"https://drive.google.com/drive/folders/$folderId$recurDepth",
headers = getHeaders,
)
}
override fun popularAnimeParse(response: Response): AnimesPage = throw Exception("Not used")
@ -102,14 +110,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
query: String,
filters: AnimeFilterList,
): Observable<AnimesPage> {
require(query.isEmpty()) { "Search is disabled. Use search in webview and add it as a single folder in filters." }
val filterList = if (filters.isEmpty()) getFilterList() else filters
val urlFilter = filterList.find { it is URLFilter } as URLFilter
return if (urlFilter.state.isEmpty()) {
val req = searchAnimeRequest(page, query, filters)
if (query.isEmpty()) {
Observable.just(parsePage(req, page))
} else {
val parentId = req.url.pathSegments.last()
val cleanQuery = URLEncoder.encode(query, "UTF-8")
val genMultiFormReq = searchReq(parentId, cleanQuery)
Observable.just(parsePage(req, page, genMultiFormReq))
}
} else {
Observable.just(addSinglePage(urlFilter.state))
}
@ -126,14 +141,11 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
val folderId = match.groups["id"]!!.value
val recurDepth = match.groups["depth"]?.value ?: ""
baseUrl = "https://drive.google.com/drive/folders/$folderId"
val driveHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Connection", "keep-alive")
.add("Cookie", getCookie("https://drive.google.com"))
.add("Host", "drive.google.com")
.build()
return GET("https://drive.google.com/drive/folders/$folderId$recurDepth", headers = driveHeaders)
return GET(
"https://drive.google.com/drive/folders/$folderId$recurDepth",
headers = getHeaders,
)
}
// ============================== FILTERS ===============================
@ -171,7 +183,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
override fun animeDetailsRequest(anime: SAnime): Request {
val parsed = json.decodeFromString<LinkData>(anime.url)
return GET(parsed.url)
return GET(parsed.url, headers = getHeaders)
}
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
@ -180,62 +192,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
if (parsed.type == "single") return Observable.just(anime)
val folderId = DRIVE_FOLDER_REGEX.matchEntire(parsed.url)!!.groups["id"]!!.value
val driveHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Connection", "keep-alive")
.add("Cookie", getCookie("https://drive.google.com"))
.add("Host", "drive.google.com")
.build()
val driveDocument = try {
client.newCall(GET(parsed.url, headers = driveHeaders)).execute().asJsoup()
client.newCall(GET(parsed.url, headers = getHeaders)).execute().asJsoup()
} catch (a: ProtocolException) {
null
} ?: return Observable.just(anime)
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return Observable.just(anime)
val keyScript = driveDocument.select("script").firstOrNull { script ->
KEY_REGEX.find(script.data()) != null
}?.data() ?: return Observable.just(anime)
val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: ""
val versionScript = driveDocument.select("script").first { script ->
KEY_REGEX.find(script.data()) != null
}.data()
val driveVersion = VERSION_REGEX.find(versionScript)?.groupValues?.get(1) ?: ""
val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: ""
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) {
return Observable.just(anime)
}
var pageToken: String? = ""
while (pageToken != null) {
val requestUrl = "/drive/v2internal/files?openDrive=false&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2ChasVisitorPermissions%2CcontainsUnsubscribedChildren%2CmodifiedByMeDate%2ClastViewedByMeDate%2CalternateLink%2CfileSize%2Cowners(kind%2CpermissionId%2CemailAddressFromAccount%2Cdomain%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CcustomerId%2CancestorHasAugmentedPermissions%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2CabuseIsAppealable%2CabuseNoticeReason%2Cshared%2CaccessRequestsCount%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2Csubscribed%2CfolderColor%2ChasChildFolders%2CfileExtension%2CprimarySyncParentId%2CsharingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CflaggedForAbuse%2CfolderFeatures%2Cspaces%2CsourceAppId%2Crecency%2CrecencyReason%2Cversion%2CactionItems%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CprimaryDomainName%2CorganizationDisplayName%2CpassivelySubscribed%2CtrashingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CtrashedDate%2Cparents(id)%2Ccapabilities(canMoveItemIntoTeamDrive%2CcanUntrash%2CcanMoveItemWithinTeamDrive%2CcanMoveItemOutOfTeamDrive%2CcanDeleteChildren%2CcanTrashChildren%2CcanRequestApproval%2CcanReadCategoryMetadata%2CcanEditCategoryMetadata%2CcanAddMyDriveParent%2CcanRemoveMyDriveParent%2CcanShareChildFiles%2CcanShareChildFolders%2CcanRead%2CcanMoveItemWithinDrive%2CcanMoveChildrenWithinDrive%2CcanAddFolderFromAnotherDrive%2CcanChangeSecurityUpdateEnabled%2CcanBlockOwner%2CcanReportSpamOrAbuse%2CcanCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2CcontentRestrictions(readOnly)%2CapprovalMetadata(approvalVersion%2CapprovalSummaries%2ChasIncomingApproval)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus%2CtargetFile%2CcanRequestAccessToTarget)%2CspamMetadata(markedAsSpamDate%2CinSpamView)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=100&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1"
val body = """--$BOUNDARY
|content-type: application/http
|content-transfer-encoding: binary
|
|GET $requestUrl
|X-Goog-Drive-Client-Version: $driveVersion
|authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0
|
|--$BOUNDARY--""".trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType())
val postUrl = buildString {
append("https://clients6.google.com/batch/drive/v2internal")
append("?${'$'}ct=multipart/mixed; boundary=\"$BOUNDARY\"")
append("&key=$key")
}
val postHeaders = headers.newBuilder()
.add("Content-Type", "text/plain; charset=UTF-8")
.add("Origin", "https://drive.google.com")
.add("Cookie", getCookie("https://drive.google.com"))
.build()
val response = client.newCall(
POST(postUrl, body = body, headers = postHeaders),
createPost(driveDocument, folderId),
).execute()
val parsed = response.parseAs<PostResponse> {
@ -263,6 +234,20 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
val episodeList = mutableListOf<SEpisode>()
val parsed = json.decodeFromString<LinkData>(anime.url)
if (parsed.type == "single") {
return Observable.just(
listOf(
SEpisode.create().apply {
name = "Video"
scanlator = parsed.info!!.size
url = parsed.url
episode_number = 1F
date_upload = -1L
},
),
)
}
val match = DRIVE_FOLDER_REGEX.matchEntire(parsed.url)!! // .groups["id"]!!.value
val maxRecursionDepth = match.groups["depth"]?.let {
it.value.substringAfter("#").substringBefore(",").toInt()
@ -275,63 +260,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
if (recursionDepth == maxRecursionDepth) return
val folderId = DRIVE_FOLDER_REGEX.matchEntire(folderUrl)!!.groups["id"]!!.value
val driveHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Connection", "keep-alive")
.add("Cookie", getCookie("https://drive.google.com"))
.add("Host", "drive.google.com")
.build()
val driveDocument = try {
client.newCall(GET(folderUrl, headers = driveHeaders)).execute().asJsoup()
client.newCall(GET(folderUrl, headers = getHeaders)).execute().asJsoup()
} catch (a: ProtocolException) {
throw Exception("Unable to get items, check webview")
}
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return
val keyScript = driveDocument.select("script").firstOrNull { script ->
KEY_REGEX.find(script.data()) != null
}?.data() ?: throw Exception("Unknown error occured, check webview")
val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: ""
val versionScript = driveDocument.select("script").first { script ->
KEY_REGEX.find(script.data()) != null
}.data()
val driveVersion = VERSION_REGEX.find(versionScript)?.groupValues?.get(1) ?: ""
val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: ""
var pageToken: String? = ""
var counter = 1
while (pageToken != null) {
val requestUrl = "/drive/v2internal/files?openDrive=false&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2ChasVisitorPermissions%2CcontainsUnsubscribedChildren%2CmodifiedByMeDate%2ClastViewedByMeDate%2CalternateLink%2CfileSize%2Cowners(kind%2CpermissionId%2CemailAddressFromAccount%2Cdomain%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CcustomerId%2CancestorHasAugmentedPermissions%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2CabuseIsAppealable%2CabuseNoticeReason%2Cshared%2CaccessRequestsCount%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2Csubscribed%2CfolderColor%2ChasChildFolders%2CfileExtension%2CprimarySyncParentId%2CsharingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CflaggedForAbuse%2CfolderFeatures%2Cspaces%2CsourceAppId%2Crecency%2CrecencyReason%2Cversion%2CactionItems%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CprimaryDomainName%2CorganizationDisplayName%2CpassivelySubscribed%2CtrashingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CtrashedDate%2Cparents(id)%2Ccapabilities(canMoveItemIntoTeamDrive%2CcanUntrash%2CcanMoveItemWithinTeamDrive%2CcanMoveItemOutOfTeamDrive%2CcanDeleteChildren%2CcanTrashChildren%2CcanRequestApproval%2CcanReadCategoryMetadata%2CcanEditCategoryMetadata%2CcanAddMyDriveParent%2CcanRemoveMyDriveParent%2CcanShareChildFiles%2CcanShareChildFolders%2CcanRead%2CcanMoveItemWithinDrive%2CcanMoveChildrenWithinDrive%2CcanAddFolderFromAnotherDrive%2CcanChangeSecurityUpdateEnabled%2CcanBlockOwner%2CcanReportSpamOrAbuse%2CcanCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2CcontentRestrictions(readOnly)%2CapprovalMetadata(approvalVersion%2CapprovalSummaries%2ChasIncomingApproval)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus%2CtargetFile%2CcanRequestAccessToTarget)%2CspamMetadata(markedAsSpamDate%2CinSpamView)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=100&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1"
val body = """--$BOUNDARY
|content-type: application/http
|content-transfer-encoding: binary
|
|GET $requestUrl
|X-Goog-Drive-Client-Version: $driveVersion
|authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0
|
|--$BOUNDARY--""".trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType())
val postUrl = buildString {
append("https://clients6.google.com/batch/drive/v2internal")
append("?${'$'}ct=multipart/mixed; boundary=\"$BOUNDARY\"")
append("&key=$key")
}
val postHeaders = headers.newBuilder()
.add("Content-Type", "text/plain; charset=UTF-8")
.add("Origin", "https://drive.google.com")
.add("Cookie", getCookie("https://drive.google.com"))
.build()
val response = client.newCall(
POST(postUrl, body = body, headers = postHeaders),
createPost(driveDocument, folderId),
).execute()
val parsed = response.parseAs<PostResponse> {
@ -342,7 +285,6 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
parsed.items.forEachIndexed { index, it ->
if (it.mimeType.startsWith("video")) {
val size = it.fileSize?.toLongOrNull()?.let { formatBytes(it) } ?: ""
val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex()
val pathName = if (preferences.trimEpisodeInfo) path.trimInfo() else path
if (start != null && maxRecursionDepth == 1 && counter < start) {
@ -353,9 +295,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
episodeList.add(
SEpisode.create().apply {
name = if (preferences.trimEpisodeName) it.title.trimInfo() else it.title
name =
if (preferences.trimEpisodeName) it.title.trimInfo() else it.title
url = "https://drive.google.com/uc?id=${it.id}"
episode_number = itemNumberRegex.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
episode_number =
ITEM_NUMBER_REGEX.find(it.title.trimInfo())?.groupValues?.get(1)
?.toFloatOrNull() ?: (index + 1).toFloat()
date_upload = -1L
scanlator = if (preferences.scanlatorOrder) {
"/$pathName$size"
@ -379,19 +324,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
}
}
if (parsed.type == "single") {
episodeList.add(
SEpisode.create().apply {
name = parsed.info!!.title
scanlator = parsed.info.size
url = parsed.url
episode_number = 1F
date_upload = -1L
},
)
} else {
traverseFolder(parsed.url, "")
}
return Observable.just(episodeList.reversed())
}
@ -406,11 +339,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ==============================
private fun addSinglePage(folderUrl: String): AnimesPage {
val match = DRIVE_FOLDER_REGEX.matchEntire(folderUrl) ?: throw Exception("Invalid drive url")
val match =
DRIVE_FOLDER_REGEX.matchEntire(folderUrl) ?: throw Exception("Invalid drive url")
val recurDepth = match.groups["depth"]?.value ?: ""
val anime = SAnime.create().apply {
title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]") ?: "Folder"
title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]")
?: "Folder"
url = LinkData(
"https://drive.google.com/drive/folders/${match.groups["id"]!!.value}$recurDepth",
"multi",
@ -420,7 +355,60 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
return AnimesPage(listOf(anime), false)
}
private fun parsePage(request: Request, page: Int): AnimesPage {
private fun createPost(
document: Document,
folderId: String,
getMultiFormPath: (String, String, String) -> String = { folderIdStr, nextPageTokenStr, keyStr ->
defaultGetRequest(folderIdStr, nextPageTokenStr, keyStr)
},
): Request {
val keyScript = document.select("script").first { script ->
KEY_REGEX.find(script.data()) != null
}.data()
val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: ""
val versionScript = document.select("script").first { script ->
KEY_REGEX.find(script.data()) != null
}.data()
val driveVersion = VERSION_REGEX.find(versionScript)?.groupValues?.get(1) ?: ""
val sapisid =
client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: ""
val requestUrl = getMultiFormPath(folderId, nextPageToken ?: "", key)
val body = """--$BOUNDARY
|content-type: application/http
|content-transfer-encoding: binary
|
|GET $requestUrl
|X-Goog-Drive-Client-Version: $driveVersion
|authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0
|
|--$BOUNDARY--""".trimMargin("|")
.toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType())
val postUrl = buildString {
append("https://clients6.google.com/batch/drive/v2internal")
append("?${'$'}ct=multipart/mixed; boundary=\"$BOUNDARY\"")
append("&key=$key")
}
val postHeaders = headers.newBuilder().apply {
add("Content-Type", "text/plain; charset=UTF-8")
add("Origin", "https://drive.google.com")
add("Cookie", getCookie("https://drive.google.com"))
}.build()
return POST(postUrl, body = body, headers = postHeaders)
}
private fun parsePage(
request: Request,
page: Int,
genMultiFormReq: ((String, String, String) -> String)? = null,
): AnimesPage {
val animeList = mutableListOf<SAnime>()
val recurDepth = request.url.encodedFragment?.let { "#$it" } ?: ""
@ -437,47 +425,17 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
return AnimesPage(emptyList(), false)
}
val keyScript = driveDocument.select("script").first { script ->
KEY_REGEX.find(script.data()) != null
}.data()
val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: ""
val versionScript = driveDocument.select("script").first { script ->
KEY_REGEX.find(script.data()) != null
}.data()
val driveVersion = VERSION_REGEX.find(versionScript)?.groupValues?.get(1) ?: ""
val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: ""
if (page == 1) nextPageToken = ""
val requestUrl = "/drive/v2internal/files?openDrive=false&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2ChasVisitorPermissions%2CcontainsUnsubscribedChildren%2CmodifiedByMeDate%2ClastViewedByMeDate%2CalternateLink%2CfileSize%2Cowners(kind%2CpermissionId%2CemailAddressFromAccount%2Cdomain%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CcustomerId%2CancestorHasAugmentedPermissions%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2CabuseIsAppealable%2CabuseNoticeReason%2Cshared%2CaccessRequestsCount%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2Csubscribed%2CfolderColor%2ChasChildFolders%2CfileExtension%2CprimarySyncParentId%2CsharingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CflaggedForAbuse%2CfolderFeatures%2Cspaces%2CsourceAppId%2Crecency%2CrecencyReason%2Cversion%2CactionItems%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CprimaryDomainName%2CorganizationDisplayName%2CpassivelySubscribed%2CtrashingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CtrashedDate%2Cparents(id)%2Ccapabilities(canMoveItemIntoTeamDrive%2CcanUntrash%2CcanMoveItemWithinTeamDrive%2CcanMoveItemOutOfTeamDrive%2CcanDeleteChildren%2CcanTrashChildren%2CcanRequestApproval%2CcanReadCategoryMetadata%2CcanEditCategoryMetadata%2CcanAddMyDriveParent%2CcanRemoveMyDriveParent%2CcanShareChildFiles%2CcanShareChildFolders%2CcanRead%2CcanMoveItemWithinDrive%2CcanMoveChildrenWithinDrive%2CcanAddFolderFromAnotherDrive%2CcanChangeSecurityUpdateEnabled%2CcanBlockOwner%2CcanReportSpamOrAbuse%2CcanCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2CcontentRestrictions(readOnly)%2CapprovalMetadata(approvalVersion%2CapprovalSummaries%2ChasIncomingApproval)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus%2CtargetFile%2CcanRequestAccessToTarget)%2CspamMetadata(markedAsSpamDate%2CinSpamView)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$nextPageToken&maxResults=100&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1"
val body = """--$BOUNDARY
|content-type: application/http
|content-transfer-encoding: binary
|
|GET $requestUrl
|X-Goog-Drive-Client-Version: $driveVersion
|authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0
|
|--$BOUNDARY--""".trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType())
val postUrl = buildString {
append("https://clients6.google.com/batch/drive/v2internal")
append("?${'$'}ct=multipart/mixed; boundary=\"$BOUNDARY\"")
append("&key=$key")
val post = if (genMultiFormReq == null) {
createPost(driveDocument, folderId)
} else {
createPost(
driveDocument,
folderId,
genMultiFormReq,
)
}
val postHeaders = headers.newBuilder()
.add("Content-Type", "text/plain; charset=UTF-8")
.add("Origin", "https://drive.google.com")
.add("Cookie", getCookie("https://drive.google.com"))
.build()
val response = client.newCall(
POST(postUrl, body = body, headers = postHeaders),
).execute()
val response = client.newCall(post).execute()
val parsed = response.parseAs<PostResponse> {
JSON_REGEX.find(it)!!.groupValues[1]
@ -492,7 +450,10 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
url = LinkData(
"https://drive.google.com/uc?id=${it.id}",
"single",
LinkDataInfo(it.title, it.fileSize?.toLongOrNull()?.let { formatBytes(it) } ?: ""),
LinkDataInfo(
it.title,
it.fileSize?.toLongOrNull()?.let { formatBytes(it) } ?: "",
),
).toJsonString()
thumbnail_url = ""
},
@ -523,7 +484,10 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
}
// https://github.com/yt-dlp/yt-dlp/blob/8f0be90ecb3b8d862397177bb226f17b245ef933/yt_dlp/extractor/youtube.py#L573
private fun generateSapisidhashHeader(SAPISID: String, origin: String = "https://drive.google.com"): String {
private fun generateSapisidhashHeader(
SAPISID: String,
origin: String = "https://drive.google.com",
): String {
val timeNow = System.currentTimeMillis() / 1000
// SAPISIDHASH algorithm from https://stackoverflow.com/a/32065323
val sapisidhash = MessageDigest
@ -582,7 +546,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
editText.addTextChangedListener(
object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int,
) {
// Do nothing.
}
@ -600,7 +569,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
.map(String::trim)
.all(::isFolder)
editText.error = if (!isValid) "${text.split(";").first { !isFolder(it) }} is not a valid google drive folder" else null
editText.error = if (!isValid) {
"${
text.split(";").first { !isFolder(it) }
} is not a valid google drive folder"
} else {
null
}
editText.rootView.findViewById<Button>(android.R.id.button1)
?.isEnabled = editText.error == null
}
@ -631,6 +606,8 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
private val VERSION_REGEX = Regex(""""([^"]+web-frontend[^"]+)"""")
private val JSON_REGEX = Regex("""(?:)\s*(\{(.+)\})\s*(?:)""", RegexOption.DOT_MATCHES_ALL)
private const val BOUNDARY = "=====vc17a3rwnndj====="
private val ITEM_NUMBER_REGEX = Regex(""" - (?:S\d+E)?(\d+)\b""")
}
private val SharedPreferences.domainList
@ -669,8 +646,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(DOMAIN_PREF_KEY, newValue as String).commit()
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
val res =
preferences.edit().putString(DOMAIN_PREF_KEY, newValue as String).commit()
Toast.makeText(
screen.context,
"Restart Aniyomi to apply changes",
Toast.LENGTH_LONG,
).show()
res
} catch (e: java.lang.Exception) {
e.printStackTrace()

View File

@ -16,16 +16,6 @@ data class PostResponse(
)
}
@Serializable
data class Details(
val title: String? = null,
val author: String? = null,
val artist: String? = null,
val description: String? = null,
val genre: List<String>? = null,
val status: Int? = null,
)
@Serializable
data class LinkData(
val url: String,

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.animeextension.all.googledrive
fun searchReq(parentId: String, query: String): (String, String, String) -> String {
return { _: String, nextPageToken: String, key: String ->
"/drive/v2internal/files?openDrive=false&reason=111&syncType=0&errorRecovery=false&q=title%20contains%20'$query'%20and%20(mimeType%20in%20'application%2Fvnd.google-apps.folder'%20or%20shortcutDetails.targetMimeType%20in%20'application%2Fvnd.google-apps.folder')%20and%20trashed%20%3D%20false%20and%20'$parentId'%20in%20ancestors&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2ChasVisitorPermissions%2CcontainsUnsubscribedChildren%2CmodifiedByMeDate%2ClastViewedByMeDate%2CalternateLink%2CfileSize%2Cowners(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CcustomerId%2CancestorHasAugmentedPermissions%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2CabuseIsAppealable%2CabuseNoticeReason%2Cshared%2CaccessRequestsCount%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2Csubscribed%2CfolderColor%2ChasChildFolders%2CfileExtension%2CprimarySyncParentId%2CsharingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CflaggedForAbuse%2CfolderFeatures%2Cspaces%2CsourceAppId%2Crecency%2CrecencyReason%2Cversion%2CactionItems%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CprimaryDomainName%2CorganizationDisplayName%2CpassivelySubscribed%2CtrashingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CtrashedDate%2Cparents(id)%2Ccapabilities(canMoveItemIntoTeamDrive%2CcanUntrash%2CcanModifyContentRestriction%2CcanMoveItemWithinTeamDrive%2CcanMoveItemOutOfTeamDrive%2CcanDeleteChildren%2CcanTrashChildren%2CcanRequestApproval%2CcanReadCategoryMetadata%2CcanEditCategoryMetadata%2CcanAddMyDriveParent%2CcanRemoveMyDriveParent%2CcanShareChildFiles%2CcanShareChildFolders%2CcanRead%2CcanMoveItemWithinDrive%2CcanMoveChildrenWithinDrive%2CcanAddFolderFromAnotherDrive%2CcanChangeSecurityUpdateEnabled%2CcanBlockOwner%2CcanReportSpamOrAbuse%2CcanCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2CcontentRestrictions(readOnly)%2CapprovalMetadata(approvalVersion%2CapprovalSummaries%2ChasIncomingApproval)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus%2CtargetFile%2CcanRequestAccessToTarget)%2CspamMetadata(markedAsSpamDate%2CinSpamView)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$nextPageToken&maxResults=50&rawUserQuery=parent%3A$parentId%20type%3Afolder%20title%3A$query&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=relevance%20desc&retryCount=0&key=$key HTTP/1.1"
}
}
fun defaultGetRequest(folderId: String, nextPageToken: String, key: String): String {
return "/drive/v2internal/files?openDrive=false&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2ChasVisitorPermissions%2CcontainsUnsubscribedChildren%2CmodifiedByMeDate%2ClastViewedByMeDate%2CalternateLink%2CfileSize%2Cowners(kind%2CpermissionId%2CemailAddressFromAccount%2Cdomain%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CcustomerId%2CancestorHasAugmentedPermissions%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2CabuseIsAppealable%2CabuseNoticeReason%2Cshared%2CaccessRequestsCount%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2Csubscribed%2CfolderColor%2ChasChildFolders%2CfileExtension%2CprimarySyncParentId%2CsharingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CflaggedForAbuse%2CfolderFeatures%2Cspaces%2CsourceAppId%2Crecency%2CrecencyReason%2Cversion%2CactionItems%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CprimaryDomainName%2CorganizationDisplayName%2CpassivelySubscribed%2CtrashingUser(kind%2CpermissionId%2CemailAddressFromAccount%2Cid)%2CtrashedDate%2Cparents(id)%2Ccapabilities(canMoveItemIntoTeamDrive%2CcanUntrash%2CcanMoveItemWithinTeamDrive%2CcanMoveItemOutOfTeamDrive%2CcanDeleteChildren%2CcanTrashChildren%2CcanRequestApproval%2CcanReadCategoryMetadata%2CcanEditCategoryMetadata%2CcanAddMyDriveParent%2CcanRemoveMyDriveParent%2CcanShareChildFiles%2CcanShareChildFolders%2CcanRead%2CcanMoveItemWithinDrive%2CcanMoveChildrenWithinDrive%2CcanAddFolderFromAnotherDrive%2CcanChangeSecurityUpdateEnabled%2CcanBlockOwner%2CcanReportSpamOrAbuse%2CcanCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2CcontentRestrictions(readOnly)%2CapprovalMetadata(approvalVersion%2CapprovalSummaries%2ChasIncomingApproval)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus%2CtargetFile%2CcanRequestAccessToTarget)%2CspamMetadata(markedAsSpamDate%2CinSpamView)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$nextPageToken&maxResults=100&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1"
}