fix(all/googledrive): New features, fixes, refactor (#1716)
This commit is contained in:
52
src/all/googledrive/README.md
Normal file
52
src/all/googledrive/README.md
Normal 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.
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
"/$pathName • $size"
|
|
||||||
} else {
|
if (start != null && maxRecursionDepth == 1 && counter < start) {
|
||||||
"$size • /$pathName"
|
counter++
|
||||||
|
return@forEachIndexed
|
||||||
}
|
}
|
||||||
episode.name = if (preferences.getBoolean("trim_episode_name", false)) {
|
if (stop != null && maxRecursionDepth == 1 && counter > stop) return
|
||||||
it.title.trimInfo()
|
|
||||||
} else {
|
episodeList.add(
|
||||||
it.title
|
SEpisode.create().apply {
|
||||||
}
|
name = if (preferences.trimEpisodeName) it.title.trimInfo() else it.title
|
||||||
episode.url = "https://drive.google.com/uc?id=${it.id}"
|
url = "https://drive.google.com/uc?id=${it.id}"
|
||||||
episode.episode_number = itemNumberRegex.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
|
episode_number = itemNumberRegex.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
|
||||||
episode.date_upload = -1L
|
date_upload = -1L
|
||||||
episodeList.add(episode)
|
scanlator = if (preferences.scanlatorOrder) {
|
||||||
|
"/$pathName • $size"
|
||||||
|
} else {
|
||||||
|
"$size • /$pathName"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
counter++
|
||||||
}
|
}
|
||||||
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}",
|
||||||
}
|
"single",
|
||||||
anime.setUrlWithoutDomain(
|
LinkDataInfo(it.title, formatBytes(it.fileSize?.toLongOrNull()) ?: ""),
|
||||||
LinkData(
|
).toJsonString()
|
||||||
"https://drive.google.com/drive/folders/${it.id}",
|
thumbnail_url = ""
|
||||||
"single",
|
},
|
||||||
LinkDataInfo(it.title, formatBytes(it.fileSize?.toLongOrNull()) ?: ""),
|
|
||||||
).toJsonString(),
|
|
||||||
)
|
)
|
||||||
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
|
"https://drive.google.com/drive/folders/${it.id}$recurDepth",
|
||||||
}
|
"multi",
|
||||||
anime.setUrlWithoutDomain(
|
).toJsonString()
|
||||||
LinkData(
|
thumbnail_url = ""
|
||||||
"https://drive.google.com/drive/folders/${it.id}$recurDepth",
|
},
|
||||||
"multi",
|
|
||||||
).toJsonString(),
|
|
||||||
)
|
)
|
||||||
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
|
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||||
val domainList = domain.split(";")
|
res
|
||||||
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
|
|
||||||
}
|
|
||||||
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user