From 868dd11c1984db3488bbc26b423befe8c3926dfe Mon Sep 17 00:00:00 2001 From: Secozzi <49240133+Secozzi@users.noreply.github.com> Date: Mon, 12 Jun 2023 14:21:54 +0200 Subject: [PATCH] fix(all/googledrive): New features, fixes, refactor (#1716) --- src/all/googledrive/README.md | 52 +++ src/all/googledrive/build.gradle | 2 +- .../all/googledrive/GoogleDrive.kt | 441 ++++++++++-------- .../{DataModel.kt => GoogleDriveDto.kt} | 0 .../{DataModel.kt => GoogleDriveIndexDto.kt} | 0 5 files changed, 295 insertions(+), 200 deletions(-) create mode 100644 src/all/googledrive/README.md rename src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/{DataModel.kt => GoogleDriveDto.kt} (100%) rename src/all/googledriveindex/src/eu/kanade/tachiyomi/animeextension/all/googledriveindex/{DataModel.kt => GoogleDriveIndexDto.kt} (100%) diff --git a/src/all/googledrive/README.md b/src/all/googledrive/README.md new file mode 100644 index 000000000..0ef9cb19f --- /dev/null +++ b/src/all/googledrive/README.md @@ -0,0 +1,52 @@ +# Google Drive + +Table of Content +- [FAQ](#FAQ) + - [How do i add entries?](#how-do-i-add-entries) + - [What are all these options for drive paths?](#what-are-all-these-options-for-drive-paths) + - [I added the drive paths but it still get "Enter drive path(s) in extension settings."](#i-added-the-drive-paths-but-it-still-get-enter-drive-paths-in-extension-settings) + - [I cannot log in through webview](i-cannot-log-in-through-webview) + +## FAQ + +### How do i add entries? +The Google Drive Extension *only* supports google drive folders, so no shared drives. If you have a folder, which contains sub-folders of an anime, such as: +``` +https://drive.google.com/drive/folders/some-long-id +├── anime1 +│ ├── episode 1.mkv +│ ├── episode 2.mkv +│ └── ... +└── anime2 + ├── episode 1.mkv + ├── episode 2.mkv + └── ... +``` +Then it you should go to extension settings, and add the url there. You can add multiple drive paths by separating them with a semicolon `;`. To select between the paths, open up the extension and click the filter, from there you can select a specific drive. + +If you instead have a folder that contains the episodes directly, such as: +``` +https://drive.google.com/drive/folders/some-long-id +├── episode 1.mkv +├── episode 2.mkv +└── ... +``` +Then you should open the extension, click filters, then paste the folder link in the `Add single folder` filter. + +### What are all these options for drive paths? +The extension allows for some options when adding the drive path: +1. You can customize the name of a drive path by prepending the url with []. This will change the display name when selecting different drive paths in filters. Example: `[Weekly episodes]https://drive.google.com/drive/folders/some-long-id` +2. You can limit the recursion depth by adding a `#` to the end of the url together with a number. If you set it to `1`, the extension will not go into any sub-folders when loading episodes. If you set it to `2`, the extension will traverse into any sub-folders, but not sub-folders of sub-folders, and so on and so forth. It's useful if one folder has a separate folder for each seasons that you want to traverse through, but if another folder has separate folder for openings/endings that you *don't* want to traverse through. Example: `https://drive.google.com/drive/folders/some-long-id#3` +3. It is also possible to specify a range of episodes to load. It needs to be added together with the recursion depth as seen in step 2. Note: it only works if the recursion depth is set to `1`. The range is inclusive, so doing #1,2,7 will load the 2nd up to, and including, the 7th item. Example: `https://drive.google.com/drive/folders/some-long-id#1,2,7` + +It is possible to mix these options, and they work for both ways to add folders. + +### I added the drive paths but it still get "Enter drive path(s) in extension settings." +This can be caused by the caching that Aniyomi does. Reinstalling the extension will fix this issue (reinstalling an extension does not remove any extension settings) + +### I cannot log in through webview +Google can sometimes think that webview isn't a secure browser, and will thus refuse to let you log in. There are a few things you can try to mitigate this: +1. In the top right, click the three dots then click `Clear cookies` +2. In the top right, click the three dots then click `Refresh` +3. Click the `Try again` button after the website doesn't let you log in + Try a combination of these steps, and after a few tries it should eventually let you log in. \ No newline at end of file diff --git a/src/all/googledrive/build.gradle b/src/all/googledrive/build.gradle index d54cefbb6..d5df5f1ca 100644 --- a/src/all/googledrive/build.gradle +++ b/src/all/googledrive/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Google Drive' pkgNameSuffix = 'all.googledrive' extClass = '.GoogleDrive' - extVersionCode = 4 + extVersionCode = 5 libVersion = '13' } diff --git a/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt index 29442a5a9..16dcd8017 100644 --- a/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt +++ b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt @@ -2,6 +2,10 @@ package eu.kanade.tachiyomi.animeextension.all.googledrive import android.app.Application import android.content.SharedPreferences +import android.text.Editable +import android.text.TextWatcher +import android.widget.Button +import android.widget.EditText import android.widget.Toast import androidx.preference.EditTextPreference import androidx.preference.PreferenceScreen @@ -32,8 +36,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.security.MessageDigest -import java.text.CharacterIterator -import java.text.StringCharacterIterator class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { @@ -45,7 +47,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { // Hack to manipulate what gets opened in webview private val baseUrlInternal by lazy { - preferences.getString("domain_list", "")!!.split(";").firstOrNull() + preferences.domainList.split(";").firstOrNull() } override val lang = "all" @@ -54,12 +56,6 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { override val supportsLatest = false - private val driveFolderRegex = Regex("""(?\[[^\[\];]+\])?https?:\/\/(?:docs|drive)\.google\.com\/drive(?:\/[^\/]+)*?\/folders\/(?[\w-]{28,})(?:\?[^;#]+)?(?#[^;]+)?""") - private val keyRegex = """"(\w{39})"""".toRegex() - private val versionRegex = """"([^"]+web-frontend[^"]+)"""".toRegex() - private val jsonRegex = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL) - private val boundary = "=====vc17a3rwnndj=====" - private val json: Json by injectLazy() private val preferences: SharedPreferences by lazy { @@ -70,16 +66,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { // ============================== Popular =============================== - override fun fetchPopularAnime(page: Int): Observable { - return Observable.just(parsePage(popularAnimeRequest(page), page)) - } + override fun fetchPopularAnime(page: Int): Observable = Observable.just(parsePage(popularAnimeRequest(page), page)) override fun popularAnimeRequest(page: Int): Request { - if (baseUrlInternal.isNullOrEmpty()) { - throw Exception("Enter drive path(s) in extension settings.") - } + require(!baseUrlInternal.isNullOrEmpty()) { "Enter drive path(s) in extension settings." } - val match = driveFolderRegex.matchEntire(baseUrlInternal!!)!! + val match = DRIVE_FOLDER_REGEX.matchEntire(baseUrlInternal!!)!! val folderId = match.groups["id"]!!.value val recurDepth = match.groups["depth"]?.value ?: "" baseUrl = "https://drive.google.com/drive/folders/$folderId" @@ -110,14 +102,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { query: String, filters: AnimeFilterList, ): Observable { - val req = searchAnimeRequest(page, query, filters) + 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 - if (query.isNotEmpty()) throw Exception("Search is disabled. Use search in webview and add it as a single folder in filters.") - return if (urlFilter.state.isEmpty()) { + val req = searchAnimeRequest(page, query, filters) Observable.just(parsePage(req, page)) } else { Observable.just(addSinglePage(urlFilter.state)) @@ -125,15 +116,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { } override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { - if (baseUrlInternal.isNullOrEmpty()) { - throw Exception("Enter drive path(s) in extension settings.") - } + require(!baseUrlInternal.isNullOrEmpty()) { "Enter drive path(s) in extension settings." } val filterList = if (filters.isEmpty()) getFilterList() else filters val serverFilter = filterList.find { it is ServerFilter } as ServerFilter val serverUrl = serverFilter.toUriPart() - val match = driveFolderRegex.matchEntire(serverUrl)!! + val match = DRIVE_FOLDER_REGEX.matchEntire(serverUrl)!! val folderId = match.groups["id"]!!.value val recurDepth = match.groups["depth"]?.value ?: "" baseUrl = "https://drive.google.com/drive/folders/$folderId" @@ -157,14 +146,14 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { ) private class ServerFilter(domains: Array>) : UriPartFilter( - "Select server", + "Select drive path", domains, ) private fun getDomains(): Array> { - if (preferences.getString("domain_list", "")!!.isBlank()) return emptyArray() - return preferences.getString("domain_list", "")!!.split(";").map { - val name = driveFolderRegex.matchEntire(it)!!.groups["name"]?.let { + if (preferences.domainList.isBlank()) return emptyArray() + return preferences.domainList.split(";").map { + val name = DRIVE_FOLDER_REGEX.matchEntire(it)!!.groups["name"]?.let { it.value.substringAfter("[").substringBeforeLast("]") } Pair(name ?: it.toHttpUrl().encodedPath, it) @@ -187,11 +176,10 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { override fun fetchAnimeDetails(anime: SAnime): Observable { val parsed = json.decodeFromString(anime.url) - val anime = anime if (parsed.type == "single") return Observable.just(anime) - val folderId = driveFolderRegex.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") @@ -208,14 +196,14 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return Observable.just(anime) val keyScript = driveDocument.select("script").firstOrNull { script -> - keyRegex.find(script.data()) != null + KEY_REGEX.find(script.data()) != null }?.data() ?: return Observable.just(anime) - val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" + val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: "" val versionScript = driveDocument.select("script").first { script -> - keyRegex.find(script.data()) != null + KEY_REGEX.find(script.data()) != null }.data() - val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" + 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 ?: "" @@ -223,7 +211,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { var pageToken: String? = "" while (pageToken != null) { val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" - val body = """--$boundary + val body = """--$BOUNDARY |content-type: application/http |content-transfer-encoding: binary | @@ -232,12 +220,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { |authorization: ${generateSapisidhashHeader(sapisid)} |x-goog-authuser: 0 | - |--$boundary + |--$BOUNDARY | - """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) + """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType()) val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() - .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") + .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$BOUNDARY\"") .addQueryParameter("key", key) .build() .toString() @@ -252,7 +240,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { POST(postUrl, body = body, headers = postHeaders), ).execute() val parsed = json.decodeFromString( - jsonRegex.find(response.body.string())!!.groupValues[1], + JSON_REGEX.find(response.body.string())!!.groupValues[1], ) if (parsed.items == null) throw Exception("Failed to load items, please log in through webview") parsed.items.forEach { @@ -275,12 +263,18 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { val episodeList = mutableListOf() val parsed = json.decodeFromString(anime.url) - val maxRecursionDepth = parsed.url.toHttpUrl().encodedFragment?.toInt() ?: 2 + val match = DRIVE_FOLDER_REGEX.matchEntire(parsed.url)!! // .groups["id"]!!.value + val maxRecursionDepth = match.groups["depth"]?.let { + it.value.substringAfter("#").substringBefore(",").toInt() + } ?: 2 + val (start, stop) = match.groups["range"]?.let { + it.value.substringAfter(",").split(",").map { it.toInt() } + } ?: listOf(null, null) - fun traverseFolder(url: String, path: String, recursionDepth: Int = 0) { + fun traverseFolder(folderUrl: String, path: String, recursionDepth: Int = 0) { if (recursionDepth == maxRecursionDepth) return - val folderId = driveFolderRegex.matchEntire(url)!!.groups["id"]!!.value + val folderId = DRIVE_FOLDER_REGEX.matchEntire(folderUrl)!!.groups["id"]!!.value val driveHeaders = headers.newBuilder() .add("Accept", "*/*") .add("Connection", "keep-alive") @@ -289,7 +283,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { .build() val driveDocument = try { - client.newCall(GET(url, headers = driveHeaders)).execute().asJsoup() + client.newCall(GET(folderUrl, headers = driveHeaders)).execute().asJsoup() } catch (a: ProtocolException) { throw Exception("Unable to get items, check webview") } @@ -297,22 +291,23 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return val keyScript = driveDocument.select("script").firstOrNull { script -> - keyRegex.find(script.data()) != null + KEY_REGEX.find(script.data()) != null }?.data() ?: throw Exception("Unknown error occured, check webview") - val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" + val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: "" val versionScript = driveDocument.select("script").first { script -> - keyRegex.find(script.data()) != null + KEY_REGEX.find(script.data()) != null }.data() - val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" + 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/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" - val body = """--$boundary + val body = """--$BOUNDARY |content-type: application/http |content-transfer-encoding: binary | @@ -321,12 +316,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { |authorization: ${generateSapisidhashHeader(sapisid)} |x-goog-authuser: 0 | - |--$boundary + |--$BOUNDARY | - """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) + """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType()) val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() - .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") + .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$BOUNDARY\"") .addQueryParameter("key", key) .build() .toString() @@ -341,34 +336,35 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { POST(postUrl, body = body, headers = postHeaders), ).execute() val parsed = json.decodeFromString( - jsonRegex.find(response.body.string())!!.groupValues[1], + JSON_REGEX.find(response.body.string())!!.groupValues[1], ) if (parsed.items == null) throw Exception("Failed to load items, please log in through webview") parsed.items.forEachIndexed { index, it -> if (it.mimeType.startsWith("video")) { - val episode = SEpisode.create() val size = formatBytes(it.fileSize?.toLongOrNull()) - val pathName = if (preferences.getBoolean("trim_episode_info", false)) { - path.trimInfo() - } else { - path - } - val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex() - episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) { - "/$pathName • $size" - } else { - "$size • /$pathName" + val pathName = if (preferences.trimEpisodeInfo) path.trimInfo() else path + + if (start != null && maxRecursionDepth == 1 && counter < start) { + counter++ + return@forEachIndexed } - episode.name = if (preferences.getBoolean("trim_episode_name", false)) { - it.title.trimInfo() - } else { - it.title - } - episode.url = "https://drive.google.com/uc?id=${it.id}" - episode.episode_number = itemNumberRegex.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat() - episode.date_upload = -1L - episodeList.add(episode) + if (stop != null && maxRecursionDepth == 1 && counter > stop) return + + episodeList.add( + SEpisode.create().apply { + 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() + date_upload = -1L + scanlator = if (preferences.scanlatorOrder) { + "/$pathName • $size" + } else { + "$size • /$pathName" + } + }, + ) + counter++ } if (it.mimeType.endsWith(".folder")) { traverseFolder( @@ -384,12 +380,15 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { } if (parsed.type == "single") { - val episode = SEpisode.create() - episode.name = parsed.info!!.title - episode.scanlator = parsed.info!!.size - episode.url = parsed.url - episode.episode_number = 1F - episode.date_upload = -1L + 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, "") } @@ -401,26 +400,23 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { // ============================ Video Links ============================= - override fun fetchVideoList(episode: SEpisode): Observable> { - val videoList = GoogleDriveExtractor(client, headers).videosFromUrl(episode.url) - return Observable.just(videoList) - } + override fun fetchVideoList(episode: SEpisode): Observable> = + Observable.just(GoogleDriveExtractor(client, headers).videosFromUrl(episode.url)) // ============================= Utilities ============================== private fun addSinglePage(folderUrl: String): AnimesPage { - val match = driveFolderRegex.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() - anime.title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]") ?: "Folder" - anime.setUrlWithoutDomain( - LinkData( + val anime = SAnime.create().apply { + title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]") ?: "Folder" + url = LinkData( "https://drive.google.com/drive/folders/${match.groups["id"]!!.value}$recurDepth", "multi", - ).toJsonString(), - ) - anime.thumbnail_url = "" + ).toJsonString() + thumbnail_url = "" + } return AnimesPage(listOf(anime), false) } @@ -429,7 +425,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { val recurDepth = request.url.encodedFragment?.let { "#$it" } ?: "" - val folderId = driveFolderRegex.matchEntire(request.url.toString())!!.groups["id"]!!.value + val folderId = DRIVE_FOLDER_REGEX.matchEntire(request.url.toString())!!.groups["id"]!!.value val driveDocument = try { client.newCall(request).execute().asJsoup() @@ -442,21 +438,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { } val keyScript = driveDocument.select("script").first { script -> - keyRegex.find(script.data()) != null + KEY_REGEX.find(script.data()) != null }.data() - val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" + val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: "" val versionScript = driveDocument.select("script").first { script -> - keyRegex.find(script.data()) != null + KEY_REGEX.find(script.data()) != null }.data() - val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" + 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/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$nextPageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" - val body = """--$boundary + val body = """--$BOUNDARY |content-type: application/http |content-transfer-encoding: binary | @@ -465,12 +461,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { |authorization: ${generateSapisidhashHeader(sapisid)} |x-goog-authuser: 0 | - |--$boundary + |--$BOUNDARY | - """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) + """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType()) val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() - .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") + .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$BOUNDARY\"") .addQueryParameter("key", key) .build() .toString() @@ -485,42 +481,34 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { POST(postUrl, body = body, headers = postHeaders), ).execute() val parsed = json.decodeFromString( - jsonRegex.find(response.body.string())!!.groupValues[1], + JSON_REGEX.find(response.body.string())!!.groupValues[1], ) if (parsed.items == null) throw Exception("Failed to load items, please log in through webview") parsed.items.forEachIndexed { index, it -> if (it.mimeType.startsWith("video")) { - val anime = SAnime.create() - anime.title = if (preferences.getBoolean("trim_anime_info", false)) { - it.title.trimInfo() - } else { - it.title - } - anime.setUrlWithoutDomain( - LinkData( - "https://drive.google.com/drive/folders/${it.id}", - "single", - LinkDataInfo(it.title, formatBytes(it.fileSize?.toLongOrNull()) ?: ""), - ).toJsonString(), + animeList.add( + SAnime.create().apply { + title = if (preferences.trimAnimeInfo) it.title.trimInfo() else it.title + url = LinkData( + "https://drive.google.com/uc?id=${it.id}", + "single", + LinkDataInfo(it.title, formatBytes(it.fileSize?.toLongOrNull()) ?: ""), + ).toJsonString() + thumbnail_url = "" + }, ) - anime.thumbnail_url = "" - animeList.add(anime) } if (it.mimeType.endsWith(".folder")) { - val anime = SAnime.create() - anime.title = if (preferences.getBoolean("trim_anime_info", false)) { - it.title.trimInfo() - } else { - it.title - } - anime.setUrlWithoutDomain( - LinkData( - "https://drive.google.com/drive/folders/${it.id}$recurDepth", - "multi", - ).toJsonString(), + animeList.add( + SAnime.create().apply { + title = if (preferences.trimAnimeInfo) it.title.trimInfo() else it.title + url = LinkData( + "https://drive.google.com/drive/folders/${it.id}$recurDepth", + "multi", + ).toJsonString() + thumbnail_url = "" + }, ) - anime.thumbnail_url = "" - animeList.add(anime) } } @@ -554,21 +542,14 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { } private fun formatBytes(bytes: Long?): String? { - if (bytes == null) return null - val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) - if (absB < 1024) { - return "$bytes B" + val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB") + var value = bytes?.toDouble() ?: return null + var i = 0 + while (value >= 1024 && i < units.size - 1) { + value /= 1024 + i++ } - var value = absB - val ci: CharacterIterator = StringCharacterIterator("KMGTPE") - var i = 40 - while (i >= 0 && absB > 0xfffccccccccccccL shr i) { - value = value shr 10 - ci.next() - i -= 10 - } - value *= java.lang.Long.signum(bytes).toLong() - return java.lang.String.format("%.1f %cB", value / 1024.0, ci.current()) + return String.format("%.1f %s", value, units[i]) } private fun getCookie(url: String): String { @@ -584,87 +565,149 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { return json.encodeToString(this) } + private fun isFolder(text: String) = DRIVE_FOLDER_REGEX matches text + + /* + * Stolen from the MangaDex manga extension + * + * This will likely need to be removed or revisited when the app migrates the + * extension preferences screen to Compose. + */ + private fun setupEditTextFolderValidator(editText: EditText) { + editText.addTextChangedListener( + object : TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + // Do nothing. + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + // Do nothing. + } + + override fun afterTextChanged(editable: Editable?) { + requireNotNull(editable) + + val text = editable.toString() + + val isValid = text.isBlank() || text + .split(";") + .map(String::trim) + .all(::isFolder) + + editText.error = if (!isValid) "${text.split(";").first { !isFolder(it) }} is not a valid google drive folder" else null + editText.rootView.findViewById