├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── 01_report_issue.yml │ ├── 02_request_source.yml │ ├── 03_report_url_change.yml │ ├── 04_report_dead_source.yml │ ├── 05_request_feature.yml │ ├── 06_request_meta.yml │ ├── 07_request_removal.yml │ └── config.yml ├── pull_request_template.md ├── readme-images │ └── app-icon.png ├── scripts │ ├── bump-versions.py │ ├── commit-repo.sh │ ├── create-repo.py │ ├── move-apks.py │ └── sign-apks.sh └── workflows │ ├── batch_close_issues.yml │ ├── build_pull_request.yml │ ├── build_push.yml │ ├── issue_moderator.yml │ └── lock.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── AndroidConfig.kt │ ├── Extensions.kt │ ├── lib-android.gradle.kts │ ├── lib-kotlin.gradle.kts │ └── lib-multisrc.gradle.kts ├── common.gradle ├── core ├── AndroidManifest.xml ├── build.gradle.kts └── res │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── mipmap-xxxhdpi │ └── ic_launcher.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ktlintCodeStyle.xml ├── lib-multisrc ├── animestream │ ├── AndroidManifest.xml │ ├── build.gradle.kts │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── multisrc │ │ └── animestream │ │ ├── AnimeStream.kt │ │ ├── AnimeStreamFilters.kt │ │ └── AnimeStreamUrlActivity.kt ├── datalifeengine │ ├── build.gradle.kts │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── multisrc │ │ └── datalifeengine │ │ └── DataLifeEngine.kt ├── dooplay │ ├── AndroidManifest.xml │ ├── build.gradle.kts │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── multisrc │ │ └── dooplay │ │ ├── DooPlay.kt │ │ └── DooPlayUrlActivity.kt ├── dopeflix │ ├── build.gradle.kts │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── multisrc │ │ └── dopeflix │ │ ├── DopeFlix.kt │ │ ├── DopeFlixFilters.kt │ │ ├── dto │ │ └── DopeFlixDto.kt │ │ └── extractors │ │ └── DopeFlixExtractor.kt └── zorotheme │ ├── build.gradle.kts │ └── src │ └── eu │ └── kanade │ └── tachiyomi │ └── multisrc │ └── zorotheme │ ├── ZoroTheme.kt │ ├── ZoroThemeFilters.kt │ └── dto │ └── ZoroThemeDto.kt ├── lib ├── blogger-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── bloggerextractor │ │ └── BloggerExtractor.kt ├── burstcloud-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── burstcloudextractor │ │ ├── BurstCloudExtractor.kt │ │ └── BurstCloudExtractorDto.kt ├── cda-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── cdaextractor │ │ └── CdaExtractor.kt ├── chillx-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── chillxextractor │ │ └── ChillxExtractor.kt ├── cloudflare-interceptor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── cloudflareinterceptor │ │ └── CloudflareInterceptor.kt ├── cryptoaes │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── cryptoaes │ │ ├── CryptoAES.kt │ │ └── Deobfuscator.kt ├── dailymotion-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── dailymotionextractor │ │ ├── DailymotionDto.kt │ │ └── DailymotionExtractor.kt ├── dataimage │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── dataimage │ │ └── DataImageInterceptor.kt ├── dood-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── doodextractor │ │ └── DoodExtractor.kt ├── fastream-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── fastreamextractor │ │ └── FastreamExtractor.kt ├── filemoon-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── filemoonextractor │ │ └── FilemoonExtractor.kt ├── fusevideo-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── fusevideoextractor │ │ └── FusevideoExtractor.kt ├── gdriveplayer-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── gdriveplayerextractor │ │ └── GdrivePlayerExtractor.kt ├── gogostream-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── gogostreamextractor │ │ ├── GogoStreamExtractor.kt │ │ └── GogoStreamExtractorDto.kt ├── googledrive-episodes │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── googledriveepisodes │ │ └── GoogleDriveEpisodes.kt ├── googledrive-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── googledriveextractor │ │ └── GoogleDriveExtractor.kt ├── javcoverfetcher │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── javcoverfetcher │ │ └── JavCoverFetcher.kt ├── megacloud-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── megacloudextractor │ │ └── MegaCloudExtractor.kt ├── mixdrop-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── mixdropextractor │ │ └── MixDropExtractor.kt ├── mp4upload-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── mp4uploadextractor │ │ └── Mp4uploadExtractor.kt ├── okru-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── okruextractor │ │ └── OkruExtractor.kt ├── playlist-utils │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── playlistutils │ │ └── PlaylistUtils.kt ├── sendvid-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── sendvidextractor │ │ └── SendvidExtractor.kt ├── sibnet-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── sibnetextractor │ │ └── SibnetExtractor.kt ├── streamdav-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── streamdavextractor │ │ └── StreamDavExtractor.kt ├── streamhidevid-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── streamhidevidextractor │ │ └── StreamHideVidExtractor.kt ├── streamhub-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── streamhubextractor │ │ └── StreamHubExtractor.kt ├── streamlare-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── streamlareextractor │ │ └── StreamlareExtractor.kt ├── streamtape-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── streamtapeextractor │ │ └── StreamTapeExtractor.kt ├── streamvid-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── streamvidextractor │ │ └── StreamVidExtractor.kt ├── streamwish-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── streamwishextractor │ │ └── StreamWishExtractor.kt ├── synchrony │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── assets │ │ └── synchrony-v2.4.5.1.js │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── synchrony │ │ └── Deobfuscator.kt ├── unpacker │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── unpacker │ │ ├── SubstringExtractor.kt │ │ └── Unpacker.kt ├── upstream-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── upstreamextractor │ │ └── UpstreamExtractor.kt ├── uqload-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── uqloadextractor │ │ └── UqloadExtractor.kt ├── vidbom-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── vidbomextractor │ │ └── VidBomExtractor.kt ├── vidhide-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── vidhideextractor │ │ └── VidHideExtractor.kt ├── vido-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── vidoextractor │ │ └── VidoExtractor.kt ├── vidsrc-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── vidsrcextractor │ │ └── VidSrcExtractor.kt ├── vk-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── vkextractor │ │ └── VkExtractor.kt ├── voe-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── voeextractor │ │ └── VoeExtractor.kt ├── vudeo-extractor │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── lib │ │ └── vudeoextractor │ │ └── VudeoExtractor.kt └── yourupload-extractor │ ├── build.gradle.kts │ └── src │ └── main │ └── java │ └── eu │ └── kanade │ └── tachiyomi │ └── lib │ └── youruploadextractor │ └── YourUploadExtractor.kt ├── renovate.json ├── repositories.gradle.kts ├── settings.gradle.kts ├── src ├── all │ ├── debridindex │ │ ├── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ └── src │ │ │ └── eu │ │ │ └── kanade │ │ │ └── tachiyomi │ │ │ └── animeextension │ │ │ └── all │ │ │ └── debridindex │ │ │ ├── DebirdIndexUrlActivity.kt │ │ │ ├── DebridIndex.kt │ │ │ └── dto │ │ │ └── DebridIndexDto.kt │ ├── hentaitorrent │ │ ├── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ └── src │ │ │ └── eu │ │ │ └── kanade │ │ │ └── tachiyomi │ │ │ └── animeextension │ │ │ └── all │ │ │ └── hentaitorrent │ │ │ ├── HentaiTorrent.kt │ │ │ └── HentaiTorrentUrlActivity.kt │ ├── nyaatorrent │ │ ├── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ └── src │ │ │ └── eu │ │ │ └── kanade │ │ │ └── tachiyomi │ │ │ └── animeextension │ │ │ └── all │ │ │ └── nyaatorrent │ │ │ ├── NyaaFactory.kt │ │ │ ├── NyaaTorrent.kt │ │ │ └── NyaaTorrentUrlActivity.kt │ ├── ptorrent │ │ ├── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ └── src │ │ │ └── eu │ │ │ └── kanade │ │ │ └── tachiyomi │ │ │ └── animeextension │ │ │ └── all │ │ │ └── ptorrent │ │ │ ├── PTorrent.kt │ │ │ └── PTorrentUrlActivity.kt │ ├── torrentio │ │ ├── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ └── src │ │ │ └── eu │ │ │ └── kanade │ │ │ └── tachiyomi │ │ │ └── animeextension │ │ │ └── all │ │ │ └── torrentio │ │ │ ├── Torrentio.kt │ │ │ ├── TorrentioUrlActivity.kt │ │ │ └── dto │ │ │ └── TorrentioDto.kt │ └── torrentioanime │ │ ├── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ └── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── animeextension │ │ └── all │ │ └── torrentioanime │ │ ├── AniListFilters.kt │ │ ├── AniListQueries.kt │ │ ├── Torrentio.kt │ │ ├── TorrentioUrlActivity.kt │ │ └── dto │ │ └── TorrentioDto.kt └── en │ ├── hanime │ ├── build.gradle │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ └── web_hi_res_500.png │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── animeextension │ │ └── en │ │ └── hanime │ │ ├── DataModel.kt │ │ └── Hanime.kt │ ├── hentaimama │ ├── build.gradle │ ├── ic_launcher-playstore.png │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ └── web_hi_res_500.png │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── animeextension │ │ └── en │ │ └── hentaimama │ │ └── HentaiMama.kt │ ├── hstream │ ├── AndroidManifest.xml │ ├── build.gradle │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ └── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── animeextension │ │ └── en │ │ └── hstream │ │ ├── Hstream.kt │ │ ├── HstreamFilters.kt │ │ └── HstreamUrlActivity.kt │ ├── oppaistream │ ├── build.gradle │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ └── web_hi_res_512.png │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── animeextension │ │ └── en │ │ └── oppaistream │ │ ├── OppaiStream.kt │ │ ├── OppaiStreamFilters.kt │ │ └── dto │ │ └── OppaiStreamDto.kt │ ├── rule34video │ ├── AndroidManifest.xml │ ├── build.gradle │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ └── web_hi_res_500.png │ └── src │ │ └── eu │ │ └── kanade │ │ └── tachiyomi │ │ └── animeextension │ │ └── en │ │ └── rule34video │ │ ├── DdosGuardInterceptor.kt │ │ ├── Rule34Video.kt │ │ └── Rule34VideoUrlActivity.kt │ └── subsplease │ ├── AndroidManifest.xml │ ├── build.gradle │ ├── res │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ └── mipmap-xxxhdpi │ │ └── ic_launcher.png │ └── src │ └── eu │ └── kanade │ └── tachiyomi │ └── animeextension │ └── en │ └── subsplease │ └── Subsplease.kt └── template ├── README-REMOVED-TEMPLATE.md └── README-TEMPLATE.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*.kt] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | ij_kotlin_allow_trailing_comma = true 12 | ij_kotlin_allow_trailing_comma_on_call_site = true 13 | ktlint_experimental = enabled 14 | ktlint_experimental_argument-list-wrapping = disabled # Doesn't play well with Android Studio 15 | ktlint_experimental_comment-wrapping = disabled 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **PLEASE READ THIS** 2 | 3 | I acknowledge that: 4 | 5 | - I have updated to the latest version of the app (stable is v0.15.2.2) 6 | - I have updated all extensions 7 | - If this is an issue with the app itself, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi 8 | - I have searched the existing issues for duplicates 9 | - For source requests, I have checked the list of existing extensions including the multi-source spreadsheet: https://aniyomi.org/extensions/ 10 | 11 | **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** 12 | 13 | --- 14 | 15 | ## Device information 16 | * Aniyomi version: ? 17 | * Android version: ? 18 | * Device: ? 19 | 20 | ## Source information 21 | * Source name: ? 22 | * Source extension version: ? 23 | 24 | ## Steps to reproduce 25 | 1. First step 26 | 2. Second step 27 | 28 | ## Issue/Request 29 | ? 30 | 31 | ## Other details 32 | Additional details and attachments. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_report_issue.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Issue report 2 | description: Report a source issue in Aniyomi 3 | labels: [Bug] 4 | body: 5 | 6 | - type: input 7 | id: source 8 | attributes: 9 | label: Source information 10 | description: | 11 | You can find the extension name and version in **Browse → Extensions**. 12 | placeholder: | 13 | Example: "AnimePahe 14.19 (English)" 14 | validations: 15 | required: true 16 | 17 | - type: input 18 | id: language 19 | attributes: 20 | label: Source language 21 | placeholder: | 22 | Example: "English" 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: reproduce-steps 28 | attributes: 29 | label: Steps to reproduce 30 | description: Provide an example of the issue. Be as specific as possible. 31 | placeholder: | 32 | Example: 33 | 1. First step (e.g. "Open Mahouka Koukou No Rettousei (first season)") 34 | 2. Second step (e.g. "Try to watch episode 15") 35 | 3. Issue here (e.g. "It shows a HTTP 403 error toast") 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: expected-behavior 41 | attributes: 42 | label: Expected behavior 43 | placeholder: | 44 | Example: 45 | "This should happen..." 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: actual-behavior 51 | attributes: 52 | label: Actual behavior 53 | placeholder: | 54 | Example: 55 | "This happened instead..." 56 | validations: 57 | required: true 58 | 59 | - type: input 60 | id: aniyomi-version 61 | attributes: 62 | label: Aniyomi version 63 | description: | 64 | You can find your Aniyomi version in **More → About**. 65 | placeholder: | 66 | Example: "0.15.2.4" or "Preview r7473" 67 | validations: 68 | required: true 69 | 70 | - type: input 71 | id: android-version 72 | attributes: 73 | label: Android version 74 | description: | 75 | You can find this somewhere in your Android settings. 76 | placeholder: | 77 | Example: "Android 11" 78 | validations: 79 | required: true 80 | 81 | - type: textarea 82 | id: other-details 83 | attributes: 84 | label: Other details 85 | placeholder: | 86 | Additional details and attachments. 87 | 88 | - type: checkboxes 89 | id: acknowledgements 90 | attributes: 91 | label: Acknowledgements 92 | description: Your issue will be closed if you haven't done these steps. 93 | options: 94 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue. 95 | required: true 96 | - label: I have written a short but informative title. 97 | required: true 98 | - label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**. 99 | required: true 100 | - label: I have updated all installed extensions. 101 | required: true 102 | - label: I have tried the [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/). 103 | required: true 104 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/aniyomiorg/aniyomi/issues/new/choose). 105 | required: true 106 | - label: I will fill out all of the requested information in this form. 107 | required: true 108 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_request_source.yml: -------------------------------------------------------------------------------- 1 | name: 🌐 Source request 2 | description: Suggest a new source for Aniyomi 3 | labels: [Source request] 4 | body: 5 | 6 | - type: input 7 | id: name 8 | attributes: 9 | label: Source name 10 | placeholder: | 11 | Example: "Not Real Source" 12 | validations: 13 | required: true 14 | 15 | - type: input 16 | id: link 17 | attributes: 18 | label: Source link 19 | placeholder: | 20 | Example: "https://notrealsource.org" 21 | validations: 22 | required: true 23 | 24 | - type: input 25 | id: language 26 | attributes: 27 | label: Source language 28 | placeholder: | 29 | Example: "English" 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | id: other-details 35 | attributes: 36 | label: Other details 37 | placeholder: | 38 | Additional details and attachments. 39 | Example: "+18/NSFW = yes" 40 | 41 | - type: checkboxes 42 | id: acknowledgements 43 | attributes: 44 | label: Acknowledgements 45 | description: Your issue will be closed if you haven't done these steps. 46 | options: 47 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue. 48 | required: true 49 | - label: I have written a title with source name. 50 | required: true 51 | - label: I have checked that the extension does not already exist on the [website extensions list](https://aniyomi.org/extensions/) or the app. 52 | required: true 53 | - label: I have checked that the extension does not already exist by searching the [GitHub repository](https://github.com/aniyomiorg/aniyomi-extensions/) and verified it does not appear in the code base. 54 | required: true 55 | - label: I will fill out all of the requested information in this form. 56 | required: true 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_report_url_change.yml: -------------------------------------------------------------------------------- 1 | name: 🔗 URL change report 2 | description: Report URL change of an existing source 3 | labels: [Bug,Domain changed] 4 | body: 5 | 6 | - type: input 7 | id: source 8 | attributes: 9 | label: Source information 10 | description: | 11 | You can find the extension name and version in **Browse → Extensions**. 12 | placeholder: | 13 | Example: "NotRealSource 14.1" 14 | validations: 15 | required: true 16 | 17 | - type: input 18 | id: language 19 | attributes: 20 | label: Source language 21 | placeholder: | 22 | Example: "English" 23 | validations: 24 | required: true 25 | 26 | - type: input 27 | id: link 28 | attributes: 29 | label: Source new URL 30 | placeholder: | 31 | Example: "https://notrealsource.org" 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | id: other-details 37 | attributes: 38 | label: Other details 39 | placeholder: | 40 | Additional details and attachments. 41 | 42 | - type: checkboxes 43 | id: acknowledgements 44 | attributes: 45 | label: Acknowledgements 46 | description: Your issue will be closed if you haven't done these steps. 47 | options: 48 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue. 49 | required: true 50 | - label: I have written a short but informative title. 51 | required: true 52 | - label: I have updated all installed extensions. 53 | required: true 54 | - label: I have opened WebView and checked that the source URL is not updated yet. 55 | required: true 56 | - label: I will fill out all of the requested information in this form. 57 | required: true 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04_report_dead_source.yml: -------------------------------------------------------------------------------- 1 | name: ❌ Dead source report 2 | description: Source is down and website is closed 3 | labels: [Source is down] 4 | body: 5 | 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ### Notice 10 | If you have a lot of dead sources to report, please go back and submit a single meta request. 11 | 12 | - type: input 13 | id: source 14 | attributes: 15 | label: Source name 16 | description: | 17 | You can find the extension name in **Browse → Extensions**. 18 | placeholder: | 19 | Example: "NotRealSource" 20 | validations: 21 | required: true 22 | 23 | - type: input 24 | id: language 25 | attributes: 26 | label: Source language 27 | placeholder: | 28 | Example: "English" 29 | validations: 30 | required: true 31 | 32 | - type: input 33 | id: link 34 | attributes: 35 | label: Source link 36 | placeholder: | 37 | Example: "https://notrealsource.org" 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: other-details 43 | attributes: 44 | label: Other details 45 | placeholder: | 46 | Additional details and attachments. 47 | 48 | - type: checkboxes 49 | id: acknowledgements 50 | attributes: 51 | label: Acknowledgements 52 | description: Your issue will be closed if you haven't done these steps. 53 | options: 54 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue. 55 | required: true 56 | - label: I have written a title with source name. 57 | required: true 58 | - label: I have updated all installed extensions. 59 | required: true 60 | - label: I have opened WebView and checked that the source website is down. 61 | required: true 62 | - label: I will fill out all of the requested information in this form. 63 | required: true 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/05_request_feature.yml: -------------------------------------------------------------------------------- 1 | name: ⭐ Feature request 2 | description: Suggest a feature to improve an existing source 3 | labels: [Feature request] 4 | body: 5 | 6 | - type: input 7 | id: source 8 | attributes: 9 | label: Source name 10 | description: | 11 | You can find the extension name in **Browse → Extensions**. 12 | placeholder: | 13 | Example: "DopeBox" 14 | validations: 15 | required: true 16 | 17 | - type: input 18 | id: language 19 | attributes: 20 | label: Source language 21 | placeholder: | 22 | Example: "English" 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: feature-description 28 | attributes: 29 | label: Describe your suggested feature 30 | description: How can an existing extension be improved? 31 | placeholder: | 32 | Example: 33 | "It should work like this..." 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: other-details 39 | attributes: 40 | label: Other details 41 | placeholder: | 42 | Additional details and attachments. 43 | 44 | - type: checkboxes 45 | id: acknowledgements 46 | attributes: 47 | label: Acknowledgements 48 | description: Your issue will be closed if you haven't done these steps. 49 | options: 50 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue. 51 | required: true 52 | - label: I have written a short but informative title. 53 | required: true 54 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/aniyomiorg/aniyomi/issues/new/choose). 55 | required: true 56 | - label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**. 57 | required: true 58 | - label: I will fill out all of the requested information in this form. 59 | required: true 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/06_request_meta.yml: -------------------------------------------------------------------------------- 1 | name: 🧠 Meta request 2 | description: Suggest improvements to the project 3 | labels: [Meta request] 4 | body: 5 | 6 | - type: textarea 7 | id: feature-description 8 | attributes: 9 | label: Describe why this should be added 10 | description: How can the project be improved? 11 | placeholder: | 12 | Example: 13 | "It should work like this..." 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: other-details 19 | attributes: 20 | label: Other details 21 | placeholder: | 22 | Additional details and attachments. 23 | 24 | - type: checkboxes 25 | id: acknowledgements 26 | attributes: 27 | label: Acknowledgements 28 | description: Your issue will be closed if you haven't done these steps. 29 | options: 30 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open (or closed) issue. 31 | required: true 32 | - label: I have written a short but informative title. 33 | required: true 34 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/aniyomiorg/aniyomi/issues/new/choose). 35 | required: true 36 | - label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**. 37 | required: true 38 | - label: I have updated all installed extensions. 39 | required: true 40 | - label: I will fill out all of the requested information in this form. 41 | required: true 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/07_request_removal.yml: -------------------------------------------------------------------------------- 1 | name: 🗑 Source removal request 2 | description: Scanlators can request their site to be removed 3 | labels: [Meta request] 4 | body: 5 | 6 | - type: input 7 | id: link 8 | attributes: 9 | label: Source link 10 | placeholder: | 11 | Example: "https://notrealscans.org" 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: other-details 17 | attributes: 18 | label: Other details 19 | placeholder: | 20 | Additional details and attachments. 21 | 22 | - type: checkboxes 23 | id: requirements 24 | attributes: 25 | label: Requirements 26 | description: Your request will be denied if you don't meet these requirements. 27 | options: 28 | - label: Proof of ownership/intent to remove sent to a Aniyomi Discord server mod via DM 29 | required: true 30 | - label: Site only hosts content scanlated by the group and not stolen from other scanlators or official releases (i.e., not an aggregator site) 31 | required: true 32 | - label: Site is not infested with user-hostile features (e.g., invasive or malicious ads) 33 | required: true 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ⚠️ Application issue 4 | url: https://github.com/aniyomiorg/aniyomi/issues/new/choose 5 | about: Issues and requests about the app itself should be opened in the tachiyomi repository instead 6 | - name: 📦 Aniyomi extensions 7 | url: https://aniyomi.org/extensions 8 | about: List of all available extensions with download links 9 | - name: 🖥️ Aniyomi website 10 | url: https://aniyomi.org/help/ 11 | about: Guides, troubleshooting, and answers to common questions 12 | - name: Aniyomi app GitHub repository 13 | url: https://github.com/aniyomiorg/aniyomi 14 | about: Issues about the app itself should be opened here instead. 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Checklist: 2 | 3 | - [ ] Updated `extVersionCode` value in `build.gradle` for individual extensions 4 | - [ ] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions 5 | - [ ] Referenced all related issues in the PR body (e.g. "Closes #xyz") 6 | - [ ] Added the `isNsfw = true` flag in `build.gradle` when appropriate 7 | - [ ] Have not changed source names 8 | - [ ] Have explicitly kept the `id` if a source's name or language were changed 9 | - [ ] Have tested the modifications by compiling and running the extension through Android Studio 10 | - [ ] Have removed `web_hi_res_512.png` when adding a new extension 11 | -------------------------------------------------------------------------------- /.github/readme-images/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni3x/aniyomi-extensions/485bcbe0dd990b31080cbaaee2e2ccbe10c10642/.github/readme-images/app-icon.png -------------------------------------------------------------------------------- /.github/scripts/bump-versions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from concurrent.futures import Future, ThreadPoolExecutor, as_completed 4 | import itertools 5 | from pathlib import Path 6 | import re 7 | import subprocess 8 | import sys 9 | 10 | VERSION_STR = "VersionCode =" 11 | VERSION_REGEX = re.compile(f"{VERSION_STR} (\\d+)") 12 | BUMPED_FILES: list[Path] = [] 13 | 14 | BOT_EMAIL = "aniyomi-bot@aniyomi.org" 15 | BOT_NAME = "aniyomi-bot[bot]" 16 | 17 | def has_match(query: str, file: Path) -> tuple[Path, bool]: 18 | return (file, query in file.read_text()) 19 | 20 | def find_files_with_match(query: str, include_multisrc: bool = True) -> list[Path]: 21 | files = Path("src").glob("*/*/build.gradle") 22 | if include_multisrc: 23 | files = itertools.chain(files, Path("lib-multisrc").glob("*/build.gradle.kts")) 24 | 25 | # Prevent bumping files twice. 26 | files = filter(lambda file: file not in BUMPED_FILES, files) 27 | 28 | # Use multiple threads to find matches. 29 | with ThreadPoolExecutor() as executor: 30 | futures = [executor.submit(has_match, query, file) for file in files] 31 | results = map(Future.result, as_completed(futures)) 32 | return [path for path, result in results if result] 33 | 34 | def replace_version(match: re.Match) -> str: 35 | version = int(match.group(1)) 36 | print(f"{version} -> {version + 1}") 37 | return f"{VERSION_STR} {version + 1}" 38 | 39 | def bump_version(file: Path): 40 | BUMPED_FILES.append(file) 41 | with file.open("r+") as f: 42 | print(f"\n{file}: ", end="") 43 | text = VERSION_REGEX.sub(replace_version, f.read()) 44 | # Move the cursor to the start again, to prevent writing at the end 45 | f.seek(0) 46 | f.write(text) 47 | 48 | def bump_lib_multisrc(theme: str): 49 | for file in find_files_with_match(f"themePkg = '{theme}'", include_multisrc=False): 50 | bump_version(file) 51 | 52 | def commit_changes(): 53 | paths = [str(path.resolve()) for path in BUMPED_FILES] 54 | subprocess.check_call(["git", "config", "--local", "user.email", BOT_EMAIL]) 55 | subprocess.check_call(["git", "config", "--local", "user.name", BOT_NAME]) 56 | subprocess.check_call(["git", "add"] + paths) 57 | subprocess.check_call(["git", "commit", "-S", "-m", "[skip ci] chore: Mass-bump on extensions"]) 58 | subprocess.check_call(["git", "push"]) 59 | 60 | if __name__ == "__main__": 61 | if len(sys.argv) > 1: 62 | # Regex to match the lib name in the path, like "unpacker" or "dood-extractor". 63 | lib_regex = re.compile(r"lib/([a-z0-9-]+)/") 64 | # Find matches and remove None results. 65 | matches = filter(None, map(lib_regex.search, sys.argv[1:])) 66 | for match in matches: 67 | project_path = ":lib:" + match.group(1) 68 | for file in find_files_with_match(project_path): 69 | if file.parent.parent.name == "lib-multisrc": 70 | bump_lib_multisrc(file.parent.name) 71 | else: 72 | bump_version(file) 73 | 74 | if len(BUMPED_FILES) > 0: 75 | commit_changes() 76 | -------------------------------------------------------------------------------- /.github/scripts/commit-repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | rsync -a --delete --exclude .git --exclude .gitignore --exclude repo.json ../master/repo/ . 5 | git config --global user.email "aniyomi-bot@aniyomi.org" 6 | git config --global user.name "aniyomi-bot[bot]" 7 | git status 8 | if [ -n "$(git status --porcelain)" ]; then 9 | git add . 10 | git commit -S -m "Update extensions repo" 11 | git push 12 | 13 | # Purge cached index on jsDelivr 14 | curl https://purge.jsdelivr.net/gh/ni3x/aniyomi-extensions@repo/index.min.json 15 | else 16 | echo "No changes to commit" 17 | fi 18 | -------------------------------------------------------------------------------- /.github/scripts/create-repo.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import subprocess 5 | from pathlib import Path 6 | from zipfile import ZipFile 7 | 8 | PACKAGE_NAME_REGEX = re.compile(r"package: name='([^']+)'") 9 | VERSION_CODE_REGEX = re.compile(r"versionCode='([^']+)'") 10 | VERSION_NAME_REGEX = re.compile(r"versionName='([^']+)'") 11 | IS_NSFW_REGEX = re.compile(r"'tachiyomi.animeextension.nsfw' value='([^']+)'") 12 | APPLICATION_LABEL_REGEX = re.compile(r"^application-label:'([^']+)'", re.MULTILINE) 13 | APPLICATION_ICON_320_REGEX = re.compile( 14 | r"^application-icon-320:'([^']+)'", re.MULTILINE 15 | ) 16 | LANGUAGE_REGEX = re.compile(r"aniyomi-([^\.]+)") 17 | 18 | *_, ANDROID_BUILD_TOOLS = (Path(os.environ["ANDROID_HOME"]) / "build-tools").iterdir() 19 | REPO_DIR = Path("repo") 20 | REPO_APK_DIR = REPO_DIR / "apk" 21 | REPO_ICON_DIR = REPO_DIR / "icon" 22 | 23 | REPO_ICON_DIR.mkdir(parents=True, exist_ok=True) 24 | 25 | with open("output.json", encoding="utf-8") as f: 26 | inspector_data = json.load(f) 27 | 28 | index_data = [] 29 | index_min_data = [] 30 | 31 | for apk in REPO_APK_DIR.iterdir(): 32 | badging = subprocess.check_output( 33 | [ 34 | ANDROID_BUILD_TOOLS / "aapt", 35 | "dump", 36 | "--include-meta-data", 37 | "badging", 38 | apk, 39 | ] 40 | ).decode() 41 | 42 | package_info = next(x for x in badging.splitlines() if x.startswith("package: ")) 43 | package_name = PACKAGE_NAME_REGEX.search(package_info).group(1) 44 | application_icon = APPLICATION_ICON_320_REGEX.search(badging).group(1) 45 | 46 | with ZipFile(apk) as z, z.open(application_icon) as i, ( 47 | REPO_ICON_DIR / f"{package_name}.png" 48 | ).open("wb") as f: 49 | f.write(i.read()) 50 | 51 | language = LANGUAGE_REGEX.search(apk.name).group(1) 52 | sources = inspector_data[package_name] 53 | 54 | if len(sources) == 1: 55 | source_language = sources[0]["lang"] 56 | 57 | if ( 58 | source_language != language 59 | and source_language not in {"all", "other"} 60 | and language not in {"all", "other"} 61 | ): 62 | language = source_language 63 | 64 | common_data = { 65 | "name": APPLICATION_LABEL_REGEX.search(badging).group(1), 66 | "pkg": package_name, 67 | "apk": apk.name, 68 | "lang": language, 69 | "code": int(VERSION_CODE_REGEX.search(package_info).group(1)), 70 | "version": VERSION_NAME_REGEX.search(package_info).group(1), 71 | "nsfw": int(IS_NSFW_REGEX.search(badging).group(1)), 72 | } 73 | min_data = { 74 | **common_data, 75 | "sources": [], 76 | } 77 | 78 | for source in sources: 79 | min_data["sources"].append( 80 | { 81 | "name": source["name"], 82 | "lang": source["lang"], 83 | "id": source["id"], 84 | "baseUrl": source["baseUrl"], 85 | } 86 | ) 87 | 88 | index_min_data.append(min_data) 89 | index_data.append( 90 | { 91 | **common_data, 92 | "hasReadme": 0, 93 | "hasChangelog": 0, 94 | "sources": sources, 95 | } 96 | ) 97 | 98 | index_data.sort(key=lambda x: x["pkg"]) 99 | index_min_data.sort(key=lambda x: x["pkg"]) 100 | 101 | with (REPO_DIR / "index.json").open("w", encoding="utf-8") as f: 102 | index_data_str = json.dumps(index_data, ensure_ascii=False, indent=2) 103 | 104 | print(index_data_str) 105 | f.write(index_data_str) 106 | 107 | with (REPO_DIR / "index.min.json").open("w", encoding="utf-8") as f: 108 | json.dump(index_min_data, f, ensure_ascii=False, separators=(",", ":")) 109 | -------------------------------------------------------------------------------- /.github/scripts/move-apks.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import shutil 3 | 4 | REPO_APK_DIR = Path("repo/apk") 5 | 6 | try: 7 | shutil.rmtree(REPO_APK_DIR) 8 | except FileNotFoundError: 9 | pass 10 | 11 | REPO_APK_DIR.mkdir(parents=True, exist_ok=True) 12 | 13 | for apk in (Path.home() / "apk-artifacts").glob("**/*.apk"): 14 | apk_name = apk.name.replace("-release.apk", ".apk") 15 | 16 | shutil.move(apk, REPO_APK_DIR / apk_name) 17 | -------------------------------------------------------------------------------- /.github/scripts/sign-apks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)" 5 | 6 | shopt -s globstar nullglob extglob 7 | APKS=( **/*".apk" ) 8 | 9 | # Fail if too little extensions seem to have been built 10 | if [ "${#APKS[@]}" -le "1" ]; then 11 | echo "Insufficient amount of APKs found. Please check the project configuration." 12 | exit 1; 13 | fi; 14 | 15 | # Take base64 encoded key input and put it into a file 16 | STORE_PATH=$PWD/signingkey.jks 17 | rm -f $STORE_PATH && touch $STORE_PATH 18 | echo $1 | base64 -d > $STORE_PATH 19 | 20 | STORE_ALIAS=$2 21 | export KEY_STORE_PASSWORD=$3 22 | export KEY_PASSWORD=$4 23 | 24 | DEST=$PWD/apk 25 | rm -rf $DEST && mkdir -p $DEST 26 | 27 | MAX_PARALLEL=4 28 | 29 | # Sign all of the APKs 30 | for APK in ${APKS[@]}; do 31 | ( 32 | BASENAME=$(basename $APK) 33 | APKNAME="${BASENAME%%+(-release*)}.apk" 34 | APKDEST="$DEST/$APKNAME" 35 | 36 | ${TOOLS}/zipalign -c -v -p 4 $APK 37 | 38 | cp $APK $APKDEST 39 | ${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:KEY_STORE_PASSWORD --key-pass env:KEY_PASSWORD $APKDEST 40 | ) & 41 | 42 | # Allow to execute up to $MAX_PARALLEL jobs in parallel 43 | if [[ $(jobs -r -p | wc -l) -ge $MAX_PARALLEL ]]; then 44 | wait -n 45 | fi 46 | done 47 | 48 | wait 49 | 50 | rm $STORE_PATH 51 | unset KEY_STORE_PASSWORD 52 | unset KEY_PASSWORD 53 | -------------------------------------------------------------------------------- /.github/workflows/batch_close_issues.yml: -------------------------------------------------------------------------------- 1 | name: "Batch close stale issues" 2 | 3 | on: 4 | # Monthly 5 | schedule: 6 | - cron: '0 0 1 * *' 7 | # Manual trigger 8 | workflow_dispatch: 9 | inputs: 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | # Close everything older than ~6 months 19 | days-before-issue-stale: 180 20 | days-before-issue-close: 0 21 | exempt-issue-labels: "do-not-autoclose,Meta request" 22 | close-issue-message: "In an effort to have a more manageable issue backlog, we're closing older requests that weren't addressed since there's a low chance of it being addressed if it hasn't already. If your request is still relevant, please [open a new request](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose)." 23 | close-issue-reason: not_planned 24 | ascending: true 25 | operations-per-run: 250 26 | -------------------------------------------------------------------------------- /.github/workflows/build_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR build check 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**' 7 | - '!**.md' 8 | - '!.github/**' 9 | - '.github/workflows/build_pull_request.yml' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | CI_CHUNK_SIZE: 65 17 | 18 | jobs: 19 | prepare: 20 | name: Prepare job 21 | runs-on: ubuntu-latest 22 | outputs: 23 | individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }} 24 | steps: 25 | - name: Clone repo 26 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 27 | 28 | - name: Validate Gradle Wrapper 29 | uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2 30 | 31 | - name: Get number of modules 32 | run: | 33 | set -x 34 | projects=(src/*/*) 35 | 36 | echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> $GITHUB_ENV 37 | 38 | - id: generate-matrices 39 | name: Create output matrices 40 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 41 | with: 42 | script: | 43 | const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES; 44 | const chunkSize = process.env.CI_CHUNK_SIZE; 45 | 46 | const numIndividualChunks = Math.ceil(numIndividualModules / chunkSize); 47 | 48 | console.log(`Individual modules: ${numIndividualModules} (${numIndividualChunks} chunks of ${chunkSize})`); 49 | 50 | core.setOutput('individualMatrix', { 'chunk': [...Array(numIndividualChunks).keys()] }); 51 | 52 | build_individual: 53 | name: Build individual modules 54 | needs: prepare 55 | runs-on: ubuntu-latest 56 | strategy: 57 | matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }} 58 | steps: 59 | - name: Checkout PR 60 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 61 | 62 | - name: Set up JDK 63 | uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4 64 | with: 65 | java-version: 17 66 | distribution: temurin 67 | 68 | - name: Set up Gradle 69 | uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3 70 | with: 71 | cache-read-only: true 72 | 73 | - name: Build extensions (chunk ${{ matrix.chunk }}) 74 | env: 75 | CI_CHUNK_NUM: ${{ matrix.chunk }} 76 | run: ./gradlew -p src assembleDebug 77 | -------------------------------------------------------------------------------- /.github/workflows/issue_moderator.yml: -------------------------------------------------------------------------------- 1 | name: Issue moderator 2 | 3 | on: 4 | issues: 5 | types: [opened, edited, reopened] 6 | issue_comment: 7 | types: [created] 8 | 9 | jobs: 10 | autoclose: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Moderate issues 14 | uses: aniyomiorg/issue-moderator-action@v2 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | duplicate-label: Duplicate 18 | 19 | duplicate-check-enabled: true 20 | duplicate-check-labels: | 21 | ["Source request", "Domain changed"] 22 | 23 | existing-check-enabled: true 24 | existing-check-labels: | 25 | ["Source request", "Domain changed"] 26 | 27 | auto-close-rules: | 28 | [ 29 | { 30 | "type": "body", 31 | "regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*", 32 | "message": "The acknowledgment section was not removed." 33 | }, 34 | { 35 | "type": "body", 36 | "regex": ".*\\* (Aniyomi version|Android version|Device): \\?.*", 37 | "message": "Requested information in the template was not filled out." 38 | }, 39 | { 40 | "type": "title", 41 | "regex": ".*(Source name|Short description).*", 42 | "message": "You did not fill out the description in the title." 43 | }, 44 | { 45 | "type": "both", 46 | "regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?().configureEach { 17 | kotlinOptions { 18 | jvmTarget = JavaVersion.VERSION_1_8.toString() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation(libs.gradle.agp) 7 | implementation(libs.gradle.kotlin) 8 | implementation(libs.gradle.kotlin.serialization) 9 | implementation(libs.gradle.kotlinter) 10 | } 11 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | apply(from = "../repositories.gradle.kts") 2 | 3 | dependencyResolutionManagement { 4 | versionCatalogs { 5 | create("libs") { 6 | from(files("../gradle/libs.versions.toml")) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/AndroidConfig.kt: -------------------------------------------------------------------------------- 1 | object AndroidConfig { 2 | const val compileSdk = 32 3 | const val minSdk = 21 4 | const val targetSdk = 32 5 | const val namespace = "eu.kanade.tachiyomi.animeextension" 6 | const val coreNamespace = "eu.kanade.tachiyomi.lib.core" 7 | const val multisrcNamespace = "eu.kanade.tachiyomi.lib.themesources" 8 | } 9 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Extensions.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.plugins.ExtensionAware 2 | import org.gradle.kotlin.dsl.extra 3 | 4 | var ExtensionAware.baseVersionCode: Int 5 | get() = extra.get("baseVersionCode") as Int 6 | set(value) = extra.set("baseVersionCode", value) 7 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/lib-android.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | id("kotlinx-serialization") 5 | } 6 | 7 | android { 8 | compileSdk = AndroidConfig.compileSdk 9 | 10 | defaultConfig { 11 | minSdk = AndroidConfig.minSdk 12 | } 13 | 14 | namespace = "eu.kanade.tachiyomi.lib.${name.replace("-", "")}" 15 | } 16 | 17 | versionCatalogs 18 | .named("libs") 19 | .findBundle("common") 20 | .ifPresent { common -> 21 | dependencies { 22 | compileOnly(common) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/lib-kotlin.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | kotlin("jvm") 4 | } 5 | 6 | versionCatalogs 7 | .named("libs") 8 | .findLibrary("kotlin-stdlib") 9 | .ifPresent { stdlib -> 10 | dependencies { 11 | compileOnly(stdlib) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/lib-multisrc.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | id("kotlinx-serialization") 5 | id("org.jmailen.kotlinter") 6 | } 7 | 8 | android { 9 | compileSdk = AndroidConfig.compileSdk 10 | 11 | defaultConfig { 12 | minSdk = AndroidConfig.minSdk 13 | } 14 | 15 | namespace = "eu.kanade.tachiyomi.multisrc.${project.name}" 16 | 17 | sourceSets { 18 | named("main") { 19 | manifest.srcFile("AndroidManifest.xml") 20 | java.setSrcDirs(listOf("src")) 21 | res.setSrcDirs(listOf("res")) 22 | assets.setSrcDirs(listOf("assets")) 23 | } 24 | } 25 | 26 | buildFeatures { 27 | resValues = false 28 | shaders = false 29 | } 30 | 31 | kotlinOptions { 32 | freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" 33 | } 34 | } 35 | 36 | versionCatalogs 37 | .named("libs") 38 | .findBundle("common") 39 | .ifPresent { common -> 40 | dependencies { 41 | compileOnly(common) 42 | } 43 | } 44 | 45 | tasks { 46 | preBuild { 47 | dependsOn(lintKotlin) 48 | } 49 | 50 | if (System.getenv("CI") != "true") { 51 | lintKotlin { 52 | dependsOn(formatKotlin) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /common.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlinx-serialization' 4 | apply plugin: 'org.jmailen.kotlinter' 5 | 6 | assert !ext.has("pkgNameSuffix") 7 | assert !ext.has("libVersion") 8 | 9 | assert extName.chars().max().asInt < 0x180 : "Extension name should be romanized" 10 | 11 | Project theme = ext.has("themePkg") ? project(":lib-multisrc:$themePkg") : null 12 | if (theme != null) evaluationDependsOn(theme.path) 13 | 14 | android { 15 | compileSdk 32 16 | namespace AndroidConfig.namespace 17 | 18 | sourceSets { 19 | main { 20 | manifest.srcFile "AndroidManifest.xml" 21 | java.srcDirs = ['src'] 22 | res.srcDirs = ['res'] 23 | } 24 | release { 25 | manifest.srcFile "AndroidManifest.xml" 26 | } 27 | debug { 28 | manifest.srcFile "AndroidManifest.xml" 29 | } 30 | } 31 | 32 | defaultConfig { 33 | minSdkVersion AndroidConfig.minSdk 34 | targetSdkVersion AndroidConfig.targetSdk 35 | applicationIdSuffix project.parent.name + "." + project.name 36 | versionCode theme == null ? extVersionCode : theme.baseVersionCode + overrideVersionCode 37 | versionName "14.$versionCode" 38 | base { 39 | archivesName = "aniyomi-$applicationIdSuffix-v$versionName" 40 | } 41 | assert extClass.startsWith(".") 42 | manifestPlaceholders = [ 43 | appName : "Aniyomi: $extName", 44 | extClass: extClass, 45 | nsfw : project.ext.find("isNsfw") ? 1 : 0, 46 | ] 47 | String baseUrl = project.ext.find("baseUrl") ?: "" 48 | if (theme != null && !baseUrl.isEmpty()) { 49 | def split = baseUrl.split("://") 50 | assert split.length == 2 51 | def path = split[1].split("/") 52 | manifestPlaceholders += [ 53 | SOURCEHOST : path[0], 54 | SOURCESCHEME: split[0], 55 | ] 56 | } 57 | 58 | } 59 | 60 | signingConfigs { 61 | release { 62 | storeFile rootProject.file("signingkey.jks") 63 | storePassword System.getenv("KEY_STORE_PASSWORD") 64 | keyAlias System.getenv("ALIAS") 65 | keyPassword System.getenv("KEY_PASSWORD") 66 | } 67 | } 68 | 69 | buildTypes { 70 | release { 71 | signingConfig signingConfigs.release 72 | minifyEnabled false 73 | } 74 | } 75 | 76 | dependenciesInfo { 77 | includeInApk = false 78 | } 79 | 80 | buildFeatures { 81 | // Disable unused AGP features 82 | aidl false 83 | renderScript false 84 | resValues false 85 | shaders false 86 | buildConfig true 87 | } 88 | 89 | compileOptions { 90 | sourceCompatibility = JavaVersion.VERSION_1_8 91 | targetCompatibility = JavaVersion.VERSION_1_8 92 | } 93 | 94 | kotlinOptions { 95 | jvmTarget = JavaVersion.VERSION_1_8.toString() 96 | freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" 97 | } 98 | } 99 | 100 | dependencies { 101 | if (theme != null) implementation(theme) // Overrides core launcher icons 102 | implementation(project(":core")) 103 | compileOnly(libs.bundles.common) 104 | } 105 | 106 | configurations.all { 107 | resolutionStrategy.eachDependency { DependencyResolveDetails details -> 108 | if (details.requested.group == 'org.jetbrains.kotlin' && details.requested.name == 'kotlin-stdlib-jdk8' && details.requested.version == '1.7.0') { 109 | details.useVersion(libs.versions.kotlin.version.get()) 110 | details.because 'Fix problems with dev.datlag JsUnpacker' 111 | } 112 | } 113 | } 114 | 115 | tasks.register("writeManifestFile") { 116 | doLast { 117 | def manifest = android.sourceSets.getByName("main").manifest 118 | if (!manifest.srcFile.exists()) { 119 | File tempFile = layout.buildDirectory.get().file("tempAndroidManifest.xml").getAsFile() 120 | if (!tempFile.exists()) { 121 | tempFile.withWriter { 122 | it.write('\n\n') 123 | } 124 | } 125 | manifest.srcFile(tempFile.path) 126 | } 127 | } 128 | } 129 | 130 | preBuild.dependsOn(writeManifestFile, lintKotlin) 131 | if (System.getenv("CI") != "true") { 132 | lintKotlin.dependsOn(formatKotlin) 133 | } 134 | -------------------------------------------------------------------------------- /core/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | } 4 | 5 | android { 6 | compileSdk = 32 7 | namespace = AndroidConfig.coreNamespace 8 | 9 | defaultConfig { 10 | minSdk = AndroidConfig.minSdk 11 | } 12 | 13 | sourceSets { 14 | named("main") { 15 | manifest.srcFile("AndroidManifest.xml") 16 | res.setSrcDirs(listOf("res")) 17 | } 18 | } 19 | 20 | libraryVariants.all { 21 | generateBuildConfigProvider?.configure { 22 | enabled = false 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni3x/aniyomi-extensions/485bcbe0dd990b31080cbaaee2e2ccbe10c10642/core/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni3x/aniyomi-extensions/485bcbe0dd990b31080cbaaee2e2ccbe10c10642/core/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni3x/aniyomi-extensions/485bcbe0dd990b31080cbaaee2e2ccbe10c10642/core/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni3x/aniyomi-extensions/485bcbe0dd990b31080cbaaee2e2ccbe10c10642/core/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni3x/aniyomi-extensions/485bcbe0dd990b31080cbaaee2e2ccbe10c10642/core/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx5120m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | org.gradle.parallel=true 18 | org.gradle.workers.max=5 19 | 20 | org.gradle.caching=true 21 | 22 | # Enable AndroidX dependencies 23 | android.useAndroidX=true 24 | android.nonTransitiveRClass=false 25 | android.nonFinalResIds=false 26 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp_version = "8.2.1" 3 | coroutines_version = "1.7.1" 4 | kotlin_version = "1.8.22" 5 | serialization_version = "1.5.1" 6 | 7 | [libraries] 8 | gradle-agp = { module = "com.android.tools.build:gradle", version.ref = "agp_version" } 9 | gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" } 10 | gradle-kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" } 11 | gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.15.0" } 12 | 13 | aniyomi-lib = { module = "com.github.Diegopyl1209:extensions-lib", version = "bc006c6ba6" } 14 | 15 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" } 16 | kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" } 17 | kotlin-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" } 18 | 19 | coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines_version" } 20 | coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines_version" } 21 | 22 | injekt = "com.github.mihonapp:injekt:91edab2317" 23 | rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" } 24 | jsoup = { module = "org.jsoup:jsoup", version = "1.16.1" } 25 | okhttp = { module = "com.squareup.okhttp3:okhttp", version = "5.0.0-alpha.11" } 26 | quickjs = { module = "app.cash.quickjs:quickjs-android", version = "0.9.2" } 27 | 28 | [bundles] 29 | common = ["kotlin-stdlib", "injekt", "rxjava", "kotlin-protobuf", "kotlin-json", "jsoup", "okhttp", "aniyomi-lib", "quickjs", "coroutines-core", "coroutines-android"] 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ni3x/aniyomi-extensions/485bcbe0dd990b31080cbaaee2e2ccbe10c10642/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /lib-multisrc/animestream/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib-multisrc/animestream/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("lib-multisrc") 3 | } 4 | 5 | baseVersionCode = 3 6 | -------------------------------------------------------------------------------- /lib-multisrc/animestream/src/eu/kanade/tachiyomi/multisrc/animestream/AnimeStreamFilters.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.multisrc.animestream 2 | 3 | import eu.kanade.tachiyomi.animesource.model.AnimeFilter 4 | import eu.kanade.tachiyomi.animesource.model.AnimeFilterList 5 | import org.jsoup.select.Elements 6 | 7 | object AnimeStreamFilters { 8 | open class QueryPartFilter( 9 | displayName: String, 10 | val vals: Array>, 11 | ) : AnimeFilter.Select( 12 | displayName, 13 | vals.map { it.first }.toTypedArray(), 14 | ) { 15 | fun toQueryPart() = vals[state].second 16 | } 17 | 18 | open class CheckBoxFilterList(name: String, val pairs: Array>) : 19 | AnimeFilter.Group(name, pairs.map { CheckBoxVal(it.first, false) }) 20 | 21 | private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state) 22 | 23 | inline fun AnimeFilterList.asQueryPart(): String { 24 | return (getFirst() as QueryPartFilter).toQueryPart() 25 | } 26 | 27 | inline fun AnimeFilterList.getFirst(): R { 28 | return first { it is R } as R 29 | } 30 | 31 | inline fun AnimeFilterList.parseCheckbox( 32 | options: Array>, 33 | name: String, 34 | ): String { 35 | return (getFirst() as CheckBoxFilterList).state 36 | .filter { it.state } 37 | .map { checkbox -> options.find { it.first == checkbox.name }!!.second } 38 | .filter(String::isNotBlank) 39 | .joinToString("&") { "$name[]=$it" } 40 | } 41 | 42 | internal class GenresFilter(name: String) : CheckBoxFilterList(name, GENRES_LIST) 43 | internal class SeasonFilter(name: String) : CheckBoxFilterList(name, SEASON_LIST) 44 | internal class StudioFilter(name: String) : CheckBoxFilterList(name, STUDIO_LIST) 45 | 46 | internal class StatusFilter(name: String) : QueryPartFilter(name, STATUS_LIST) 47 | internal class TypeFilter(name: String) : QueryPartFilter(name, TYPE_LIST) 48 | internal class SubFilter(name: String) : QueryPartFilter(name, SUB_LIST) 49 | internal class OrderFilter(name: String) : QueryPartFilter(name, ORDER_LIST) 50 | 51 | internal data class FilterSearchParams( 52 | val genres: String = "", 53 | val seasons: String = "", 54 | val studios: String = "", 55 | val status: String = "", 56 | val type: String = "", 57 | val sub: String = "", 58 | val order: String = "", 59 | ) 60 | 61 | internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { 62 | if (filters.isEmpty() || !filterInitialized()) return FilterSearchParams() 63 | 64 | return FilterSearchParams( 65 | filters.parseCheckbox(GENRES_LIST, "genre"), 66 | filters.parseCheckbox(SEASON_LIST, "season"), 67 | filters.parseCheckbox(STUDIO_LIST, "studio"), 68 | filters.asQueryPart(), 69 | filters.asQueryPart(), 70 | filters.asQueryPart(), 71 | filters.asQueryPart(), 72 | ) 73 | } 74 | 75 | lateinit var filterElements: Elements 76 | 77 | fun filterInitialized() = ::filterElements.isInitialized 78 | 79 | fun getPairListByIndex(index: Int) = filterElements.get(index) 80 | .select("li") 81 | .map { element -> 82 | val key = element.selectFirst("label")!!.text() 83 | val value = element.selectFirst("input")!!.attr("value") 84 | Pair(key, value) 85 | }.toTypedArray() 86 | 87 | private val GENRES_LIST by lazy { getPairListByIndex(0) } 88 | private val SEASON_LIST by lazy { getPairListByIndex(1) } 89 | private val STUDIO_LIST by lazy { getPairListByIndex(2) } 90 | private val STATUS_LIST by lazy { getPairListByIndex(3) } 91 | private val TYPE_LIST by lazy { getPairListByIndex(4) } 92 | private val SUB_LIST by lazy { getPairListByIndex(5) } 93 | private val ORDER_LIST by lazy { getPairListByIndex(6) } 94 | } 95 | -------------------------------------------------------------------------------- /lib-multisrc/animestream/src/eu/kanade/tachiyomi/multisrc/animestream/AnimeStreamUrlActivity.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.multisrc.animestream 2 | 3 | import android.app.Activity 4 | import android.content.ActivityNotFoundException 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.util.Log 8 | import kotlin.system.exitProcess 9 | 10 | class AnimeStreamUrlActivity : Activity() { 11 | 12 | private val tag by lazy { javaClass.simpleName } 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | val pathSegments = intent?.data?.pathSegments 17 | if (pathSegments != null && pathSegments.isNotEmpty()) { 18 | val path = pathSegments.joinToString("/") 19 | val mainIntent = Intent().apply { 20 | action = "eu.kanade.tachiyomi.ANIMESEARCH" 21 | putExtra("query", "${AnimeStream.PREFIX_SEARCH}$path") 22 | putExtra("filter", packageName) 23 | } 24 | 25 | try { 26 | startActivity(mainIntent) 27 | } catch (e: ActivityNotFoundException) { 28 | Log.e(tag, e.toString()) 29 | } 30 | } else { 31 | Log.e(tag, "could not parse uri from intent $intent") 32 | } 33 | 34 | finish() 35 | exitProcess(0) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib-multisrc/datalifeengine/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("lib-multisrc") 3 | } 4 | 5 | baseVersionCode = 1 -------------------------------------------------------------------------------- /lib-multisrc/dooplay/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib-multisrc/dooplay/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("lib-multisrc") 3 | } 4 | 5 | baseVersionCode = 2 6 | -------------------------------------------------------------------------------- /lib-multisrc/dooplay/src/eu/kanade/tachiyomi/multisrc/dooplay/DooPlayUrlActivity.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.multisrc.dooplay 2 | 3 | import android.app.Activity 4 | import android.content.ActivityNotFoundException 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.util.Log 8 | import kotlin.system.exitProcess 9 | 10 | class DooPlayUrlActivity : Activity() { 11 | 12 | private val tag = "DooPlayUrlActivity" 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | val pathSegments = intent?.data?.pathSegments 17 | if (pathSegments != null && pathSegments.size > 1) { 18 | val path = pathSegments[0] 19 | val slug = pathSegments[1] 20 | val searchQuery = "$path/$slug" 21 | val mainIntent = Intent().apply { 22 | action = "eu.kanade.tachiyomi.ANIMESEARCH" 23 | putExtra("query", "${DooPlay.PREFIX_SEARCH}$searchQuery") 24 | putExtra("filter", packageName) 25 | } 26 | 27 | try { 28 | startActivity(mainIntent) 29 | } catch (e: ActivityNotFoundException) { 30 | Log.e(tag, e.toString()) 31 | } 32 | } else { 33 | Log.e(tag, "could not parse uri from intent $intent") 34 | } 35 | 36 | finish() 37 | exitProcess(0) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib-multisrc/dopeflix/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("lib-multisrc") 3 | } 4 | 5 | baseVersionCode = 20 6 | 7 | dependencies { 8 | api(project(":lib:dood-extractor")) 9 | api(project(":lib:cryptoaes")) 10 | api(project(":lib:playlist-utils")) 11 | } 12 | -------------------------------------------------------------------------------- /lib-multisrc/dopeflix/src/eu/kanade/tachiyomi/multisrc/dopeflix/dto/DopeFlixDto.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.multisrc.dopeflix.dto 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | 6 | @Serializable 7 | data class VideoDto( 8 | val sources: List, 9 | val tracks: List? = null, 10 | ) 11 | 12 | @Serializable 13 | data class SourceResponseDto( 14 | val sources: JsonElement, 15 | val encrypted: Boolean = true, 16 | val tracks: List? = null, 17 | ) 18 | 19 | @Serializable 20 | data class VideoLink(val file: String = "") 21 | 22 | @Serializable 23 | data class TrackDto(val file: String, val kind: String, val label: String = "") 24 | -------------------------------------------------------------------------------- /lib-multisrc/zorotheme/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("lib-multisrc") 3 | } 4 | 5 | baseVersionCode = 3 6 | 7 | dependencies { 8 | api(project(":lib:megacloud-extractor")) 9 | api(project(":lib:streamtape-extractor")) 10 | } 11 | -------------------------------------------------------------------------------- /lib-multisrc/zorotheme/src/eu/kanade/tachiyomi/multisrc/zorotheme/dto/ZoroThemeDto.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.multisrc.zorotheme.dto 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import org.jsoup.Jsoup 6 | import org.jsoup.nodes.Document 7 | 8 | @Serializable 9 | data class HtmlResponse( 10 | val html: String, 11 | ) { 12 | fun getHtml(): Document { 13 | return Jsoup.parseBodyFragment(html) 14 | } 15 | } 16 | 17 | @Serializable 18 | data class SourcesResponse( 19 | val link: String? = null, 20 | ) 21 | 22 | @Serializable 23 | data class VideoDto( 24 | val sources: List, 25 | val tracks: List? = null, 26 | ) 27 | 28 | @Serializable 29 | data class SourceResponseDto( 30 | val sources: JsonElement, 31 | val encrypted: Boolean = true, 32 | val tracks: List? = null, 33 | ) 34 | 35 | @Serializable 36 | data class VideoLink(val file: String = "") 37 | 38 | @Serializable 39 | data class TrackDto(val file: String, val kind: String, val label: String = "") 40 | -------------------------------------------------------------------------------- /lib/blogger-extractor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("lib-android") 3 | } 4 | -------------------------------------------------------------------------------- /lib/blogger-extractor/src/main/java/eu/kanade/tachiyomi/lib/bloggerextractor/BloggerExtractor.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.lib.bloggerextractor 2 | 3 | import eu.kanade.tachiyomi.animesource.model.Video 4 | import eu.kanade.tachiyomi.network.GET 5 | import okhttp3.Headers 6 | import okhttp3.OkHttpClient 7 | 8 | class BloggerExtractor(private val client: OkHttpClient) { 9 | fun videosFromUrl(url: String, headers: Headers, suffix: String = ""): List