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' extName = 'Google Drive'
pkgNameSuffix = 'all.googledrive' pkgNameSuffix = 'all.googledrive'
extClass = '.GoogleDrive' extClass = '.GoogleDrive'
extVersionCode = 4 extVersionCode = 5
libVersion = '13' libVersion = '13'
} }

View File

@ -2,6 +2,10 @@ package eu.kanade.tachiyomi.animeextension.all.googledrive
import android.app.Application import android.app.Application
import android.content.SharedPreferences 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 android.widget.Toast
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
@ -32,8 +36,6 @@ 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.security.MessageDigest import java.security.MessageDigest
import java.text.CharacterIterator
import java.text.StringCharacterIterator
class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
@ -45,7 +47,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
// Hack to manipulate what gets opened in webview // Hack to manipulate what gets opened in webview
private val baseUrlInternal by lazy { private val baseUrlInternal by lazy {
preferences.getString("domain_list", "")!!.split(";").firstOrNull() preferences.domainList.split(";").firstOrNull()
} }
override val lang = "all" override val lang = "all"
@ -54,12 +56,6 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
override val supportsLatest = false 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 json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
@ -70,16 +66,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular =============================== // ============================== Popular ===============================
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> { override fun fetchPopularAnime(page: Int): Observable<AnimesPage> = Observable.just(parsePage(popularAnimeRequest(page), page))
return Observable.just(parsePage(popularAnimeRequest(page), page))
}
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request {
if (baseUrlInternal.isNullOrEmpty()) { require(!baseUrlInternal.isNullOrEmpty()) { "Enter drive path(s) in extension settings." }
throw Exception("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 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"
@ -110,14 +102,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
query: String, query: String,
filters: AnimeFilterList, filters: AnimeFilterList,
): Observable<AnimesPage> { ): 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 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
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()) { return if (urlFilter.state.isEmpty()) {
val req = searchAnimeRequest(page, query, filters)
Observable.just(parsePage(req, page)) Observable.just(parsePage(req, page))
} else { } else {
Observable.just(addSinglePage(urlFilter.state)) Observable.just(addSinglePage(urlFilter.state))
@ -125,15 +116,13 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
} }
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
if (baseUrlInternal.isNullOrEmpty()) { require(!baseUrlInternal.isNullOrEmpty()) { "Enter drive path(s) in extension settings." }
throw Exception("Enter drive path(s) in extension settings.")
}
val filterList = if (filters.isEmpty()) getFilterList() else filters val filterList = if (filters.isEmpty()) getFilterList() else filters
val serverFilter = filterList.find { it is ServerFilter } as ServerFilter val serverFilter = filterList.find { it is ServerFilter } as ServerFilter
val serverUrl = serverFilter.toUriPart() val serverUrl = serverFilter.toUriPart()
val match = driveFolderRegex.matchEntire(serverUrl)!! val match = DRIVE_FOLDER_REGEX.matchEntire(serverUrl)!!
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"
@ -157,14 +146,14 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
) )
private class ServerFilter(domains: Array<Pair<String, String>>) : UriPartFilter( private class ServerFilter(domains: Array<Pair<String, String>>) : UriPartFilter(
"Select server", "Select drive path",
domains, domains,
) )
private fun getDomains(): Array<Pair<String, String>> { private fun getDomains(): Array<Pair<String, String>> {
if (preferences.getString("domain_list", "")!!.isBlank()) return emptyArray() if (preferences.domainList.isBlank()) return emptyArray()
return preferences.getString("domain_list", "")!!.split(";").map { return preferences.domainList.split(";").map {
val name = driveFolderRegex.matchEntire(it)!!.groups["name"]?.let { val name = DRIVE_FOLDER_REGEX.matchEntire(it)!!.groups["name"]?.let {
it.value.substringAfter("[").substringBeforeLast("]") it.value.substringAfter("[").substringBeforeLast("]")
} }
Pair(name ?: it.toHttpUrl().encodedPath, it) Pair(name ?: it.toHttpUrl().encodedPath, it)
@ -187,11 +176,10 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
val parsed = json.decodeFromString<LinkData>(anime.url) val parsed = json.decodeFromString<LinkData>(anime.url)
val anime = anime
if (parsed.type == "single") return Observable.just(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() val driveHeaders = headers.newBuilder()
.add("Accept", "*/*") .add("Accept", "*/*")
.add("Connection", "keep-alive") .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) if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return Observable.just(anime)
val keyScript = driveDocument.select("script").firstOrNull { script -> val keyScript = driveDocument.select("script").firstOrNull { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}?.data() ?: return Observable.just(anime) }?.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 -> val versionScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.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 { val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID" it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: "" }?.value ?: ""
@ -223,7 +211,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
var pageToken: String? = "" var pageToken: String? = ""
while (pageToken != null) { 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 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-type: application/http
|content-transfer-encoding: binary |content-transfer-encoding: binary
| |
@ -232,12 +220,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
|authorization: ${generateSapisidhashHeader(sapisid)} |authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0 |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() 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) .addQueryParameter("key", key)
.build() .build()
.toString() .toString()
@ -252,7 +240,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
POST(postUrl, body = body, headers = postHeaders), POST(postUrl, body = body, headers = postHeaders),
).execute() ).execute()
val parsed = json.decodeFromString<PostResponse>( 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") if (parsed.items == null) throw Exception("Failed to load items, please log in through webview")
parsed.items.forEach { parsed.items.forEach {
@ -275,12 +263,18 @@ 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)
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 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() val driveHeaders = headers.newBuilder()
.add("Accept", "*/*") .add("Accept", "*/*")
.add("Connection", "keep-alive") .add("Connection", "keep-alive")
@ -289,7 +283,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
.build() .build()
val driveDocument = try { val driveDocument = try {
client.newCall(GET(url, headers = driveHeaders)).execute().asJsoup() client.newCall(GET(folderUrl, headers = driveHeaders)).execute().asJsoup()
} catch (a: ProtocolException) { } catch (a: ProtocolException) {
throw Exception("Unable to get items, check webview") 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 if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return
val keyScript = driveDocument.select("script").firstOrNull { script -> 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") }?.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 -> val versionScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.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 { val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID" it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: "" }?.value ?: ""
var pageToken: String? = "" var pageToken: String? = ""
var counter = 1
while (pageToken != null) { 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 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-type: application/http
|content-transfer-encoding: binary |content-transfer-encoding: binary
| |
@ -321,12 +316,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
|authorization: ${generateSapisidhashHeader(sapisid)} |authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0 |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() 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) .addQueryParameter("key", key)
.build() .build()
.toString() .toString()
@ -341,34 +336,35 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
POST(postUrl, body = body, headers = postHeaders), POST(postUrl, body = body, headers = postHeaders),
).execute() ).execute()
val parsed = json.decodeFromString<PostResponse>( 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") if (parsed.items == null) throw Exception("Failed to load items, please log in through webview")
parsed.items.forEachIndexed { index, it -> parsed.items.forEachIndexed { index, it ->
if (it.mimeType.startsWith("video")) { if (it.mimeType.startsWith("video")) {
val episode = SEpisode.create()
val size = formatBytes(it.fileSize?.toLongOrNull()) 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() val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex()
episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) { val pathName = if (preferences.trimEpisodeInfo) path.trimInfo() else path
if (start != null && maxRecursionDepth == 1 && counter < start) {
counter++
return@forEachIndexed
}
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" "/$pathName$size"
} else { } else {
"$size • /$pathName" "$size • /$pathName"
} }
episode.name = if (preferences.getBoolean("trim_episode_name", false)) { },
it.title.trimInfo() )
} else { counter++
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 (it.mimeType.endsWith(".folder")) { if (it.mimeType.endsWith(".folder")) {
traverseFolder( traverseFolder(
@ -384,12 +380,15 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
} }
if (parsed.type == "single") { if (parsed.type == "single") {
val episode = SEpisode.create() episodeList.add(
episode.name = parsed.info!!.title SEpisode.create().apply {
episode.scanlator = parsed.info!!.size name = parsed.info!!.title
episode.url = parsed.url scanlator = parsed.info.size
episode.episode_number = 1F url = parsed.url
episode.date_upload = -1L episode_number = 1F
date_upload = -1L
},
)
} else { } else {
traverseFolder(parsed.url, "") traverseFolder(parsed.url, "")
} }
@ -401,26 +400,23 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> { override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> =
val videoList = GoogleDriveExtractor(client, headers).videosFromUrl(episode.url) Observable.just(GoogleDriveExtractor(client, headers).videosFromUrl(episode.url))
return Observable.just(videoList)
}
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun addSinglePage(folderUrl: String): AnimesPage { 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 recurDepth = match.groups["depth"]?.value ?: ""
val anime = SAnime.create() val anime = SAnime.create().apply {
anime.title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]") ?: "Folder" title = match.groups["name"]?.value?.substringAfter("[")?.substringBeforeLast("]") ?: "Folder"
anime.setUrlWithoutDomain( url = LinkData(
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",
).toJsonString(), ).toJsonString()
) thumbnail_url = ""
anime.thumbnail_url = "" }
return AnimesPage(listOf(anime), false) return AnimesPage(listOf(anime), false)
} }
@ -429,7 +425,7 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
val recurDepth = request.url.encodedFragment?.let { "#$it" } ?: "" 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 { val driveDocument = try {
client.newCall(request).execute().asJsoup() client.newCall(request).execute().asJsoup()
@ -442,21 +438,21 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
} }
val keyScript = driveDocument.select("script").first { script -> val keyScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.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 -> val versionScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.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 { val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID" it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: "" }?.value ?: ""
if (page == 1) nextPageToken = "" 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 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-type: application/http
|content-transfer-encoding: binary |content-transfer-encoding: binary
| |
@ -465,12 +461,12 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
|authorization: ${generateSapisidhashHeader(sapisid)} |authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0 |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() 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) .addQueryParameter("key", key)
.build() .build()
.toString() .toString()
@ -485,42 +481,34 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
POST(postUrl, body = body, headers = postHeaders), POST(postUrl, body = body, headers = postHeaders),
).execute() ).execute()
val parsed = json.decodeFromString<PostResponse>( 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") if (parsed.items == null) throw Exception("Failed to load items, please log in through webview")
parsed.items.forEachIndexed { index, it -> parsed.items.forEachIndexed { index, it ->
if (it.mimeType.startsWith("video")) { if (it.mimeType.startsWith("video")) {
val anime = SAnime.create() animeList.add(
anime.title = if (preferences.getBoolean("trim_anime_info", false)) { SAnime.create().apply {
it.title.trimInfo() title = if (preferences.trimAnimeInfo) it.title.trimInfo() else it.title
} else { url = LinkData(
it.title "https://drive.google.com/uc?id=${it.id}",
}
anime.setUrlWithoutDomain(
LinkData(
"https://drive.google.com/drive/folders/${it.id}",
"single", "single",
LinkDataInfo(it.title, formatBytes(it.fileSize?.toLongOrNull()) ?: ""), LinkDataInfo(it.title, formatBytes(it.fileSize?.toLongOrNull()) ?: ""),
).toJsonString(), ).toJsonString()
thumbnail_url = ""
},
) )
anime.thumbnail_url = ""
animeList.add(anime)
} }
if (it.mimeType.endsWith(".folder")) { if (it.mimeType.endsWith(".folder")) {
val anime = SAnime.create() animeList.add(
anime.title = if (preferences.getBoolean("trim_anime_info", false)) { SAnime.create().apply {
it.title.trimInfo() title = if (preferences.trimAnimeInfo) it.title.trimInfo() else it.title
} else { url = LinkData(
it.title
}
anime.setUrlWithoutDomain(
LinkData(
"https://drive.google.com/drive/folders/${it.id}$recurDepth", "https://drive.google.com/drive/folders/${it.id}$recurDepth",
"multi", "multi",
).toJsonString(), ).toJsonString()
thumbnail_url = ""
},
) )
anime.thumbnail_url = ""
animeList.add(anime)
} }
} }
@ -554,21 +542,14 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
} }
private fun formatBytes(bytes: Long?): String? { private fun formatBytes(bytes: Long?): String? {
if (bytes == null) return null val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) var value = bytes?.toDouble() ?: return null
if (absB < 1024) { var i = 0
return "$bytes B" while (value >= 1024 && i < units.size - 1) {
value /= 1024
i++
} }
var value = absB return String.format("%.1f %s", value, units[i])
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())
} }
private fun getCookie(url: String): String { private fun getCookie(url: String): String {
@ -584,87 +565,149 @@ class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() {
return json.encodeToString(this) 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) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainListPref = EditTextPreference(screen.context).apply { EditTextPreference(screen.context).apply {
key = "domain_list" key = DOMAIN_PREF_KEY
title = "Enter drive paths to be shown in extension" title = "Enter drive paths to be shown in extension"
summary = """Enter links of drive folders to be shown in extension summary = """Enter links of drive folders to be shown in extension
|Enter as a semicolon `;` separated list |Enter as a semicolon `;` separated list
""".trimMargin() """.trimMargin()
this.setDefaultValue("") this.setDefaultValue(DOMAIN_PREF_DEFAULT)
dialogTitle = "Path list" dialogTitle = "Path list"
dialogMessage = """Separate paths with a semicolon. 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 [] 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() """.trimMargin()
setOnBindEditTextListener(::setupEditTextFolderValidator)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
try { try {
// Validate the urls val res = preferences.edit().putString(DOMAIN_PREF_KEY, newValue as String).commit()
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() Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
res res
} else {
Toast.makeText(screen.context, message, Toast.LENGTH_SHORT).show()
false
}
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
e.printStackTrace() e.printStackTrace()
false false
} }
} }
} }.also(screen::addPreference)
val trimAnimeInfo = SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = "trim_anime_info" key = TRIM_ANIME_KEY
title = "Trim info from anime titles" title = "Trim info from anime titles"
setDefaultValue(false) setDefaultValue(TRIM_ANIME_DEFAULT)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit() preferences.edit().putBoolean(key, newValue as Boolean).commit()
} }
} }.also(screen::addPreference)
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()
}
}
screen.addPreference(trimAnimeInfo) SwitchPreferenceCompat(screen.context).apply {
screen.addPreference(trimEpisodeName) key = TRIM_EPISODE_NAME_KEY
screen.addPreference(trimEpisodeInfo) title = "Trim info from episode name"
screen.addPreference(domainListPref) 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)
} }
} }