fix(all/googledrive): New features, fixes, refactor (#1716)

This commit is contained in:
Secozzi
2023-06-12 14:21:54 +02:00
committed by GitHub
parent 6a1fb2c074
commit 868dd11c19
5 changed files with 295 additions and 200 deletions

View File

@ -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 [<insert name>]. 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.

View File

@ -6,7 +6,7 @@ ext {
extName = 'Google Drive'
pkgNameSuffix = 'all.googledrive'
extClass = '.GoogleDrive'
extVersionCode = 4
extVersionCode = 5
libVersion = '13'
}

View File

@ -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("""(?<name>\[[^\[\];]+\])?https?:\/\/(?:docs|drive)\.google\.com\/drive(?:\/[^\/]+)*?\/folders\/(?<id>[\w-]{28,})(?:\?[^;#]+)?(?<depth>#[^;]+)?""")
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<AnimesPage> {
return 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 {
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<AnimesPage> {
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<Pair<String, String>>) : UriPartFilter(
"Select server",
"Select drive path",
domains,
)
private fun getDomains(): Array<Pair<String, String>> {
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<SAnime> {
val parsed = json.decodeFromString<LinkData>(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<PostResponse>(
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<SEpisode>()
val parsed = json.decodeFromString<LinkData>(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<PostResponse>(
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<List<Video>> {
val videoList = GoogleDriveExtractor(client, headers).videosFromUrl(episode.url)
return Observable.just(videoList)
}
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> =
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<PostResponse>(
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<Button>(android.R.id.button1)
?.isEnabled = editText.error == null
}
},
)
}
companion object {
private const val DOMAIN_PREF_KEY = "domain_list"
private const val DOMAIN_PREF_DEFAULT = ""
private const val TRIM_ANIME_KEY = "trim_anime_info"
private const val TRIM_ANIME_DEFAULT = false
private const val TRIM_EPISODE_NAME_KEY = "trim_episode_name"
private const val TRIM_EPISODE_NAME_DEFAULT = true
private const val TRIM_EPISODE_INFO_KEY = "trim_episode_info"
private const val TRIM_EPISODE_INFO_DEFAULT = false
private const val SCANLATOR_ORDER_KEY = "scanlator_order"
private const val SCANLATOR_ORDER_DEFAULT = false
private val DRIVE_FOLDER_REGEX = Regex(
"""(?<name>\[[^\[\];]+\])?https?:\/\/(?:docs|drive)\.google\.com\/drive(?:\/[^\/]+)*?\/folders\/(?<id>[\w-]{28,})(?:\?[^;#]+)?(?<depth>#\d+(?<range>,\d+,\d+)?)?${'$'}""",
)
private val KEY_REGEX = Regex(""""(\w{39})"""")
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 SharedPreferences.domainList
get() = getString(DOMAIN_PREF_KEY, DOMAIN_PREF_DEFAULT)!!
private val SharedPreferences.trimAnimeInfo
get() = getBoolean(TRIM_ANIME_KEY, TRIM_ANIME_DEFAULT)
private val SharedPreferences.trimEpisodeName
get() = getBoolean(TRIM_EPISODE_NAME_KEY, TRIM_EPISODE_NAME_DEFAULT)
private val SharedPreferences.trimEpisodeInfo
get() = getBoolean(TRIM_EPISODE_INFO_KEY, TRIM_EPISODE_INFO_DEFAULT)
private val SharedPreferences.scanlatorOrder
get() = getBoolean(SCANLATOR_ORDER_KEY, SCANLATOR_ORDER_DEFAULT)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainListPref = EditTextPreference(screen.context).apply {
key = "domain_list"
EditTextPreference(screen.context).apply {
key = DOMAIN_PREF_KEY
title = "Enter drive paths to be shown in extension"
summary = """Enter links of drive folders to be shown in extension
|Enter as a semicolon `;` separated list
""".trimMargin()
this.setDefaultValue("")
this.setDefaultValue(DOMAIN_PREF_DEFAULT)
dialogTitle = "Path list"
dialogMessage = """Separate paths with a semicolon.
|- (optional) Add [] before url to customize name. For example: [drive 5]https://drive.google.com/drive/folders/whatever
|- (optional) add #<integer> to limit the depth of recursion when loading epsiodes, defaults is 2. For example: https://drive.google.com/drive/folders/whatever#5
|- (optional) add #<integer> to limit the depth of recursion when loading episodes, defaults is 2. For example: https://drive.google.com/drive/folders/whatever#5
|- (optional) add #depth,start,stop (all integers) to specify range when loading episodes. Only works if depth is 1. For example: https://drive.google.com/drive/folders/whatever#1,2,6
""".trimMargin()
setOnBindEditTextListener(::setupEditTextFolderValidator)
setOnPreferenceChangeListener { _, newValue ->
try {
// Validate the urls
val domain = newValue as String
val domainList = domain.split(";")
var isValid = true
var message = ""
domainList.forEach { d ->
if (message.isNotBlank()) return@forEach
val matchResult = driveFolderRegex.matchEntire(d)
if (matchResult == null) {
message = "Invalid url for $d"
isValid = false
} else {
matchResult.groups["depth"]?.let {
if (it.value.substringAfter("#").toIntOrNull() == null) {
isValid = false
message = "Level depth must be an integer, got `${it.value.substringAfter("#")}`"
}
}
}
}
if (isValid) {
val res = preferences.edit().putString("domain_list", newValue).commit()
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
res
} else {
Toast.makeText(screen.context, message, Toast.LENGTH_SHORT).show()
false
}
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()
false
}
}
}
}.also(screen::addPreference)
val trimAnimeInfo = SwitchPreferenceCompat(screen.context).apply {
key = "trim_anime_info"
SwitchPreferenceCompat(screen.context).apply {
key = TRIM_ANIME_KEY
title = "Trim info from anime titles"
setDefaultValue(false)
setDefaultValue(TRIM_ANIME_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
val trimEpisodeName = SwitchPreferenceCompat(screen.context).apply {
key = "trim_episode_name"
title = "Trim info from episode name"
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
val trimEpisodeInfo = SwitchPreferenceCompat(screen.context).apply {
key = "trim_episode_info"
title = "Trim info from episode info"
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
}.also(screen::addPreference)
screen.addPreference(trimAnimeInfo)
screen.addPreference(trimEpisodeName)
screen.addPreference(trimEpisodeInfo)
screen.addPreference(domainListPref)
SwitchPreferenceCompat(screen.context).apply {
key = TRIM_EPISODE_NAME_KEY
title = "Trim info from episode name"
setDefaultValue(TRIM_EPISODE_NAME_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = TRIM_EPISODE_INFO_KEY
title = "Trim info from episode info"
setDefaultValue(TRIM_EPISODE_INFO_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SCANLATOR_ORDER_KEY
title = "Switch order of file path and size"
setDefaultValue(SCANLATOR_ORDER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
}
}