From 54b0b365be92d935385e7dfdb4b1345e1f2d6937 Mon Sep 17 00:00:00 2001 From: Secozzi <49240133+Secozzi@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:38:34 +0200 Subject: [PATCH] New extension: Google Drive (#1528) --- src/all/googledrive/AndroidManifest.xml | 2 + src/all/googledrive/build.gradle | 13 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2910 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1604 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3586 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6358 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 8342 bytes src/all/googledrive/res/web_hi_res_512.png | Bin 0 -> 31465 bytes .../all/googledrive/DataModel.kt | 40 ++ .../all/googledrive/GoogleDrive.kt | 624 ++++++++++++++++++ .../extractors/GoogleDriveExtractor.kt | 95 +++ 11 files changed, 774 insertions(+) create mode 100644 src/all/googledrive/AndroidManifest.xml create mode 100644 src/all/googledrive/build.gradle create mode 100644 src/all/googledrive/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/googledrive/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/googledrive/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/googledrive/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/googledrive/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/googledrive/res/web_hi_res_512.png create mode 100644 src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/DataModel.kt create mode 100644 src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt create mode 100644 src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/extractors/GoogleDriveExtractor.kt diff --git a/src/all/googledrive/AndroidManifest.xml b/src/all/googledrive/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/all/googledrive/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/all/googledrive/build.gradle b/src/all/googledrive/build.gradle new file mode 100644 index 000000000..b8fe5cc76 --- /dev/null +++ b/src/all/googledrive/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Google Drive' + pkgNameSuffix = 'all.googledrive' + extClass = '.GoogleDrive' + extVersionCode = 1 + libVersion = '13' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/googledrive/res/mipmap-hdpi/ic_launcher.png b/src/all/googledrive/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde6264211d08ceda780be168489fcbabf0b7002 GIT binary patch literal 2910 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 IM6N<$g2+vREdT%j literal 0 HcmV?d00001 diff --git a/src/all/googledrive/res/mipmap-mdpi/ic_launcher.png b/src/all/googledrive/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e335d7428ec73734c904472998f3610a69551877 GIT binary patch literal 1604 zcmV-K2D|x*P);c;tFwI6ffq`MjBN}Hwg8B!Ozf4$SmIwnMD2dZS^p8Q93^X`V z!sMYu&48FF{DJa^VQPTk7Q|;AEWplZtH7s~{4dU= z0`xm=nA`d$2CtgoyM0Fh@C)h#Ak?aI2C;h@jmG7u8a`sxdM<*h*G{QH7PiU(MjEIC<+KL{ceHY{|Q7w&ojeN)KlU1WVNc|mA2o;CO9tZ}_F?$VHz^N*10`luX4ysSivaB9iG4r;g;Sg@;Q&J(CuSdC zgMVGNu=OQz&-GbRp|atqsc)os<^?S!^1{EV=1q$WA>ja<&VPrGFMO{ko^`5RKAbeP zqikHTh?MN9!Tq&pJy5*y!2|RSSum&d4LH5-aQH|zeI|8f(TmujKM$DqgkwLL146<8 zybf4!4j5N-Pl$wipsJ%0M{b;oDn3JPz+cbrK@O{a+ol8F+Yt~V0PCHw9GG?J!2$ek z?ZQi^K8z}!4Q!q98rBxSEPSVL2hjVzBo8FwgI=5S3)R6DF(DG-0k7YOr+-_AbAy*< z`+OiG#N|!K$!LG>b7*+A(BmU7v_Az5bqk-xJG89MFwO$zbPh965P*G`f5tmq zyJX_!sp-ez`Snp(&+t&pK5q9?v{p4ueV`0T|ZGexvpbVq4oX zWV_j;t&}4?W5%u%;4?;^DU1q$8K>ftYv{P*k_!~q&CSk%(U>lMehF_j5jrc)KVtp) zt?~pCf2lUj!1$f}6^p!40Wj5V7l*Ov)9ZraBrectwaCvakS!`54}Dd9!_ij!b$c~T z_8!p~;mI;T+Y8OiClj9o!5ORf_v2uP{D3S!FCRLc5ITy&WuYP%ezxrln*V&A(q0r# zKh!RM0T~}OL`|Vldw|Vsws}ytsSkJD!oo&_0lB#oWx^x$%0fk&I}KeMu-I~3pe6%k zee(lU6Y`P^z-IsGCmS{%y+zL=941dH1RuQPN@G1JL!wxA!=n%R!#8l~^efPNoiJ3d zf#KCvQN^=?gm{2W#oX+0GP0xRh=6#b(raj${0u7I0dsOk=c! z0kAbJb`N%+rW(!4g^$S;tR5Lo(i~w=lVv$>)Xp0QpLOq}*n|SaG*yxY9|0uEuyIct zw+DPapM~u(W2v#z4xZ=ZIvdS)I-M7EI-PP&n<6Tg%QdLi>x*f5RzyWTUT0>{=E&bg!)Z5#8 zxTdD2zO%FQD!UCymr?>?52dUaXCY*yOG8(x5`x_h*#ihGz6R(TmWB@li6)HoGvVy1 zoB^d0fWf>Jk00004=H{B#0&Nr&As`CjnE*mwBr6G!_k+#u_WyThvXj}_otfQ*nPunZ%-QVhy>svV zzW@L4KmWaVMh99_6ak&0wQH4wMu5UXpb?-EpfCaoSFdS+Mu5TyC|td!0YORtOPofa zlR!JeBl{_tR=EIDemeBb!G4!}9mx$;c}4=9dssQ&B{f|Ra1PKnZrqqSb?VgVCX;EV z!C>gC)9I2W)pVO?vfJ&|R;%?wLqo%RJ9qBfF?;sx>)2HrbBOj@Dggw3*tTuk(x*?K zzBMc?ELZf(A()}Pz5V#!y?bX&o;0mt2WKe+ z81W)ejg5`}jfsgF9>Tav?s{``^9jUi3>Ij{!@>;Ag5|{sAoCM}M-1Ax^El`xNNz<4 zXT(9d7RSlE5rRfMZs0-Qkxv8|uv8=-aX8vnhA^hGyN=#rIu@$Oqtzb*!WaVLk$?)> ztq9=)NWh&80WJ7R9+6KPKmw1jQ}4&3uKYohJfnkYp}2MTPUlq0-K$JjGW|l zgYP_{)9E3@lmcVBkAT$JE`h{V^0k7V0E^8Ee?Gkt*6n#4&;`bzyL&i=&Ufac^LM(9 zuYPzOMD|&Rfpm(f+`1Ih1Q4(f9eN4Ax_T1As+*wwYMmPaV&D@uzfZ4&mousW5-=1|Ll*@ zt)&rizWg0jx79kbl2I|#wScwiT1Qba@JagDWL|-%y12?fcvpaX3vpWM*2EKoRs;TY zdK0X^xYGqT^Pj$?8f>jM9OD5$@yCa(=o@fwz(vlwOQuY>1pvL5i^)Jheh*3liW@87 zzWvWZn~g%~uB8U_zoFRz*0L*Z1n|Kpes=e-fYGrO31$;RfF2|3!wWFD-Y*~^d%H+b z5-_b`3FKEFa|564sG+hJ+Us1v3VBA4B{jk3p4_-0Ph*nC17v@u5x_@aUv(i&Dai8( zKA9e!)#ezGU2@F>7r$Q2{sB^qP7Ug;LmX|${trOX*qcg1P-#HB-3qz;7C>=hxq!Ei zFmmj^p{5b6H9~GFz@qf)uq5M0chkc+MP!0T97Pw4qU|>)1r-5nE`ADsJiS2_d{(G+ zKbXPR-Z{dXVzfeW)&($<-|9{JS-g9{3eau-%|Rcp8Z-o4yU_rH^M3=i?M;GU2)Ug4 z`c|-2UUkVeV!pYT8Gf1MbX|oB;F+tic!1n90LD9R3JF0&!2H5Bu(@onB=DV9-3g?j z{2ExBTF^WJjE-xC-F-`>gU@P6`fPxHYeScSqK0x9zVBJt;8Ouzs|D;OAhYC=&>kZTA*42DB9V5_bgmM z?*vH54R-V8`XJ z`~p8WX#o6h)N;Dnae_`@evdJpx`Pr;5mTmZie;urVHn>SYC(rP2a3w67G(VH-2%rh|@d*Br z>&1dy6N+r|!Kd$YdQ5^0!HRP`B!MsFzfi%R(W!89+*_`J*Ap%Tbin{zlYFsr4sUk9BPXZ!%*9yg zOTMA6CJngMWQJil$?lqF^aQbR6vru(cKGDpr7$5mM;ahkF}PRw5J%M{xTSoJ(c`N8 z+*8h*G#`>`G~kJEm%}Gj2L%^avHkK)i4%PC_dgnm5-BGYVrbzit_6}~dU!etDKh}> z*(#^jj=ZWMfTkHK;3n7Bvr)>{&5#EK-5DPmgk^Zk8D&LF@jhedmjH=B zl~=bKOpq1b&QZ9+;bi?K!N!Y8JzHo&uW7J+;M1}Ja1=QI0zeseXRQ=d$qQpGi3Q|TUfw(uWrz^h_leU9V@8jGpEpOS63$kpqNyP{7+13x( zEPGX*q6D<#!rFoPbD*N7rh@=Ez_W$cXD@(Xcl()d($AKv`X|7dIRe*^NC=G~(Y$hr@pXq`-qBgiQbnQIkT@w( z3qA!yx2)hGp!BK*`Y-mpsnY8Jj6cT3#z0aM+HPL*R89zde%__0@?e5lc=F#}A>3<9 zd=CjorM=hElA;mR6eoaYx_+^`3ih3&jpD+IZ#u$_VUUJuPJ6t> z%#_d-#e_M++6wO%JPxU?IP)cznW?}yWEjM~x>h^}#cPn_1Wk zW@^{25E&J7Q^05OnPA=nOHVBp;7@=NG=~XqZ-cN*bVFq(RS-aJer|g;tpA!G8sZ@= z!*nbVw{FqGVex;|SgV7i+*x5jV&vMeQ{w!=fG8-TLNN0rHQd|ljYl6hBpMf4feMzbz zfEvGce;qu(!*y58c8KkIyeSqE6W#aodh_k&N@W=7(z8_pTaWa5xD(w@o!)hu?-TG28z+FL2M80|uEU21AAuMv#fg~2 zRBY(p6B5wv)ET6Q9{AO$N&~2IUlg^#q}A>Z-RhE*1hKKsIbLtTd%)M(Zx8Aq=EBY@ zeHZ3mU;>7m%Hv*r6T)*w`YioaP;~@QqaIynhR;vB_5p+&jX<`a2HrqQy*IMB^@L%x zblc&X(5=aJ*B+ib8sh%y|HC0wNkG{(3l7L-_{;~VaHV!lhKNXd9FcJHb}!(~R~3_d zZf*C~d=0N1U+6rA_aBK{G31bv=9l!VRs*PETM8Or&Xy|><8V7u0}8$z;JI4zCc34= zt{;WiMN1%ZB4;ky4;oad0c?M^A8mjYUm2m{hWi6FZiUqIP1Im~9Nzo6#Srnp&;5YB zb5T_jKuvA2*x?A8-U~H$urR9$y@KxpG-89g5uKd7lXWoSE*(S#_+culC%^|hgQ|!| zfWmZX1ZV^(jDW(`YZ{;tpfCaoSFdTn?M8sjW-II1lcjLy)LuPJ$LjU^fIU0f@$~ej zrlwQT(b4#X3oVHu(AL&g7a19O59SNV)6;32NIbq5<@1!3lx(rewcqhsrPbAElarHa zO(hXP53%t1>FMknG)%j8?V3Ar;>4wz84?$<{QUf7B;w25l9jiQ3ro z^z<$V4jkB-nVH#Fya}P3qqMa2!jvgfrkyx(0@uEDyhFz0gt*wFWa)PbQ;``m&73*2 zZ(d&B8YH1#Lv?$82w+g4O*VUd zgAzd`rU7goTBu1tlSf2E&?l(?ohLKJzSJ{BVDIQA`_4!jWC?5noeq+G`cCJ)M%kqh zK(#quXNU)xBLbcd`kzZcsDV%YC2)xZ0-8(>y*ZB#`k!Vwyf21gBv&eM>?>@E2m+oy z=?GQnX9*w@2yFIXU*ahWK3zbn2CyV?-zOaE?-FJ)VVBpj+TdZiov@tCC4hSsZdre> zBUJ;{n2&4UeVS1XRp3-Zrbd9mRA>Zf1SpJv!qsaUpfCad2V-<@yA4CSfdBvi07*qo IM6N<$g0RWA%>V!Z literal 0 HcmV?d00001 diff --git a/src/all/googledrive/res/mipmap-xxhdpi/ic_launcher.png b/src/all/googledrive/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..75c56dbc2f9011d89f8bd5e923ecfc2d13b94785 GIT binary patch literal 6358 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 literal 0 HcmV?d00001 diff --git a/src/all/googledrive/res/web_hi_res_512.png b/src/all/googledrive/res/web_hi_res_512.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca28cb0759e00586b01dec00205789df51b4751 GIT binary patch literal 31465 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 UgMQiw0v_+)D8DXyW%Tv`0kxwCHUIzs literal 0 HcmV?d00001 diff --git a/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/DataModel.kt b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/DataModel.kt new file mode 100644 index 000000000..9a74656ed --- /dev/null +++ b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/DataModel.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.animeextension.all.googledrive + +import kotlinx.serialization.Serializable + +@Serializable +data class PostResponse( + val nextPageToken: String? = null, + val items: List? = null, +) { + @Serializable + data class ResponseItem( + val id: String, + val title: String, + val mimeType: String, + val fileSize: String? = null, + ) +} + +@Serializable +data class Details( + val title: String? = null, + val author: String? = null, + val artist: String? = null, + val description: String? = null, + val genre: List? = null, + val status: Int? = null, +) + +@Serializable +data class LinkData( + val url: String, + val type: String, + val info: LinkDataInfo? = null, +) + +@Serializable +data class LinkDataInfo( + val title: String, + val size: String, +) diff --git a/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt new file mode 100644 index 000000000..cc053f024 --- /dev/null +++ b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/GoogleDrive.kt @@ -0,0 +1,624 @@ +package eu.kanade.tachiyomi.animeextension.all.googledrive + +import android.app.Application +import android.content.SharedPreferences +import android.widget.Toast +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.animeextension.all.googledrive.extractors.GoogleDriveExtractor +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.AnimesPage +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.security.MessageDigest +import java.text.CharacterIterator +import java.text.StringCharacterIterator + +class GoogleDrive : ConfigurableAnimeSource, AnimeHttpSource() { + + override val name = "Google Drive (Experimental)" + + override var baseUrl = "" + + // Hack to manipulate what gets opened in webview + private val baseUrlInternal by lazy { + preferences.getString("domain_list", "")!!.split(";").first() + } + + override val lang = "all" + + private var nextPageToken: String? = "" + + override val supportsLatest = false + + private val driveFolderRegex = Regex("""(?\[[^\[\];]+\])?https?:\/\/(?:docs|drive)\.google\.com\/drive(?:\/u\/\d+)?\/folders\/(?[\w-]{28,})(?#[^;]+)?""") + private val keyRegex = """"(\w{39})"""".toRegex() + private val versionRegex = """"([^"]+web-frontend[^"]+)"""".toRegex() + private val jsonRegex = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL) + private val boundary = "=====vc17a3rwnndj=====" + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override val client: OkHttpClient = network.cloudflareClient + + // ============================== Popular =============================== + + override fun fetchPopularAnime(page: Int): Observable { + return Observable.just(parsePage(popularAnimeRequest(page), page)) + } + + override fun popularAnimeRequest(page: Int): Request { + val match = driveFolderRegex.matchEntire(baseUrlInternal)!! + val folderId = match.groups["id"]!!.value + val recurDepth = match.groups["depth"]?.value ?: "" + baseUrl = "https://drive.google.com/drive/folders/$folderId" + val driveHeaders = headers.newBuilder() + .add("Accept", "*/*") + .add("Connection", "keep-alive") + .add("Cookie", getCookie("https://drive.google.com")) + .add("Host", "drive.google.com") + .build() + + return GET("https://drive.google.com/drive/folders/$folderId$recurDepth", headers = driveHeaders) + } + + override fun popularAnimeParse(response: Response): AnimesPage = throw Exception("Not used") + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") + + override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("Not used") + + // =============================== Search =============================== + + override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used") + + override fun fetchSearchAnime( + page: Int, + query: String, + filters: AnimeFilterList, + ): Observable { + val req = searchAnimeRequest(page, query, filters) + return Observable.just(parsePage(req, page, query)) + } + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + if (baseUrlInternal.isEmpty()) { + throw Exception("Enter drive path(s) in extension settings.") + } + + val filterList = if (filters.isEmpty()) getFilterList() else filters + val serverFilter = filterList.find { it is ServerFilter } as ServerFilter + val serverUrl = serverFilter.toUriPart() + + val match = driveFolderRegex.matchEntire(serverUrl)!! + val folderId = match.groups["id"]!!.value + val recurDepth = match.groups["depth"]?.value ?: "" + baseUrl = "https://drive.google.com/drive/folders/$folderId" + val driveHeaders = headers.newBuilder() + .add("Accept", "*/*") + .add("Connection", "keep-alive") + .add("Cookie", getCookie("https://drive.google.com")) + .add("Host", "drive.google.com") + .build() + + return GET("https://drive.google.com/drive/folders/$folderId$recurDepth", headers = driveHeaders) + } + + // ============================== FILTERS =============================== + + override fun getFilterList(): AnimeFilterList = AnimeFilterList( + AnimeFilter.Header("Text search will only search inside selected server"), + ServerFilter(getDomains()), + ) + + private class ServerFilter(domains: Array>) : UriPartFilter( + "Select server", + domains, + ) + + private fun getDomains(): Array> { + return preferences.getString("domain_list", "")!!.split(";").map { + val name = driveFolderRegex.matchEntire(it)!!.groups["name"]?.let { + it.value.substringAfter("[").substringBeforeLast("]") + } + Pair(name ?: it.toHttpUrl().encodedPath, it) + }.toTypedArray() + } + + private open class UriPartFilter(displayName: String, val vals: Array>) : + AnimeFilter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + // =========================== Anime Details ============================ + + override fun fetchAnimeDetails(anime: SAnime): Observable { + val parsed = json.decodeFromString(anime.url) + val anime = anime + + if (parsed.type == "single") return Observable.just(anime) + + val folderId = driveFolderRegex.matchEntire(parsed.url)!!.groups["id"]!!.value + val driveHeaders = headers.newBuilder() + .add("Accept", "*/*") + .add("Connection", "keep-alive") + .add("Cookie", getCookie("https://drive.google.com")) + .add("Host", "drive.google.com") + .build() + + val driveDocument = client.newCall( + GET(parsed.url, headers = driveHeaders), + ).execute().asJsoup() + if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return Observable.just(anime) + + val keyScript = driveDocument.select("script").first { script -> + keyRegex.find(script.data()) != null + }.data() + val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" + + val versionScript = driveDocument.select("script").first { script -> + keyRegex.find(script.data()) != null + }.data() + val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" + val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull { + it.name == "SAPISID" || it.name == "__Secure-3PAPISID" + }?.value ?: "" + + var pageToken: String? = "" + while (pageToken != null) { + val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" + val body = """--$boundary + |content-type: application/http + |content-transfer-encoding: binary + | + |GET $requestUrl + |X-Goog-Drive-Client-Version: $driveVersion + |authorization: ${generateSapisidhashHeader(sapisid)} + |x-goog-authuser: 0 + | + |--$boundary + | + """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) + + val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() + .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") + .addQueryParameter("key", key) + .build() + .toString() + + val postHeaders = headers.newBuilder() + .add("Content-Type", "text/plain; charset=UTF-8") + .add("Origin", "https://drive.google.com") + .add("Cookie", getCookie("https://drive.google.com")) + .build() + + val response = client.newCall( + POST(postUrl, body = body, headers = postHeaders), + ).execute() + val parsed = json.decodeFromString( + jsonRegex.find(response.body.string())!!.groupValues[1], + ) + if (parsed.items == null) throw Exception("Failed to load items, please log in through webview") + parsed.items.forEach { + if (it.mimeType.startsWith("image/") && it.title.startsWith("cover.")) { + anime.thumbnail_url = "https://drive.google.com/uc?id=${it.id}" + } + } + + pageToken = parsed.nextPageToken + } + + return Observable.just(anime) + } + + override fun animeDetailsParse(response: Response): SAnime = throw Exception("Not used") + + // ============================== Episodes ============================== + + override fun fetchEpisodeList(anime: SAnime): Observable> { + val episodeList = mutableListOf() + val parsed = json.decodeFromString(anime.url) + + val maxRecursionDepth = parsed.url.toHttpUrl().encodedFragment?.toInt() ?: 2 + + fun traverseFolder(url: String, path: String, recursionDepth: Int = 0) { + if (recursionDepth == maxRecursionDepth) return + + val folderId = driveFolderRegex.matchEntire(url)!!.groups["id"]!!.value + val driveHeaders = headers.newBuilder() + .add("Accept", "*/*") + .add("Connection", "keep-alive") + .add("Cookie", getCookie("https://drive.google.com")) + .add("Host", "drive.google.com") + .build() + + val driveDocument = client.newCall( + GET(url, headers = driveHeaders), + ).execute().asJsoup() + if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return + + val keyScript = driveDocument.select("script").first { script -> + keyRegex.find(script.data()) != null + }.data() + val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" + + val versionScript = driveDocument.select("script").first { script -> + keyRegex.find(script.data()) != null + }.data() + val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" + val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull { + it.name == "SAPISID" || it.name == "__Secure-3PAPISID" + }?.value ?: "" + + var pageToken: String? = "" + while (pageToken != null) { + val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" + val body = """--$boundary + |content-type: application/http + |content-transfer-encoding: binary + | + |GET $requestUrl + |X-Goog-Drive-Client-Version: $driveVersion + |authorization: ${generateSapisidhashHeader(sapisid)} + |x-goog-authuser: 0 + | + |--$boundary + | + """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) + + val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() + .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") + .addQueryParameter("key", key) + .build() + .toString() + + val postHeaders = headers.newBuilder() + .add("Content-Type", "text/plain; charset=UTF-8") + .add("Origin", "https://drive.google.com") + .add("Cookie", getCookie("https://drive.google.com")) + .build() + + val response = client.newCall( + POST(postUrl, body = body, headers = postHeaders), + ).execute() + val parsed = json.decodeFromString( + jsonRegex.find(response.body.string())!!.groupValues[1], + ) + if (parsed.items == null) throw Exception("Failed to load items, please log in through webview") + parsed.items.forEachIndexed { index, it -> + if (it.mimeType.startsWith("video")) { + val episode = SEpisode.create() + val size = formatBytes(it.fileSize?.toLongOrNull()) + val pathName = if (preferences.getBoolean("trim_episode_info", false)) { + path.trimInfo() + } else { + path + } + + val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex() + episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) { + "/$pathName • $size" + } else { + "$size • /$pathName" + } + episode.name = if (preferences.getBoolean("trim_episode_name", false)) { + it.title.trimInfo() + } else { + it.title + } + episode.url = "https://drive.google.com/uc?id=${it.id}" + episode.episode_number = itemNumberRegex.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat() + episode.date_upload = -1L + episodeList.add(episode) + } + if (it.mimeType.endsWith(".folder")) { + traverseFolder( + "https://drive.google.com/drive/folders/${it.id}", + if (path.isEmpty()) it.title else "$path/${it.title}", + recursionDepth + 1, + ) + } + } + + pageToken = parsed.nextPageToken + } + } + + if (parsed.type == "single") { + val episode = SEpisode.create() + episode.name = parsed.info!!.title + episode.scanlator = parsed.info!!.size + episode.url = parsed.url + episode.episode_number = 1F + episode.date_upload = -1L + } else { + traverseFolder(parsed.url, "") + } + + return Observable.just(episodeList.reversed()) + } + + override fun episodeListParse(response: Response): List = throw Exception("Not used") + + // ============================ Video Links ============================= + + override fun fetchVideoList(episode: SEpisode): Observable> { + val videoList = GoogleDriveExtractor(client, headers).videosFromUrl(episode.url) + return Observable.just(videoList) + } + + // ============================= Utilities ============================== + + private fun parsePage(request: Request, page: Int, matches: String? = null): AnimesPage { + val animeList = mutableListOf() + + val recurDepth = request.url.encodedFragment?.let { "#$it" } ?: "" + + val folderId = driveFolderRegex.matchEntire(request.url.toString())!!.groups["id"]!!.value + val driveDocument = client.newCall(request).execute().asJsoup() + if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) { + return AnimesPage(emptyList(), false) + } + + val keyScript = driveDocument.select("script").first { script -> + keyRegex.find(script.data()) != null + }.data() + val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" + + val versionScript = driveDocument.select("script").first { script -> + keyRegex.find(script.data()) != null + }.data() + val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" + val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull { + it.name == "SAPISID" || it.name == "__Secure-3PAPISID" + }?.value ?: "" + + if (page == 1) nextPageToken = "" + val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$nextPageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" + val body = """--$boundary + |content-type: application/http + |content-transfer-encoding: binary + | + |GET $requestUrl + |X-Goog-Drive-Client-Version: $driveVersion + |authorization: ${generateSapisidhashHeader(sapisid)} + |x-goog-authuser: 0 + | + |--$boundary + | + """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) + + val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() + .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") + .addQueryParameter("key", key) + .build() + .toString() + + val postHeaders = headers.newBuilder() + .add("Content-Type", "text/plain; charset=UTF-8") + .add("Origin", "https://drive.google.com") + .add("Cookie", getCookie("https://drive.google.com")) + .build() + + val response = client.newCall( + POST(postUrl, body = body, headers = postHeaders), + ).execute() + val parsed = json.decodeFromString( + jsonRegex.find(response.body.string())!!.groupValues[1], + ) + if (parsed.items == null) throw Exception("Failed to load items, please log in through webview") + parsed.items.forEachIndexed { index, it -> + if (matches != null) { + if (!it.title.contains(matches, true)) return@forEachIndexed + } + + if (it.mimeType.startsWith("video")) { + val anime = SAnime.create() + anime.title = if (preferences.getBoolean("trim_anime_info", false)) { + it.title.trimInfo() + } else { + it.title + } + anime.setUrlWithoutDomain( + LinkData( + "https://drive.google.com/drive/folders/${it.id}", + "single", + LinkDataInfo(it.title, formatBytes(it.fileSize?.toLongOrNull()) ?: ""), + ).toJsonString(), + ) + anime.thumbnail_url = "" + animeList.add(anime) + } + if (it.mimeType.endsWith(".folder")) { + val anime = SAnime.create() + anime.title = if (preferences.getBoolean("trim_anime_info", false)) { + it.title.trimInfo() + } else { + it.title + } + anime.setUrlWithoutDomain( + LinkData( + "https://drive.google.com/drive/folders/${it.id}$recurDepth", + "multi", + ).toJsonString(), + ) + anime.thumbnail_url = "" + animeList.add(anime) + } + } + + nextPageToken = parsed.nextPageToken + + return AnimesPage(animeList, nextPageToken != null) + } + + // https://github.com/yt-dlp/yt-dlp/blob/8f0be90ecb3b8d862397177bb226f17b245ef933/yt_dlp/extractor/youtube.py#L573 + private fun generateSapisidhashHeader(SAPISID: String, origin: String = "https://drive.google.com"): String { + val timeNow = System.currentTimeMillis() / 1000 + // SAPISIDHASH algorithm from https://stackoverflow.com/a/32065323 + val sapisidhash = MessageDigest + .getInstance("SHA-1") + .digest("$timeNow $SAPISID $origin".toByteArray()) + .joinToString("") { "%02x".format(it) } + return "SAPISIDHASH ${timeNow}_$sapisidhash" + } + + private fun String.trimInfo(): String { + var newString = this.replaceFirst("""^\[\w+\] ?""".toRegex(), "") + val regex = """( ?\[[\s\w-]+\]| ?\([\s\w-]+\))(\.mkv|\.mp4|\.avi)?${'$'}""".toRegex() + + while (regex.containsMatchIn(newString)) { + newString = regex.replace(newString) { matchResult -> + matchResult.groups[2]?.value ?: "" + } + } + + return newString.trim() + } + + private fun formatBytes(bytes: Long?): String? { + if (bytes == null) return null + val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) + if (absB < 1024) { + return "$bytes B" + } + var value = absB + val ci: CharacterIterator = StringCharacterIterator("KMGTPE") + var i = 40 + while (i >= 0 && absB > 0xfffccccccccccccL shr i) { + value = value shr 10 + ci.next() + i -= 10 + } + value *= java.lang.Long.signum(bytes).toLong() + return java.lang.String.format("%.1f %cB", value / 1024.0, ci.current()) + } + + private fun getCookie(url: String): String { + val cookieList = client.cookieJar.loadForRequest(url.toHttpUrl()) + return if (cookieList.isNotEmpty()) { + cookieList.joinToString("; ") { "${it.name}=${it.value}" } + } else { + "" + } + } + + private fun LinkData.toJsonString(): String { + return json.encodeToString(this) + } + + private fun LinkDataInfo.toJsonString(): String { + return json.encodeToString(this) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + val domainListPref = EditTextPreference(screen.context).apply { + key = "domain_list" + title = "Enter drive paths to be shown in extension" + summary = """Enter drive paths to be shown in extension + |Enter as a semicolon `;` separated list + """.trimMargin() + this.setDefaultValue("") + dialogTitle = "Path list" + dialogMessage = """Separate paths with a semicolon. + |- (optional) Add [] before url to customize name. For example: [drive 5]https://drive.google.com/drive/folders/whatever + |- (optional) add # to limit the depth of recursion when loading epsiodes, defaults is 2. For example: https://drive.google.com/drive/folders/whatever#5 + """.trimMargin() + + setOnPreferenceChangeListener { _, newValue -> + try { + // Validate the urls + val domain = newValue as String + val domainList = domain.split(";") + var isValid = true + var message = "" + + domainList.forEach { d -> + if (message.isNotBlank()) return@forEach + val matchResult = driveFolderRegex.matchEntire(d) + if (matchResult == null) { + message = "Invalid url for $d" + isValid = false + } else { + matchResult.groups["depth"]?.let { + if (it.value.substringAfter("#").toIntOrNull() == null) { + isValid = false + message = "Level depth must be an integer, got `${it.value.substringAfter("#")}`" + } + } + } + } + + if (isValid) { + val res = preferences.edit().putString("domain_list", newValue as String).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) { + e.printStackTrace() + false + } + } + } + + val trimAnimeInfo = SwitchPreferenceCompat(screen.context).apply { + key = "trim_anime_info" + title = "Trim info from anime titles" + setDefaultValue(false) + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(key, newValue as Boolean).commit() + } + } + val trimEpisodeName = SwitchPreferenceCompat(screen.context).apply { + key = "trim_episode_name" + title = "Trim info from episode name" + setDefaultValue(true) + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(key, newValue as Boolean).commit() + } + } + val trimEpisodeInfo = SwitchPreferenceCompat(screen.context).apply { + key = "trim_episode_info" + title = "Trim info from episode info" + setDefaultValue(false) + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(key, newValue as Boolean).commit() + } + } + + screen.addPreference(trimAnimeInfo) + screen.addPreference(trimEpisodeName) + screen.addPreference(trimEpisodeInfo) + screen.addPreference(domainListPref) + } +} diff --git a/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/extractors/GoogleDriveExtractor.kt b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/extractors/GoogleDriveExtractor.kt new file mode 100644 index 000000000..01dc3e94b --- /dev/null +++ b/src/all/googledrive/src/eu/kanade/tachiyomi/animeextension/all/googledrive/extractors/GoogleDriveExtractor.kt @@ -0,0 +1,95 @@ +package eu.kanade.tachiyomi.animeextension.all.googledrive.extractors + +import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody + +class GoogleDriveExtractor(private val client: OkHttpClient, private val headers: Headers) { + // Needs to be the form of `https://drive.google.com/uc?id=GOOGLEDRIVEITEMID` + fun videosFromUrl(itemUrl: String, videoName: String = "Video"): List