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

View File

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

View File

@ -31,10 +31,12 @@ import okhttp3.ProtocolException
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.security.MessageDigest import java.security.MessageDigest
class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
@ -52,8 +54,6 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
override val lang = "all" override val lang = "all"
private var nextPageToken: String? = ""
override val supportsLatest = false override val supportsLatest = false
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -62,11 +62,22 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) 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 =============================== // ============================== 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 { override fun popularAnimeRequest(page: Int): Request {
require(!baseUrlInternal.isNullOrEmpty()) { "Enter drive path(s) in extension settings." } 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 folderId = match.groups["id"]!!.value
val recurDepth = match.groups["depth"]?.value ?: "" val recurDepth = match.groups["depth"]?.value ?: ""
baseUrl = "https://drive.google.com/drive/folders/$folderId" 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") override fun popularAnimeParse(response: Response): AnimesPage = throw Exception("Not used")
@ -102,14 +110,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
query: String, query: String,
filters: AnimeFilterList, filters: AnimeFilterList,
): Observable<AnimesPage> { ): 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 filterList = if (filters.isEmpty()) getFilterList() else filters
val urlFilter = filterList.find { it is URLFilter } as URLFilter val urlFilter = filterList.find { it is URLFilter } as URLFilter
return if (urlFilter.state.isEmpty()) { return if (urlFilter.state.isEmpty()) {
val req = searchAnimeRequest(page, query, filters) val req = searchAnimeRequest(page, query, filters)
Observable.just(parsePage(req, page))
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 { } else {
Observable.just(addSinglePage(urlFilter.state)) Observable.just(addSinglePage(urlFilter.state))
} }
@ -126,14 +141,11 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
val folderId = match.groups["id"]!!.value val folderId = match.groups["id"]!!.value
val recurDepth = match.groups["depth"]?.value ?: "" val recurDepth = match.groups["depth"]?.value ?: ""
baseUrl = "https://drive.google.com/drive/folders/$folderId" 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 =============================== // ============================== FILTERS ===============================
@ -171,7 +183,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
override fun animeDetailsRequest(anime: SAnime): Request { override fun animeDetailsRequest(anime: SAnime): Request {
val parsed = json.decodeFromString<LinkData>(anime.url) val parsed = json.decodeFromString<LinkData>(anime.url)
return GET(parsed.url) return GET(parsed.url, headers = getHeaders)
} }
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
@ -180,62 +192,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
if (parsed.type == "single") return Observable.just(anime) if (parsed.type == "single") return Observable.just(anime)
val folderId = DRIVE_FOLDER_REGEX.matchEntire(parsed.url)!!.groups["id"]!!.value 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 { val driveDocument = try {
client.newCall(GET(parsed.url, headers = driveHeaders)).execute().asJsoup() client.newCall(GET(parsed.url, headers = getHeaders)).execute().asJsoup()
} catch (a: ProtocolException) { } catch (a: ProtocolException) {
null null
} ?: return Observable.just(anime) } ?: return Observable.just(anime)
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != 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 ?: ""
var pageToken: String? = "" var pageToken: String? = ""
while (pageToken != null) { 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( val response = client.newCall(
POST(postUrl, body = body, headers = postHeaders), createPost(driveDocument, folderId),
).execute() ).execute()
val parsed = response.parseAs<PostResponse> { val parsed = response.parseAs<PostResponse> {
@ -263,6 +234,20 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
val episodeList = mutableListOf<SEpisode>() val episodeList = mutableListOf<SEpisode>()
val parsed = json.decodeFromString<LinkData>(anime.url) 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 match = DRIVE_FOLDER_REGEX.matchEntire(parsed.url)!! // .groups["id"]!!.value
val maxRecursionDepth = match.groups["depth"]?.let { val maxRecursionDepth = match.groups["depth"]?.let {
it.value.substringAfter("#").substringBefore(",").toInt() it.value.substringAfter("#").substringBefore(",").toInt()
@ -275,63 +260,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
if (recursionDepth == maxRecursionDepth) return if (recursionDepth == maxRecursionDepth) return
val folderId = DRIVE_FOLDER_REGEX.matchEntire(folderUrl)!!.groups["id"]!!.value 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 { val driveDocument = try {
client.newCall(GET(folderUrl, headers = driveHeaders)).execute().asJsoup() client.newCall(GET(folderUrl, headers = getHeaders)).execute().asJsoup()
} catch (a: ProtocolException) { } catch (a: ProtocolException) {
throw Exception("Unable to get items, check webview") throw Exception("Unable to get items, check webview")
} }
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return 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 pageToken: String? = ""
var counter = 1 var counter = 1
while (pageToken != null) { 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( val response = client.newCall(
POST(postUrl, body = body, headers = postHeaders), createPost(driveDocument, folderId),
).execute() ).execute()
val parsed = response.parseAs<PostResponse> { val parsed = response.parseAs<PostResponse> {
@ -342,7 +285,6 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
parsed.items.forEachIndexed { index, it -> parsed.items.forEachIndexed { index, it ->
if (it.mimeType.startsWith("video")) { if (it.mimeType.startsWith("video")) {
val size = it.fileSize?.toLongOrNull()?.let { formatBytes(it) } ?: "" val size = it.fileSize?.toLongOrNull()?.let { formatBytes(it) } ?: ""
val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex()
val pathName = if (preferences.trimEpisodeInfo) path.trimInfo() else path val pathName = if (preferences.trimEpisodeInfo) path.trimInfo() else path
if (start != null && maxRecursionDepth == 1 && counter < start) { if (start != null && maxRecursionDepth == 1 && counter < start) {
@ -353,9 +295,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
episodeList.add( episodeList.add(
SEpisode.create().apply { 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}" 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 date_upload = -1L
scanlator = if (preferences.scanlatorOrder) { scanlator = if (preferences.scanlatorOrder) {
"/$pathName$size" "/$pathName$size"
@ -379,19 +324,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
} }
} }
if (parsed.type == "single") { traverseFolder(parsed.url, "")
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()) return Observable.just(episodeList.reversed())
} }
@ -406,11 +339,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun addSinglePage(folderUrl: String): AnimesPage { 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 recurDepth = match.groups["depth"]?.value ?: ""
val anime = SAnime.create().apply { val anime = SAnime.create().apply {
title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]") ?: "Folder" title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]")
?: "Folder"
url = LinkData( url = LinkData(
"https://drive.google.com/drive/folders/${match.groups["id"]!!.value}$recurDepth", "https://drive.google.com/drive/folders/${match.groups["id"]!!.value}$recurDepth",
"multi", "multi",
@ -420,7 +355,60 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
return AnimesPage(listOf(anime), false) 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 animeList = mutableListOf<SAnime>()
val recurDepth = request.url.encodedFragment?.let { "#$it" } ?: "" val recurDepth = request.url.encodedFragment?.let { "#$it" } ?: ""
@ -437,47 +425,17 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
return AnimesPage(emptyList(), false) 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 = "" 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 post = if (genMultiFormReq == null) {
val body = """--$BOUNDARY createPost(driveDocument, folderId)
|content-type: application/http } else {
|content-transfer-encoding: binary createPost(
| driveDocument,
|GET $requestUrl folderId,
|X-Goog-Drive-Client-Version: $driveVersion genMultiFormReq,
|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 response = client.newCall(post).execute()
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 parsed = response.parseAs<PostResponse> { val parsed = response.parseAs<PostResponse> {
JSON_REGEX.find(it)!!.groupValues[1] JSON_REGEX.find(it)!!.groupValues[1]
@ -492,7 +450,10 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
url = LinkData( url = LinkData(
"https://drive.google.com/uc?id=${it.id}", "https://drive.google.com/uc?id=${it.id}",
"single", "single",
LinkDataInfo(it.title, it.fileSize?.toLongOrNull()?.let { formatBytes(it) } ?: ""), LinkDataInfo(
it.title,
it.fileSize?.toLongOrNull()?.let { formatBytes(it) } ?: "",
),
).toJsonString() ).toJsonString()
thumbnail_url = "" 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 // 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 val timeNow = System.currentTimeMillis() / 1000
// SAPISIDHASH algorithm from https://stackoverflow.com/a/32065323 // SAPISIDHASH algorithm from https://stackoverflow.com/a/32065323
val sapisidhash = MessageDigest val sapisidhash = MessageDigest
@ -582,7 +546,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
editText.addTextChangedListener( editText.addTextChangedListener(
object : TextWatcher { 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. // Do nothing.
} }
@ -600,7 +569,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
.map(String::trim) .map(String::trim)
.all(::isFolder) .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) editText.rootView.findViewById<Button>(android.R.id.button1)
?.isEnabled = editText.error == null ?.isEnabled = editText.error == null
} }
@ -631,6 +606,8 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
private val VERSION_REGEX = Regex(""""([^"]+web-frontend[^"]+)"""") private val VERSION_REGEX = Regex(""""([^"]+web-frontend[^"]+)"""")
private val JSON_REGEX = Regex("""(?:)\s*(\{(.+)\})\s*(?:)""", RegexOption.DOT_MATCHES_ALL) private val JSON_REGEX = Regex("""(?:)\s*(\{(.+)\})\s*(?:)""", RegexOption.DOT_MATCHES_ALL)
private const val BOUNDARY = "=====vc17a3rwnndj=====" private const val BOUNDARY = "=====vc17a3rwnndj====="
private val ITEM_NUMBER_REGEX = Regex(""" - (?:S\d+E)?(\d+)\b""")
} }
private val SharedPreferences.domainList private val SharedPreferences.domainList
@ -669,8 +646,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
try { try {
val res = preferences.edit().putString(DOMAIN_PREF_KEY, newValue as String).commit() val res =
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show() preferences.edit().putString(DOMAIN_PREF_KEY, newValue as String).commit()
Toast.makeText(
screen.context,
"Restart Aniyomi to apply changes",
Toast.LENGTH_LONG,
).show()
res res
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
e.printStackTrace() 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 @Serializable
data class LinkData( data class LinkData(
val url: String, 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"
}