├── .drone.yml ├── .gitattributes ├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── encodings.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── modules.xml ├── tachiload.iml ├── tachiload_dev.iml └── vcs.xml ├── COMPATIBILITY.md ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.md ├── app ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── tachiload │ │ ├── app │ │ ├── App.kt │ │ ├── CLI.kt │ │ ├── ConfigItem.kt │ │ ├── Download.kt │ │ ├── ExtensionsIndex.kt │ │ ├── Helpers.kt │ │ └── SBrowser.kt │ │ └── tachiyomi │ │ ├── Duktape.kt │ │ ├── annotations │ │ └── Nsfw.kt │ │ ├── network │ │ ├── CloudflareInterceptor.kt │ │ ├── CustomCookieJar.kt │ │ ├── NetworkHelper.kt │ │ ├── OkHttpExtensions.kt │ │ ├── ProgressListener.kt │ │ ├── ProgressResponseBody.kt │ │ ├── Requests.kt │ │ └── UserAgentInterceptor.kt │ │ ├── source │ │ ├── CatalogueSource.kt │ │ ├── ConfigurableSource.kt │ │ ├── Source.kt │ │ ├── SourceFactory.kt │ │ ├── model │ │ │ ├── ChapterInfo.kt │ │ │ ├── Filter.kt │ │ │ ├── FilterList.kt │ │ │ ├── Listing.kt │ │ │ ├── MangaInfo.kt │ │ │ ├── MangasPage.kt │ │ │ ├── MangasPageInfo.kt │ │ │ ├── Page.kt │ │ │ ├── PageListEmpty.kt │ │ │ ├── SChapter.kt │ │ │ ├── SChapterImpl.kt │ │ │ ├── SManga.kt │ │ │ └── SMangaImpl.kt │ │ └── online │ │ │ ├── HttpSource.kt │ │ │ └── ParsedHttpSource.kt │ │ └── util │ │ └── JsoupExtensions.kt │ └── resources │ └── .gitkeep ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── tachiload.kotlin-application-conventions.gradle.kts │ ├── tachiload.kotlin-common-conventions.gradle.kts │ └── tachiload.kotlin-library-conventions.gradle.kts ├── docker-compose.yml ├── entrypoint.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── build.py ├── compat_build.py ├── compat_deps.py ├── compat_export.py ├── compat_init.py ├── configure.py ├── prepare.py └── test.py └── settings.gradle.kts /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: tachiload 5 | 6 | clone: 7 | disable: true 8 | 9 | steps: 10 | - name: build 11 | image: openjdk:8-jdk-buster 12 | environment: 13 | github_token: 14 | from_secret: github_token 15 | commands: 16 | - apt-get update && apt-get install -y python3 git libwoff1 libopus0 libwebp6 libwebpdemux2 libenchant1c2a libgudev-1.0-0 libsecret-1-0 libhyphen0 libgdk-pixbuf2.0-0 libegl1 libnotify4 libxslt1.1 libevent-2.1-6 libgles2 libvpx5 libxcomposite1 libatk1.0-0 libatk-bridge2.0-0 libepoxy0 libgtk-3-0 libharfbuzz-icu0 libnss3 libxss1 libasound2 fonts-noto-color-emoji libxtst6 libdbus-glib-1-2 libxt6 xvfb 17 | - git clone https://drosocode:$github_token@github.com/drosoCode/tachiload /app && git clone --depth 1 https://github.com/tachiyomiorg/tachiyomi-extensions /tmp/tachiyomi-extensions 18 | - mkdir -p /app/app/src/main/kotlin/tachiload/extension && mv /tmp/tachiyomi-extensions/src/* /app/app/src/main/kotlin/tachiload/extension 19 | - chmod +x /app/gradlew 20 | - cd /app/scripts && python3 compat_init.py 21 | - cd /app/scripts && python3 prepare.py 22 | - cd /app/scripts && python3 compat_deps.py 23 | - cd /app/scripts && python3 build.py 24 | - cd /app/scripts && python3 compat_build.py 25 | - cd /app/scripts && python3 test.py 26 | - cd /app/scripts && python3 compat_export.py 27 | - cd /app && git add COMPATIBILITY.md && git commit -m "[CI] Update COMPATIBILITY.md" && git push -u origin main 28 | - mkdir /drone/src/release && cp /app/app/build/libs/app-all.jar /drone/src/release/tachiload.jar 29 | 30 | - name: publish 31 | image: debian:10-slim 32 | environment: 33 | GITHUB_TOKEN: 34 | from_secret: github_token 35 | commands: 36 | - apt-get update && apt-get install -y git wget 37 | - wget https://github.com/cli/cli/releases/download/v1.4.0/gh_1.4.0_linux_amd64.deb 38 | - dpkg -i gh_1.4.0_linux_amd64.deb 39 | - gh release create ${DRONE_TAG} /drone/src/release/tachiload.jar -R drosoCode/tachiload -n "Release of $(date +'%Y/%m/%d') version ${DRONE_TAG}" -t "Tachiload V. ${DRONE_TAG}" 40 | 41 | - name: discord_notification 42 | image: appleboy/drone-discord 43 | when: 44 | status: 45 | - success 46 | - failure 47 | settings: 48 | webhook_id: 49 | from_secret: webhook_id 50 | webhook_token: 51 | from_secret: webhook_token 52 | message: > 53 | {{#success build.status}} 54 | ✅ Build #{{build.number}} of `{{repo.name}}` succeeded. 55 | 📝 Commit by {{commit.author}} on `{{commit.branch}}`: 56 | ``` 57 | {{commit.message}} 58 | ``` 59 | {{else}} 60 | ❌ Build #{{build.number}} of `{{repo.name}}` failed. 61 | 📝 Commit by {{commit.author}} on `{{commit.branch}}`: 62 | ``` 63 | {{commit.message}} 64 | ``` 65 | {{/success}} 66 | 67 | trigger: 68 | event: 69 | - tag 70 | --- 71 | kind: signature 72 | hmac: ae15c0109791cb4ab64c75d393a6c792a13934fdec4b3eb83dfb25928cfa5737 73 | 74 | ... 75 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .gradle 26 | build 27 | __cache 28 | .cache 29 | app/src/main/kotlin/tachiload/extension 30 | !gradle-wrapper.jar 31 | 32 | extensions.json 33 | config.json 34 | downloads/ 35 | docker-compose.yml -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/tachiload.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/tachiload_dev.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /COMPATIBILITY.md: -------------------------------------------------------------------------------- 1 | # Tachiload Compatibility List 2 | ## Tested with this version: [tachiyomi-extensions](https://github.com/tachiyomiorg/tachiyomi-extensions/tree/849e4456fd50f9619fad81778c08219442a1486f) 3 | |Language | Name | Depandancies | Compilation | Tests| 4 | |---------|------|--------------|-------------|------| 5 | | all | batoto | ✔️ | ✔️ | ✔️ | 6 | | all | dragonball_multiverse | ✔️ | ❌ | ❌ | 7 | | all | ehentai | ✔️ | ❌ | ❌ | 8 | | all | hentaihand | ✔️ | ❌ | ❌ | 9 | | all | hitomi | ✔️ | ❌ | ❌ | 10 | | all | imhentai | ✔️ | ✔️ | ✔️ | 11 | | all | komga | ✔️ | ❌ | ❌ | 12 | | all | lanraragi | ✔️ | ❌ | ❌ | 13 | | all | luscious | ✔️ | ❌ | ❌ | 14 | | all | mangadex | ✔️ | ❌ | ❌ | 15 | | all | mangaplus | ✔️ | ❌ | ❌ | 16 | | all | mangatoon | ✔️ | ✔️ | ✔️ | 17 | | all | mango | ✔️ | ❌ | ❌ | 18 | | all | mmrcms | ✔️ | ❌ | ❌ | 19 | | all | myreadingmanga | ✔️ | ❌ | ❌ | 20 | | all | nhentai | ✔️ | ❌ | ❌ | 21 | | all | ninehentai | ✔️ | ❌ | ❌ | 22 | | all | ninemanga | ✔️ | ✔️ | ✔️ | 23 | | all | noisemanga | ✔️ | ✔️ | ✔️ | 24 | | all | simplyhentai | ✔️ | ❌ | ❌ | 25 | | all | thelibraryofohara | ✔️ | ✔️ | ✔️ | 26 | | all | toomics | ✔️ | ✔️ | ✔️ | 27 | | all | webtoons | ✔️ | ✔️ | ✔️ | 28 | | all | zbulu | ✔️ | ❌ | ❌ | 29 | | ar | andromedascans | ✔️ | ✔️ | ✔️ | 30 | | ar | gmanga | ✔️ | ❌ | ❌ | 31 | | ar | mangaae | ✔️ | ❌ | ❌ | 32 | | ar | mangalink | ✔️ | ✔️ | ✔️ | 33 | | ar | mangazen | ✔️ | ✔️ | ❌ | 34 | | ar | shqqaa | ✔️ | ✔️ | ✔️ | 35 | | ca | fansubscat | ✔️ | ❌ | ❌ | 36 | | de | mangatube | ✔️ | ✔️ | ✔️ | 37 | | de | wiemanga | ✔️ | ✔️ | ✔️ | 38 | | en | boommanga | ❌ | ❌ | ❌ | 39 | | en | clonemanga | ✔️ | ✔️ | ✔️ | 40 | | en | comicastle | ✔️ | ✔️ | ✔️ | 41 | | en | comicextra | ✔️ | ✔️ | ✔️ | 42 | | en | comicpunch | ✔️ | ✔️ | ✔️ | 43 | | en | dilbert | ✔️ | ❌ | ❌ | 44 | | en | doujins | ✔️ | ❌ | ❌ | 45 | | en | dynasty | ✔️ | ❌ | ❌ | 46 | | en | earlymanga | ✔️ | ✔️ | ✔️ | 47 | | en | eggporncomics | ✔️ | ✔️ | ✔️ | 48 | | en | existentialcomics | ✔️ | ✔️ | ✔️ | 49 | | en | explosm | ✔️ | ✔️ | ✔️ | 50 | | en | gunnerkriggcourt | ✔️ | ✔️ | ✔️ | 51 | | en | guya | ✔️ | ❌ | ❌ | 52 | | en | hbrowse | ✔️ | ✔️ | ✔️ | 53 | | en | hentai2read | ✔️ | ❌ | ❌ | 54 | | en | hentaifox | ✔️ | ✔️ | ✔️ | 55 | | en | hiveworks | ✔️ | ❌ | ❌ | 56 | | en | honkaiimpact3 | ❌ | ❌ | ❌ | 57 | | en | keenspot | ✔️ | ✔️ | ❌ | 58 | | en | killsixbilliondemons | ✔️ | ✔️ | ❌ | 59 | | en | latisbooks | ✔️ | ✔️ | ✔️ | 60 | | en | lemonfont | ✔️ | ✔️ | ✔️ | 61 | | en | madokami | ✔️ | ❌ | ❌ | 62 | | en | manga1s | ✔️ | ✔️ | ✔️ | 63 | | en | mangadog | ✔️ | ❌ | ❌ | 64 | | en | mangadoom | ✔️ | ✔️ | ✔️ | 65 | | en | mangaeden | ✔️ | ✔️ | ✔️ | 66 | | en | mangafast | ✔️ | ✔️ | ✔️ | 67 | | en | mangafreak | ✔️ | ❌ | ❌ | 68 | | en | mangahasu | ✔️ | ❌ | ❌ | 69 | | en | mangahere | ✔️ | ✔️ | ✔️ | 70 | | en | mangahub | ✔️ | ✔️ | ✔️ | 71 | | en | mangajar | ✔️ | ✔️ | ✔️ | 72 | | en | mangakatana | ✔️ | ✔️ | ✔️ | 73 | | en | mangalife | ✔️ | ❌ | ❌ | 74 | | en | mangalinkz | ✔️ | ❌ | ❌ | 75 | | en | mangamainac | ✔️ | ✔️ | ❌ | 76 | | en | mangamutiny | ✔️ | ❌ | ❌ | 77 | | en | mangaowl | ✔️ | ✔️ | ✔️ | 78 | | en | mangapark | ✔️ | ❌ | ❌ | 79 | | en | mangapill | ✔️ | ✔️ | ✔️ | 80 | | en | mangareader | ✔️ | ✔️ | ❌ | 81 | | en | mangarockes | ✔️ | ✔️ | ✔️ | 82 | | en | mangasail | ✔️ | ✔️ | ✔️ | 83 | | en | mangasee | ✔️ | ❌ | ❌ | 84 | | en | mangatown | ✔️ | ✔️ | ✔️ | 85 | | en | manhwamanga | ✔️ | ✔️ | ✔️ | 86 | | en | manhwatime | ✔️ | ✔️ | ❌ | 87 | | en | manmanga | ✔️ | ✔️ | ✔️ | 88 | | en | merakiscans | ✔️ | ✔️ | ✔️ | 89 | | en | multporn | ✔️ | ❌ | ❌ | 90 | | en | myhentaicomics | ✔️ | ✔️ | ✔️ | 91 | | en | myhentaigallery | ✔️ | ❌ | ❌ | 92 | | en | naniscans | ✔️ | ❌ | ❌ | 93 | | en | nhentaicom | ❌ | ❌ | ❌ | 94 | | en | nineanime | ✔️ | ✔️ | ✔️ | 95 | | en | nuxscans | ✔️ | ✔️ | ✔️ | 96 | | en | nyahentai | ✔️ | ✔️ | ✔️ | 97 | | en | oglaf | ✔️ | ✔️ | ✔️ | 98 | | en | patchfriday | ✔️ | ✔️ | ✔️ | 99 | | en | perveden | ✔️ | ✔️ | ✔️ | 100 | | en | pururin | ✔️ | ✔️ | ✔️ | 101 | | en | questionablecontent | ✔️ | ❌ | ❌ | 102 | | en | rainofsnow | ✔️ | ✔️ | ✔️ | 103 | | en | readcomiconline | ✔️ | ❌ | ❌ | 104 | | en | readjump | ✔️ | ✔️ | ❌ | 105 | | en | readm | ✔️ | ✔️ | ✔️ | 106 | | en | readmangatoday | ✔️ | ✔️ | ✔️ | 107 | | en | readmanhwa | ✔️ | ❌ | ❌ | 108 | | en | schlockmercenary | ✔️ | ✔️ | ✔️ | 109 | | en | silentmangaaudition | ✔️ | ✔️ | ✔️ | 110 | | en | sleepypandascans | ✔️ | ✔️ | ❌ | 111 | | en | swordscomic | ✔️ | ✔️ | ✔️ | 112 | | en | tapastic | ✔️ | ❌ | ❌ | 113 | | en | tcbscans | ✔️ | ✔️ | ✔️ | 114 | | en | thepropertyofhate | ✔️ | ✔️ | ❌ | 115 | | en | timelessleaf | ✔️ | ✔️ | ✔️ | 116 | | en | tsumino | ✔️ | ❌ | ❌ | 117 | | en | vgperson | ✔️ | ❌ | ❌ | 118 | | en | vizshonenjump | ✔️ | ❌ | ❌ | 119 | | en | webcomics | ✔️ | ✔️ | ✔️ | 120 | | en | webnovel | ✔️ | ✔️ | ✔️ | 121 | | en | wutopia | ✔️ | ❌ | ❌ | 122 | | en | xkcd | ✔️ | ✔️ | ✔️ | 123 | | es | doujinyang | ✔️ | ❌ | ❌ | 124 | | es | heavenmanga | ✔️ | ✔️ | ✔️ | 125 | | es | ikuhentai | ✔️ | ✔️ | ✔️ | 126 | | es | inmanga | ✔️ | ✔️ | ✔️ | 127 | | es | kumanga | ✔️ | ❌ | ❌ | 128 | | es | lectormanga | ✔️ | ❌ | ❌ | 129 | | es | mangamx | ✔️ | ❌ | ❌ | 130 | | es | tmohentai | ✔️ | ❌ | ❌ | 131 | | es | tumangaonline | ✔️ | ❌ | ❌ | 132 | | es | vcpvmp | ✔️ | ✔️ | ✔️ | 133 | | fr | japscan | ✔️ | ❌ | ❌ | 134 | | fr | kangaryu | ✔️ | ✔️ | ✔️ | 135 | | fr | lirescan | ✔️ | ✔️ | ✔️ | 136 | | fr | mangakawaii | ✔️ | ❌ | ❌ | 137 | | fr | scantrad | ✔️ | ❌ | ❌ | 138 | | fr | scantradunion | ✔️ | ✔️ | ❌ | 139 | | id | KomikFan | ❌ | ❌ | ❌ | 140 | | id | bacakomik | ✔️ | ✔️ | ✔️ | 141 | | id | bacamanga | ✔️ | ✔️ | ✔️ | 142 | | id | comicfx | ✔️ | ✔️ | ✔️ | 143 | | id | komiku | ✔️ | ✔️ | ❌ | 144 | | id | maidmanga | ✔️ | ✔️ | ✔️ | 145 | | id | mangaindo | ✔️ | ✔️ | ✔️ | 146 | | id | mangaku | ✔️ | ❌ | ❌ | 147 | | id | mangayu | ✔️ | ❌ | ❌ | 148 | | id | manhuaid | ✔️ | ✔️ | ✔️ | 149 | | id | neumanga | ✔️ | ✔️ | ❌ | 150 | | id | nyanfm | ✔️ | ✔️ | ✔️ | 151 | | it | digitalteam | ✔️ | ❌ | ❌ | 152 | | it | hentaifantasy | ✔️ | ✔️ | ✔️ | 153 | | it | mangaeden | ✔️ | ✔️ | ✔️ | 154 | | it | mangaworld | ✔️ | ✔️ | ✔️ | 155 | | it | novelleleggere | ✔️ | ✔️ | ❌ | 156 | | it | perveden | ✔️ | ✔️ | ✔️ | 157 | | ja | mangaraw | ✔️ | ✔️ | ❌ | 158 | | ja | nikkangecchan | ✔️ | ✔️ | ✔️ | 159 | | ja | rawdevart | ✔️ | ✔️ | ✔️ | 160 | | ja | senmanga | ✔️ | ❌ | ❌ | 161 | | ja | shonenjumpplus | ✔️ | ❌ | ❌ | 162 | | ko | jmana | ✔️ | ❌ | ❌ | 163 | | ko | navercomic | ✔️ | ❌ | ❌ | 164 | | ko | newtoki | ✔️ | ❌ | ❌ | 165 | | ko | toonkor | ✔️ | ❌ | ❌ | 166 | | pt | bruttal | ✔️ | ❌ | ❌ | 167 | | pt | centraldemangas | ✔️ | ❌ | ❌ | 168 | | pt | goldenmangas | ✔️ | ✔️ | ❌ | 169 | | pt | hipercool | ✔️ | ❌ | ❌ | 170 | | pt | hqdragon | ✔️ | ✔️ | ✔️ | 171 | | pt | hqnow | ✔️ | ✔️ | ✔️ | 172 | | pt | mangahost | ✔️ | ❌ | ❌ | 173 | | pt | mangasproject | ✔️ | ❌ | ❌ | 174 | | pt | mangayabu | ✔️ | ✔️ | ✔️ | 175 | | pt | mundohentai | ✔️ | ✔️ | ❌ | 176 | | pt | mundomangakun | ✔️ | ✔️ | ✔️ | 177 | | pt | saikaiscan | ✔️ | ✔️ | ✔️ | 178 | | pt | socialcomics | ✔️ | ❌ | ❌ | 179 | | pt | supermangas | ✔️ | ❌ | ❌ | 180 | | pt | taosect | ✔️ | ✔️ | ✔️ | 181 | | pt | tsukimangas | ✔️ | ❌ | ❌ | 182 | | pt | unionmangas | ✔️ | ❌ | ❌ | 183 | | pt | yesmangas | ✔️ | ✔️ | ✔️ | 184 | | pt | zinnes | ✔️ | ✔️ | ✔️ | 185 | | ru | acomics | ✔️ | ✔️ | ✔️ | 186 | | ru | allhentai | ✔️ | ❌ | ❌ | 187 | | ru | comx | ✔️ | ❌ | ❌ | 188 | | ru | desu | ✔️ | ✔️ | ✔️ | 189 | | ru | henchan | ✔️ | ❌ | ❌ | 190 | | ru | libmanga | ✔️ | ❌ | ❌ | 191 | | ru | mangachan | ✔️ | ❌ | ❌ | 192 | | ru | mangaclub | ✔️ | ❌ | ❌ | 193 | | ru | mangahub | ✔️ | ❌ | ❌ | 194 | | ru | mangaonlinebiz | ✔️ | ✔️ | ✔️ | 195 | | ru | mintmanga | ✔️ | ❌ | ❌ | 196 | | ru | nudemoon | ✔️ | ✔️ | ✔️ | 197 | | ru | readmanga | ✔️ | ❌ | ❌ | 198 | | ru | remanga | ✔️ | ❌ | ❌ | 199 | | ru | risensteam | ✔️ | ✔️ | ✔️ | 200 | | ru | selfmanga | ✔️ | ✔️ | ✔️ | 201 | | ru | yaoichan | ✔️ | ❌ | ❌ | 202 | | th | nekopost | ✔️ | ✔️ | ❌ | 203 | | tr | MangaDenizi | ❌ | ❌ | ❌ | 204 | | tr | mangaship | ✔️ | ❌ | ❌ | 205 | | tr | serimanga | ✔️ | ✔️ | ✔️ | 206 | | vi | academyvn | ✔️ | ✔️ | ✔️ | 207 | | vi | blogtruyen | ✔️ | ✔️ | ✔️ | 208 | | vi | hentaivn | ✔️ | ✔️ | ❌ | 209 | | vi | iutruyentranh | ✔️ | ❌ | ❌ | 210 | | vi | medoctruyentranh | ✔️ | ✔️ | ✔️ | 211 | | vi | ngonphong | ✔️ | ✔️ | ✔️ | 212 | | vi | truyenqq | ✔️ | ❌ | ❌ | 213 | | vi | truyentranhlh | ✔️ | ✔️ | ✔️ | 214 | | zh | bainianmanga | ✔️ | ✔️ | ✔️ | 215 | | zh | bh3 | ✔️ | ✔️ | ❌ | 216 | | zh | comico | ✔️ | ✔️ | ✔️ | 217 | | zh | copymanga | ✔️ | ❌ | ❌ | 218 | | zh | dmzj | ✔️ | ❌ | ❌ | 219 | | zh | gufengmh | ✔️ | ❌ | ❌ | 220 | | zh | hanhankuman | ✔️ | ❌ | ❌ | 221 | | zh | jinmantiantang | ✔️ | ❌ | ❌ | 222 | | zh | kuaikanmanhua | ✔️ | ✔️ | ✔️ | 223 | | zh | mangabz | ✔️ | ❌ | ❌ | 224 | | zh | manhuadb | ✔️ | ✔️ | ✔️ | 225 | | zh | manhuadui | ✔️ | ✔️ | ✔️ | 226 | | zh | manhuagui | ✔️ | ❌ | ❌ | 227 | | zh | manhuaren | ✔️ | ❌ | ❌ | 228 | | zh | onemanhua | ✔️ | ❌ | ❌ | 229 | | zh | pufei | ✔️ | ❌ | ❌ | 230 | | zh | qimiaomh | ✔️ | ✔️ | ❌ | 231 | | zh | qiximh | ✔️ | ❌ | ❌ | 232 | | zh | tohomh123 | ✔️ | ✔️ | ❌ | 233 | | zh | wnacg | ✔️ | ✔️ | ✔️ | 234 | | zh | wuqimanga | ✔️ | ❌ | ❌ | 235 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-buster AS builder 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get update && apt-get install -y python3 git 6 | ADD . /app 7 | 8 | RUN git clone --depth 1 https://github.com/tachiyomiorg/tachiyomi-extensions /tmp/tachiyomi-extensions \ 9 | && mkdir -p /app/app/src/main/kotlin/tachiload/extension \ 10 | && mv /tmp/tachiyomi-extensions/src/* /app/app/src/main/kotlin/tachiload/extension \ 11 | && cd /app/scripts && python3 prepare.py \ 12 | && python3 build.py 13 | 14 | FROM openjdk:8-jre-slim 15 | 16 | WORKDIR /app 17 | 18 | RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-pip whiptail \ 19 | libwoff1 libopus0 libwebp6 libwebpdemux2 libenchant1c2a libgudev-1.0-0 libsecret-1-0 libhyphen0 libgdk-pixbuf2.0-0 libegl1 libnotify4 libxslt1.1 libevent-2.1-6 libgles2 libvpx5 libxcomposite1 libatk1.0-0 libatk-bridge2.0-0 libepoxy0 libgtk-3-0 libharfbuzz-icu0 libnss3 libxss1 libasound2 fonts-noto-color-emoji libxtst6 libdbus-glib-1-2 libxt6 xvfb \ 20 | && pip3 install whiptail-dialogs 21 | COPY --from=builder /app/entrypoint.sh /app/entrypoint.sh 22 | COPY --from=builder /app/scripts/configure.py /app/configure.py 23 | COPY --from=builder /app/app/build/libs/app-all.jar /app/app.jar 24 | ENTRYPOINT ["/app/entrypoint.sh"] 25 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-buster AS builder 2 | 3 | WORKDIR /app 4 | RUN apt-get update && apt-get install -y python3 git \ 5 | libwoff1 libopus0 libwebp6 libwebpdemux2 libenchant1c2a libgudev-1.0-0 libsecret-1-0 libhyphen0 libgdk-pixbuf2.0-0 libegl1 libnotify4 libxslt1.1 libevent-2.1-6 libgles2 libvpx5 libxcomposite1 libatk1.0-0 libatk-bridge2.0-0 libepoxy0 libgtk-3-0 libharfbuzz-icu0 libnss3 libxss1 libasound2 fonts-noto-color-emoji libxtst6 libdbus-glib-1-2 libxt6 xvfb 6 | RUN git clone --depth 1 https://github.com/tachiyomiorg/tachiyomi-extensions /tmp/tachiyomi-extensions 7 | 8 | ADD . /app 9 | RUN rm -rf /app/app/src/main/kotlin/tachiload/extension \ 10 | && mkdir -p /app/app/src/main/kotlin/tachiload/extension \ 11 | && mv /tmp/tachiyomi-extensions/src/* /app/app/src/main/kotlin/tachiload/extension \ 12 | && cd /app/scripts \ 13 | && python3 compat_init.py \ 14 | && python3 prepare.py \ 15 | && python3 compat_deps.py \ 16 | && python3 build.py \ 17 | && python3 compat_build.py \ 18 | && python3 test.py \ 19 | && python3 compat_export.py 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 drosoCode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tachiload 2 | 3 | ## Manga downloader based on [tachiyomi-extensions](https://github.com/tachiyomiorg/tachiyomi-extensions) 4 | 5 | This software is compatible with most of the extensions of [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi), see the compatibility list [here](COMPATIBILITY.md) 6 | 7 | ## Usage 8 | - set your config file path and your download path in the `docker-compose.yml` file 9 | - (optionnal) edit the environment variables (TACHILOAD_CRON, TACHILOAD_WEBHOOK, TACHILOAD_CONFIG, TACHILOAD_DOWNLOAD) in the `docker-compose.yml` file (they correspond to the cli args) 10 | - run `docker-compose build` to build the container and `docker-compose up -d` to run it 11 | - to use the configuration utility, run `docker-compose exec tachiload /app/configure.py` and restart the container to apply changes 12 | 13 | ## CLI 14 | |Name | Short Name | Description | Example| 15 | |--------|------|------------|-------| 16 | | --cron [pattern] | -c | Cron pattern to run the download, if not specified, the download will run once and the program will exit | --cron "0 */6 * * *" | 17 | | --webhook [url] | -wh | Webhook url to receive notifications (discord) | --webhook "https://discord.com/api/webhooks/ID/TOKEN" | 18 | | --configPath [path] | -cfg | Path to the configuration file (default: ./config.json) | --configPath "/app/config.json" | 19 | | --downloadPath [path] | -dl | Path to the download directory (default: ./downloads) | --downloadPath "/app/downloads" | 20 | | --extensions | | (DEV) return a json object of all available extensions | --extensions | 21 | | --search [name] [ext1_lang] [ext1_name] [ext2_lang] [ext2_name] | | (DEV) return a json list of results for a search | --search "solo leveling" "en" "mangasee" | 22 | | --download [json dict] | | (DEV) download the specified manga | --download '{"extension": "mangasee", "language": "en", "data": {"url": "/manga/Akame-Ga-Kiru", "title": "Akame ga Kiru!", "status": 0, "thumbnail_url": "https://cover.nep.li/cover/Akame-Ga-Kiru.jpg", "initialized": false}}' | 23 | 24 | Thanks to [ClementD64](https://github.com/ClementD64) for his help on this project 25 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 5 | 6 | plugins { 7 | id("org.jetbrains.kotlin.jvm") 8 | id("tachiload.kotlin-application-conventions") 9 | id("com.github.johnrengelman.shadow") version "6.1.0" 10 | } 11 | 12 | repositories { 13 | google() 14 | maven { url = uri("https://jitpack.io") } 15 | maven { url = uri("https://kotlin.bintray.com/kotlinx") } 16 | mavenCentral() 17 | jcenter() 18 | } 19 | 20 | dependencies { 21 | // Use the Kotlin JDK 8 standard library. 22 | implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.4.20")) 23 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.20") 24 | 25 | // This dependency is used by the application. 26 | implementation("com.google.guava:guava:29.0-jre") 27 | compileOnly("com.github.salomonbrys.kotson:kotson:2.5.0") 28 | 29 | // HTML parser 30 | implementation("org.jsoup:jsoup:1.13.1") 31 | 32 | // ReactiveX 33 | implementation("io.reactivex:rxandroid:1.2.1") 34 | implementation("io.reactivex:rxjava:1.3.8") 35 | implementation("com.jakewharton.rxrelay:rxrelay:1.2.0") 36 | implementation("com.github.pwittchen:reactivenetwork:0.13.0") 37 | 38 | // Network client 39 | val okhttpVersion = "4.10.0-RC1" 40 | //val okhttpVersion = "3.12.0" 41 | implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") 42 | implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") 43 | implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") 44 | implementation("com.squareup.okio:okio:2.9.0") 45 | 46 | // Dependency injection 47 | implementation("com.github.inorichi.injekt:injekt-core:65b0440") 48 | 49 | // JSON 50 | val kotlinSerializationVersion = "1.0.1" 51 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") 52 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") 53 | implementation("com.google.code.gson:gson:2.8.6") 54 | implementation("com.github.salomonbrys.kotson:kotson:2.5.0") 55 | implementation("org.json:json:20201115") 56 | 57 | // Coroutines 58 | val coroutinesVersion = "1.4.2" 59 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") 60 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") 61 | 62 | // CRON 63 | implementation("it.sauronsoftware.cron4j:cron4j:2.2.5") 64 | 65 | // Args Parser 66 | implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3") 67 | 68 | // Alternative to Webview for cloudflare interceptor 69 | implementation("com.microsoft.playwright:playwright:0.171.0") 70 | } 71 | 72 | application { 73 | // Define the main class for the application. 74 | mainClass.set("tachiload.app.AppKt") 75 | @Suppress("DEPRECATION") 76 | mainClassName = "tachiload.app.AppKt" 77 | } 78 | 79 | 80 | tasks { 81 | // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) 82 | withType { 83 | kotlinOptions.freeCompilerArgs += listOf( 84 | "-Xopt-in=kotlin.Experimental", 85 | "-Xopt-in=kotlin.RequiresOptIn", 86 | "-Xuse-experimental=kotlin.ExperimentalStdlibApi", 87 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview", 88 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 89 | "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi", 90 | "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi" 91 | ) 92 | kotlinOptions { 93 | jvmTarget = JavaVersion.VERSION_1_8.toString() 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/app/App.kt: -------------------------------------------------------------------------------- 1 | package tachiload.app 2 | 3 | import kotlin.system.exitProcess 4 | import it.sauronsoftware.cron4j.Scheduler 5 | import java.time.LocalDateTime 6 | import kotlinx.cli.* 7 | 8 | 9 | fun main(args: Array) { 10 | 11 | if(args.isNotEmpty() && (args[0] == "--extensions" || args[0] == "--search" || args[0] == "--download")) 12 | { 13 | CLI("./config.json", "./downloads", args) 14 | } 15 | else 16 | { 17 | val parser = ArgParser("Tachiload") 18 | val cron by parser.option(ArgType.String, shortName = "c", description = "Cron Expression") 19 | val webhook by parser.option(ArgType.String, shortName = "wh", description = "Webhook URL (discord) for notification") 20 | val configPath by parser.option(ArgType.String, shortName = "cfg", description = "Path to configuration file").default("./config.json") 21 | val downloadPath by parser.option(ArgType.String, shortName = "dl", description = "Path to download directory").default("./downloads") 22 | parser.parse(args) 23 | 24 | if(cron != null) 25 | { 26 | println("========================================== Tachiload [CRON] ==========================================") 27 | val s = Scheduler() 28 | s.schedule(cron, Runnable() { 29 | println("Running update at:"+LocalDateTime.now()) 30 | Download(configPath, downloadPath, webhook).update() 31 | }) 32 | s.start() 33 | try { 34 | Thread.sleep(Long.MAX_VALUE) 35 | } catch (e: InterruptedException) { 36 | } 37 | } 38 | else 39 | { 40 | println("========================================== Tachiload ==========================================") 41 | Download(configPath, downloadPath, webhook).update() 42 | } 43 | } 44 | exitProcess(0) 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/app/CLI.kt: -------------------------------------------------------------------------------- 1 | package tachiload.app 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | import tachiload.tachiyomi.source.model.FilterList 6 | import tachiload.tachiyomi.source.model.SMangaImpl 7 | 8 | class CLI(private val configPath: String, private val downloadPath: String, private val args: Array) { 9 | init { 10 | if (this.args[0] == "--extensions" && this.args.size == 1) { 11 | print(this.extList()) 12 | } else if(this.args[0] == "--search" && this.args.size >= 4) { 13 | print(this.search()) 14 | } else if(this.args[0] == "--download" && this.args.size == 2) { 15 | print(this.download()) 16 | } else { 17 | print("Error") 18 | } 19 | } 20 | 21 | private fun extList(): String { 22 | return this::class.java.classLoader.getResource("extensions.json").readText() 23 | } 24 | 25 | private fun search(): String { 26 | //args: --search [name] [lang_ext1] [name_ext1] [lang_ext2] [name_ext2] 27 | var lst = mutableListOf() 28 | for (i in 2 until args.size step 2) 29 | { 30 | val ext = Helpers.loadExtension(Helpers.loadIndex(), this.args[i], this.args[i+1]) ?: return "Error" 31 | 32 | var page = 1 33 | var nextPage = true 34 | while(nextPage) { 35 | val value = ext.fetchSearchManga(page, args[1], FilterList()).toBlocking().first() 36 | for (m in value.mangas) { 37 | lst.add(ConfigItem(this.args[i+1], this.args[i], m as SMangaImpl)) 38 | } 39 | if(value.hasNextPage) 40 | page++ 41 | else 42 | nextPage = false 43 | } 44 | } 45 | return Gson().toJson(lst) 46 | } 47 | 48 | private fun download(): String { 49 | val item: ConfigItem = Gson().fromJson( 50 | args[1], 51 | object: TypeToken() {}.type 52 | ) 53 | 54 | val dl = Download(this.configPath, this.downloadPath) 55 | dl.downloadNewChapters( 56 | Helpers.loadExtension(Helpers.loadIndex(), item.language, item.extension), 57 | item.data, 58 | dl.getLatestChapter(item.data.title) 59 | ) 60 | return "" 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/app/ConfigItem.kt: -------------------------------------------------------------------------------- 1 | package tachiload.app 2 | 3 | import tachiload.tachiyomi.source.model.SMangaImpl 4 | 5 | data class ConfigItem( 6 | val extension: String, 7 | val language: String, 8 | val data: SMangaImpl 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/app/Download.kt: -------------------------------------------------------------------------------- 1 | package tachiload.app 2 | 3 | import tachiload.tachiyomi.source.model.SChapter 4 | import tachiload.tachiyomi.source.model.SManga 5 | import tachiload.tachiyomi.source.online.HttpSource 6 | import java.io.File 7 | import java.io.FileOutputStream 8 | import okhttp3.OkHttpClient 9 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 10 | import okhttp3.Request 11 | import okhttp3.RequestBody.Companion.toRequestBody 12 | 13 | 14 | class Download(private val configPath: String, private val downloadPath: String, private val webhook: String?=null) { 15 | 16 | private fun downloadChapter(extension: HttpSource, chapter: SChapter, title: String, number: Int) { 17 | println(" Downloading chapter $number") 18 | extension.fetchPageList(chapter).subscribe { pages -> 19 | for ((i, p) in pages.withIndex()) { 20 | extension.fetchImage(p).subscribe { value -> 21 | val p = this.downloadPath+"/$title/$number/" 22 | val directory = File(p) 23 | if (!directory.exists()) { 24 | directory.mkdirs() 25 | } 26 | val fos = FileOutputStream("$p$i.png") 27 | fos.write(value.body!!.bytes()) 28 | fos.close() 29 | } 30 | } 31 | } 32 | } 33 | 34 | fun downloadNewChapters(extension: HttpSource?, manga: SManga, latestChapter: Int) { 35 | if (extension == null) 36 | return 37 | println(" Found $latestChapter chapters") 38 | extension.fetchChapterList(manga).subscribe { chapters -> 39 | if(latestChapter < 0) 40 | { 41 | for ((i, c) in chapters.withIndex()) { 42 | this.downloadChapter(extension, c, manga.title, chapters.size-i) 43 | } 44 | notify("Downloaded "+chapters.size+" new chapters for "+manga.title) 45 | } 46 | else if (chapters.size - latestChapter > 0) 47 | { 48 | for (i in 0 until chapters.size-latestChapter) { 49 | this.downloadChapter(extension, chapters[i], manga.title, chapters.size-i) 50 | } 51 | notify("Downloaded "+(chapters.size - latestChapter) +" new chapters ["+ (latestChapter+1) +"-"+ chapters.size +"] for "+manga.title) 52 | } 53 | } 54 | } 55 | 56 | fun getLatestChapter(title: String): Int 57 | { 58 | val dir = File(this.downloadPath+"/"+title) 59 | return if (!dir.exists()) { 60 | -1 61 | } else { 62 | val lst = dir.list() 63 | var i = -1 64 | for (a in lst) { 65 | if (a.toInt() > i) 66 | i = a.toInt() 67 | } 68 | i 69 | } 70 | } 71 | 72 | private fun notify(text: String) 73 | { 74 | if(this.webhook != null) { 75 | OkHttpClient().newCall( 76 | Request.Builder() 77 | .url(this.webhook) 78 | .post("{\"content\": \"$text\"}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())) 79 | .build() 80 | ).execute() 81 | } 82 | } 83 | 84 | fun update() 85 | { 86 | val index = Helpers.loadIndex() 87 | val items = Helpers.loadItems(configPath) 88 | 89 | for (manga in items) 90 | { 91 | println("Updating " + manga.data.title + " ...") 92 | this.downloadNewChapters( 93 | Helpers.loadExtension(index, manga.language, manga.extension), 94 | manga.data, 95 | this.getLatestChapter(manga.data.title) 96 | ) 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/app/ExtensionsIndex.kt: -------------------------------------------------------------------------------- 1 | package tachiload.app 2 | 3 | data class ExtensionsIndex( 4 | val libVersion: String, 5 | val extVersionCode: String, 6 | val extClass: String, 7 | val name: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/app/Helpers.kt: -------------------------------------------------------------------------------- 1 | package tachiload.app 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | import tachiload.tachiyomi.source.SourceFactory 6 | import tachiload.tachiyomi.source.online.HttpSource 7 | import java.nio.file.Files 8 | import java.nio.file.Paths 9 | 10 | class Helpers { 11 | companion object { 12 | fun loadExtension(index: Map>, lang: String, ext: String): HttpSource? { 13 | val extClass = index[lang]!!.find { it.name == ext }?.extClass 14 | val instance = this.javaClass.classLoader.loadClass("tachiload.extension.$lang.$ext.src.eu.kanade.tachiyomi.extension.$lang.$ext$extClass") 15 | .getDeclaredConstructor() 16 | .newInstance() 17 | 18 | when (instance) { 19 | is HttpSource -> return instance 20 | is SourceFactory -> return instance.createSources()[0] as HttpSource 21 | } 22 | return null 23 | } 24 | 25 | fun loadIndex(): Map> { 26 | return Gson().fromJson( 27 | this::class.java.classLoader.getResource("extensions.json").readText(), 28 | object: TypeToken>>() {}.type 29 | ) 30 | } 31 | 32 | fun loadItems(configPath: String): List { 33 | return Gson().fromJson( 34 | Files.newBufferedReader(Paths.get(configPath)), 35 | object: TypeToken>() {}.type 36 | ) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/app/SBrowser.kt: -------------------------------------------------------------------------------- 1 | package tachiload.app 2 | 3 | import com.microsoft.playwright.* 4 | 5 | object SBrowser { 6 | var pw:Playwright 7 | var bw:Browser 8 | 9 | init { 10 | println("Initializing Browser ...") 11 | this.pw = Playwright.create() 12 | this.bw = this.pw.firefox().launch() 13 | } 14 | 15 | fun getContext(): BrowserContext { 16 | return this.bw.newContext() 17 | } 18 | 19 | fun close() { 20 | this.bw.close() 21 | this.pw.close() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/Duktape.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi 2 | 3 | import tachiload.app.SBrowser 4 | import com.microsoft.playwright.* 5 | 6 | class Duktape(val c:BrowserContext) { 7 | val p:Page = c.newPage() 8 | 9 | companion object { 10 | fun create(): Duktape { 11 | return Duktape(SBrowser.getContext()) 12 | } 13 | } 14 | 15 | fun evaluate(script: String): String { 16 | return this.p.evaluate(script).toString() 17 | } 18 | 19 | fun close() { 20 | this.p.close() 21 | this.c.close() 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/annotations/Nsfw.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.annotations 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | @Target(AnnotationTarget.CLASS) 5 | annotation class Nsfw 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/CloudflareInterceptor.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | import tachiload.tachiyomi.source.online.HttpSource 4 | import okhttp3.Cookie 5 | import okhttp3.HttpUrl.Companion.toHttpUrl 6 | import okhttp3.Interceptor 7 | import okhttp3.Request 8 | import okhttp3.Response 9 | import java.io.IOException 10 | import java.util.concurrent.TimeUnit 11 | import tachiload.app.SBrowser 12 | import com.microsoft.playwright.* 13 | import java.nio.file.Paths 14 | 15 | class CloudflareInterceptor : Interceptor { 16 | 17 | init { 18 | println("/////////////////////////////////////////////") 19 | } 20 | 21 | private val networkHelper = NetworkHelper() 22 | private lateinit var browser: BrowserContext 23 | 24 | @Synchronized 25 | override fun intercept(chain: Interceptor.Chain): Response { 26 | println("-------------------------------------------------") 27 | browser = SBrowser.getContext() 28 | 29 | val originalRequest = chain.request() 30 | 31 | val response = chain.proceed(originalRequest) 32 | 33 | // Check if Cloudflare anti-bot is on 34 | if (response.code != 503 || response.header("Server") !in SERVER_CHECK) { 35 | return response 36 | } 37 | 38 | try { 39 | response.close() 40 | networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0) 41 | val oldCookie = networkHelper.cookieManager.get(originalRequest.url).firstOrNull { it.name == "cf_clearance" } 42 | resolveWithWebView(originalRequest, oldCookie) 43 | 44 | return chain.proceed(originalRequest) 45 | } catch (e: Exception) { 46 | // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that 47 | // we don't crash the entire app 48 | throw IOException(e) 49 | } 50 | } 51 | 52 | private fun resolveWithWebView(request: Request, oldCookie: Cookie?) { 53 | 54 | val origRequestUrl = request.url.toString() 55 | val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() 56 | //headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH 57 | println("aaaaaaaaaaaaaaaaa") 58 | browser.setExtraHTTPHeaders(headers) 59 | println("bbbbbbbbbbbbbbbbbbbbbbbb") 60 | val page = browser.newPage() 61 | println("ccccccccccccccccccccccccc") 62 | page.navigate(origRequestUrl) 63 | println("ddddddddddddddddddddddddddd") 64 | val opts = Page.ScreenshotOptions() 65 | println("eeeeeeeeeeeeeeeeeeeeeeeee") 66 | page.screenshot(opts.withPath(Paths.get("test.png"))) 67 | println("fffffffffffffffffffffffffffff") 68 | val cookies = browser.cookies() 69 | println("ggggggggggggggggggggggggggggg") 70 | browser.close() 71 | println("hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh") 72 | 73 | for(c in cookies) 74 | { 75 | println(c) 76 | } 77 | 78 | /*if( networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()).firstOrNull { it.name == "cf_clearance" }.let { it != null && it != oldCookie }) 79 | { 80 | // cloudflare bypassed 81 | } 82 | else 83 | { 84 | // Throw exception if we failed to bypass Cloudflare 85 | throw Exception("Unable to bypass cloudflare") 86 | }*/ 87 | } 88 | 89 | companion object { 90 | private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") 91 | private val COOKIE_NAMES = listOf("cf_clearance") 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/CustomCookieJar.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | import okhttp3.Cookie 4 | import okhttp3.CookieJar 5 | import okhttp3.HttpUrl 6 | import java.util.stream.Collectors 7 | 8 | import java.util.ArrayList 9 | 10 | 11 | class CustomCookieJar : CookieJar { 12 | private val storage: MutableList = ArrayList() 13 | override fun saveFromResponse(url: HttpUrl, cookies: List) { 14 | storage.addAll(cookies) 15 | } 16 | 17 | override fun loadForRequest(url: HttpUrl): List { 18 | 19 | // Remove expired Cookies 20 | //storage.removeIf { cookie: Cookie -> cookie.expiresAt() < System.currentTimeMillis() } 21 | 22 | // Only return matching Cookies 23 | return storage.stream().filter { cookie: Cookie -> 24 | cookie.matches( 25 | url 26 | ) 27 | }.collect(Collectors.toList()) 28 | } 29 | 30 | fun get(url: HttpUrl): List { 31 | 32 | // Remove expired Cookies 33 | //storage.removeIf { cookie: Cookie -> cookie.expiresAt() < System.currentTimeMillis() } 34 | 35 | // Only return matching Cookies 36 | return storage.stream().filter { cookie: Cookie -> 37 | cookie.matches( 38 | url 39 | ) 40 | }.collect(Collectors.toList()) 41 | } 42 | 43 | fun remove(url: HttpUrl, cookieNames: List? = null, maxAge: Int = -1) { 44 | val urlString = url.toString() 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/NetworkHelper.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | import tachiload.tachiyomi.network.CustomCookieJar 4 | import tachiload.tachiyomi.network.UserAgentInterceptor 5 | 6 | import okhttp3.Cache 7 | import okhttp3.HttpUrl.Companion.toHttpUrl 8 | import okhttp3.OkHttpClient 9 | import okhttp3.dnsoverhttps.DnsOverHttps 10 | import java.io.File 11 | import java.net.InetAddress 12 | import java.util.concurrent.TimeUnit 13 | 14 | class NetworkHelper() { 15 | 16 | private val cacheDirPath: String = ".cache"; 17 | 18 | private val cacheDir = File(cacheDirPath, "network_cache") 19 | 20 | private val cacheSize = 5L * 1024 * 1024 // 5 MiB 21 | 22 | val cookieManager = CustomCookieJar() 23 | 24 | val client by lazy { 25 | val builder = OkHttpClient.Builder() 26 | .cookieJar(cookieManager) 27 | .cache(Cache(cacheDir, cacheSize)) 28 | .connectTimeout(30, TimeUnit.SECONDS) 29 | .readTimeout(30, TimeUnit.SECONDS) 30 | .addInterceptor(UserAgentInterceptor()) 31 | 32 | /*if (BuildConfig.DEBUG) { 33 | val httpLoggingInterceptor = HttpLoggingInterceptor().apply { 34 | level = HttpLoggingInterceptor.Level.HEADERS 35 | } 36 | builder.addInterceptor(httpLoggingInterceptor) 37 | }*/ 38 | 39 | if (true) { // preferences.enableDoh() 40 | builder.dns( 41 | DnsOverHttps.Builder().client(builder.build()) 42 | .url("https://cloudflare-dns.com/dns-query".toHttpUrl()) 43 | .bootstrapDnsHosts( 44 | listOf( 45 | InetAddress.getByName("162.159.36.1"), 46 | InetAddress.getByName("162.159.46.1"), 47 | InetAddress.getByName("1.1.1.1"), 48 | InetAddress.getByName("1.0.0.1"), 49 | InetAddress.getByName("162.159.132.53"), 50 | InetAddress.getByName("2606:4700:4700::1111"), 51 | InetAddress.getByName("2606:4700:4700::1001"), 52 | InetAddress.getByName("2606:4700:4700::0064"), 53 | InetAddress.getByName("2606:4700:4700::6400") 54 | ) 55 | ) 56 | .build() 57 | ) 58 | } 59 | 60 | builder.build() 61 | } 62 | 63 | val cloudflareClient by lazy { 64 | client.newBuilder() 65 | .addInterceptor(CloudflareInterceptor()) 66 | .build() 67 | client 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/OkHttpExtensions.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | import kotlinx.coroutines.suspendCancellableCoroutine 4 | import kotlinx.serialization.decodeFromString 5 | import kotlinx.serialization.json.Json 6 | import okhttp3.Call 7 | import okhttp3.Callback 8 | import okhttp3.OkHttpClient 9 | import okhttp3.Request 10 | import okhttp3.Response 11 | import rx.Observable 12 | import rx.Producer 13 | import rx.Subscription 14 | import uy.kohesive.injekt.Injekt 15 | import uy.kohesive.injekt.api.fullType 16 | import uy.kohesive.injekt.api.get 17 | import java.io.IOException 18 | import java.util.concurrent.atomic.AtomicBoolean 19 | import kotlin.coroutines.resume 20 | import kotlin.coroutines.resumeWithException 21 | import okhttp3.MediaType 22 | import okhttp3.ResponseBody 23 | import okio.Buffer 24 | import okio.BufferedSource 25 | import okio.ForwardingSource 26 | import okio.Source 27 | import okio.buffer 28 | 29 | fun Call.asObservable(): Observable { 30 | return Observable.unsafeCreate { subscriber -> 31 | // Since Call is a one-shot type, clone it for each new subscriber. 32 | val call = clone() 33 | 34 | // Wrap the call in a helper which handles both unsubscription and backpressure. 35 | val requestArbiter = object : AtomicBoolean(), Producer, Subscription { 36 | override fun request(n: Long) { 37 | if (n == 0L || !compareAndSet(false, true)) return 38 | 39 | try { 40 | val response = call.execute() 41 | if (!subscriber.isUnsubscribed) { 42 | subscriber.onNext(response) 43 | subscriber.onCompleted() 44 | } 45 | } catch (error: Exception) { 46 | if (!subscriber.isUnsubscribed) { 47 | subscriber.onError(error) 48 | } 49 | } 50 | } 51 | 52 | override fun unsubscribe() { 53 | call.cancel() 54 | } 55 | 56 | override fun isUnsubscribed(): Boolean { 57 | return call.isCanceled() 58 | } 59 | } 60 | 61 | subscriber.add(requestArbiter) 62 | subscriber.setProducer(requestArbiter) 63 | } 64 | } 65 | 66 | // Based on https://github.com/gildor/kotlin-coroutines-okhttp 67 | suspend fun Call.await(): Response { 68 | return suspendCancellableCoroutine { continuation -> 69 | enqueue( 70 | object : Callback { 71 | override fun onResponse(call: Call, response: Response) { 72 | if (!response.isSuccessful) { 73 | continuation.resumeWithException(Exception("HTTP error ${response.code}")) 74 | return 75 | } 76 | 77 | continuation.resume(response) 78 | } 79 | 80 | override fun onFailure(call: Call, e: IOException) { 81 | // Don't bother with resuming the continuation if it is already cancelled. 82 | if (continuation.isCancelled) return 83 | continuation.resumeWithException(e) 84 | } 85 | } 86 | ) 87 | 88 | continuation.invokeOnCancellation { 89 | try { 90 | cancel() 91 | } catch (ex: Throwable) { 92 | // Ignore cancel exception 93 | } 94 | } 95 | } 96 | } 97 | 98 | fun Call.asObservableSuccess(): Observable { 99 | return asObservable().doOnNext { response -> 100 | if (!response.isSuccessful) { 101 | response.close() 102 | throw Exception("HTTP error ${response.code}") 103 | } 104 | } 105 | } 106 | 107 | fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { 108 | val progressClient = newBuilder() 109 | .cache(null) 110 | .addNetworkInterceptor { chain -> 111 | val originalResponse = chain.proceed(chain.request()) 112 | originalResponse.newBuilder() 113 | .body(ProgressResponseBody(originalResponse.body!!, listener)) 114 | .build() 115 | } 116 | .build() 117 | 118 | return progressClient.newCall(request) 119 | } 120 | 121 | inline fun Response.parseAs(): T { 122 | // Avoiding Injekt.get() due to compiler issues 123 | val json = Injekt.getInstance(fullType().type) 124 | this.use { 125 | val responseBody = it.body?.string().orEmpty() 126 | return json.decodeFromString(responseBody) 127 | } 128 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/ProgressListener.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | interface ProgressListener { 4 | fun update(bytesRead: Long, contentLength: Long, done: Boolean) 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/ProgressResponseBody.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | import okhttp3.MediaType 4 | import okhttp3.ResponseBody 5 | import okio.Buffer 6 | import okio.BufferedSource 7 | import okio.ForwardingSource 8 | import okio.Source 9 | import okio.buffer 10 | import java.io.IOException 11 | 12 | class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { 13 | 14 | private val bufferedSource: BufferedSource by lazy { 15 | source(responseBody.source()).buffer() 16 | } 17 | 18 | override fun contentType(): MediaType { 19 | return responseBody.contentType()!! 20 | } 21 | 22 | override fun contentLength(): Long { 23 | return responseBody.contentLength() 24 | } 25 | 26 | override fun source(): BufferedSource { 27 | return bufferedSource 28 | } 29 | 30 | private fun source(source: Source): Source { 31 | return object : ForwardingSource(source) { 32 | var totalBytesRead = 0L 33 | 34 | @Throws(IOException::class) 35 | override fun read(sink: Buffer, byteCount: Long): Long { 36 | val bytesRead = super.read(sink, byteCount) 37 | // read() returns the number of bytes read, or -1 if this source is exhausted. 38 | totalBytesRead += if (bytesRead != -1L) bytesRead else 0 39 | progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) 40 | return bytesRead 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/Requests.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | import okhttp3.CacheControl 4 | import okhttp3.FormBody 5 | import okhttp3.Headers 6 | import okhttp3.Request 7 | import okhttp3.RequestBody 8 | import java.util.concurrent.TimeUnit.MINUTES 9 | 10 | private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() 11 | private val DEFAULT_HEADERS = Headers.Builder().build() 12 | private val DEFAULT_BODY: RequestBody = FormBody.Builder().build() 13 | 14 | fun GET( 15 | url: String, 16 | headers: Headers = DEFAULT_HEADERS, 17 | cache: CacheControl = DEFAULT_CACHE_CONTROL 18 | ): Request { 19 | return Request.Builder() 20 | .url(url) 21 | .headers(headers) 22 | .cacheControl(cache) 23 | .build() 24 | } 25 | 26 | fun POST( 27 | url: String, 28 | headers: Headers = DEFAULT_HEADERS, 29 | body: RequestBody = DEFAULT_BODY, 30 | cache: CacheControl = DEFAULT_CACHE_CONTROL 31 | ): Request { 32 | return Request.Builder() 33 | .url(url) 34 | .post(body) 35 | .headers(headers) 36 | .cacheControl(cache) 37 | .build() 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/network/UserAgentInterceptor.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.network 2 | 3 | 4 | import tachiload.tachiyomi.source.online.HttpSource 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | 8 | class UserAgentInterceptor : Interceptor { 9 | override fun intercept(chain: Interceptor.Chain): Response { 10 | val originalRequest = chain.request() 11 | 12 | return if (originalRequest.header("User-Agent").isNullOrEmpty()) { 13 | val newRequest = originalRequest 14 | .newBuilder() 15 | .removeHeader("User-Agent") 16 | .addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT) 17 | .build() 18 | chain.proceed(newRequest) 19 | } else { 20 | chain.proceed(originalRequest) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/CatalogueSource.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source 2 | 3 | import tachiload.tachiyomi.source.model.FilterList 4 | import tachiload.tachiyomi.source.model.MangasPage 5 | import rx.Observable 6 | 7 | interface CatalogueSource : Source { 8 | 9 | /** 10 | * An ISO 639-1 compliant language code (two letters in lower case). 11 | */ 12 | val lang: String 13 | 14 | /** 15 | * Whether the source has support for latest updates. 16 | */ 17 | val supportsLatest: Boolean 18 | 19 | val versionId: Int 20 | 21 | /** 22 | * Returns an observable containing a page with a list of manga. 23 | * 24 | * @param page the page number to retrieve. 25 | */ 26 | fun fetchPopularManga(page: Int): Observable 27 | 28 | /** 29 | * Returns an observable containing a page with a list of manga. 30 | * 31 | * @param page the page number to retrieve. 32 | * @param query the search query. 33 | * @param filters the list of filters to apply. 34 | */ 35 | fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable 36 | 37 | /** 38 | * Returns an observable containing a page with a list of latest manga updates. 39 | * 40 | * @param page the page number to retrieve. 41 | */ 42 | fun fetchLatestUpdates(page: Int): Observable 43 | 44 | /** 45 | * Returns the list of filters for the source. 46 | */ 47 | fun getFilterList(): FilterList 48 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/ConfigurableSource.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source 2 | 3 | interface ConfigurableSource { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/Source.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source 2 | 3 | import tachiload.tachiyomi.source.model.Page 4 | import tachiload.tachiyomi.source.model.SChapter 5 | import tachiload.tachiyomi.source.model.SManga 6 | import rx.Observable 7 | 8 | /** 9 | * A basic interface for creating a source. It could be an online source, a local source, etc... 10 | */ 11 | interface Source { 12 | 13 | /** 14 | * Id for the source. Must be unique. 15 | */ 16 | val id: Long 17 | 18 | /** 19 | * Name of the source. 20 | */ 21 | val name: String 22 | 23 | /** 24 | * Returns an observable with the updated details for a manga. 25 | * 26 | * @param manga the manga to update. 27 | */ 28 | fun fetchMangaDetails(manga: SManga): Observable 29 | 30 | /** 31 | * Returns an observable with all the available chapters for a manga. 32 | * 33 | * @param manga the manga to update. 34 | */ 35 | fun fetchChapterList(manga: SManga): Observable> 36 | 37 | /** 38 | * Returns an observable with the list of pages a chapter has. 39 | * 40 | * @param chapter the chapter. 41 | */ 42 | fun fetchPageList(chapter: SChapter): Observable> 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/SourceFactory.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source 2 | 3 | /** 4 | * A factory for creating sources at runtime. 5 | */ 6 | interface SourceFactory { 7 | /** 8 | * Create a new copy of the sources 9 | * @return The created sources 10 | */ 11 | fun createSources(): List 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/ChapterInfo.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | data class ChapterInfo( 4 | var key: String, 5 | var name: String, 6 | var dateUpload: Long = 0, 7 | var number: Float = -1f, 8 | var scanlator: String = "" 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/Filter.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | sealed class Filter(val name: String, var state: T) { 4 | open class Header(name: String) : Filter(name, 0) 5 | open class Separator(name: String = "") : Filter(name, 0) 6 | abstract class Select(name: String, val values: Array, state: Int = 0) : Filter(name, state) 7 | abstract class Text(name: String, state: String = "") : Filter(name, state) 8 | abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) 9 | abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(name, state) { 10 | fun isIgnored() = state == STATE_IGNORE 11 | fun isIncluded() = state == STATE_INCLUDE 12 | fun isExcluded() = state == STATE_EXCLUDE 13 | 14 | companion object { 15 | const val STATE_IGNORE = 0 16 | const val STATE_INCLUDE = 1 17 | const val STATE_EXCLUDE = 2 18 | } 19 | } 20 | 21 | abstract class Group(name: String, state: List) : Filter>(name, state) 22 | 23 | abstract class Sort(name: String, val values: Array, state: Selection? = null) : 24 | Filter(name, state) { 25 | data class Selection(val index: Int, val ascending: Boolean) 26 | } 27 | 28 | override fun equals(other: Any?): Boolean { 29 | if (this === other) return true 30 | if (other !is Filter<*>) return false 31 | 32 | return name == other.name && state == other.state 33 | } 34 | 35 | override fun hashCode(): Int { 36 | var result = name.hashCode() 37 | result = 31 * result + (state?.hashCode() ?: 0) 38 | return result 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/FilterList.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | data class FilterList(val list: List>) : List> by list { 4 | 5 | constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/Listing.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | abstract class Listing(val name: String) 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/MangaInfo.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | /** 4 | * Model for a manga given by a source 5 | * 6 | * TODO: we should avoid data class due to possible incompatibilities across versions 7 | */ 8 | data class MangaInfo( 9 | val key: String, 10 | val title: String, 11 | val artist: String = "", 12 | val author: String = "", 13 | val description: String = "", 14 | val genres: List = emptyList(), 15 | val status: Int = UNKNOWN, 16 | val cover: String = "" 17 | ) { 18 | 19 | companion object { 20 | const val UNKNOWN = 0 21 | const val ONGOING = 1 22 | const val COMPLETED = 2 23 | const val LICENSED = 3 24 | const val PUBLISHING_FINISHED = 4 25 | const val CANCELLED = 5 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/MangasPage.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | data class MangasPage(val mangas: List, val hasNextPage: Boolean) -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/MangasPageInfo.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | data class MangasPageInfo( 4 | val mangas: List, 5 | val hasNextPage: Boolean 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/Page.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | import java.net.URI 4 | 5 | class Page( 6 | val index: Int, 7 | val url: String = "", 8 | var imageUrl: String? = null, 9 | var uri: URI? = null 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/PageListEmpty.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 The Tachiyomi Open Source Project 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | package tachiload.tachiyomi.source.model 10 | 11 | class PageListEmpty : Exception() 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/SChapter.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | import tachiload.tachiyomi.source.model.ChapterInfo 4 | import java.io.Serializable 5 | 6 | interface SChapter : Serializable { 7 | 8 | var url: String 9 | 10 | var name: String 11 | 12 | var date_upload: Long 13 | 14 | var chapter_number: Float 15 | 16 | var scanlator: String? 17 | 18 | fun copyFrom(other: SChapter) { 19 | name = other.name 20 | url = other.url 21 | date_upload = other.date_upload 22 | chapter_number = other.chapter_number 23 | scanlator = other.scanlator 24 | } 25 | 26 | companion object { 27 | fun create(): SChapter { 28 | return SChapterImpl() 29 | } 30 | } 31 | } 32 | 33 | fun SChapter.toChapterInfo(): ChapterInfo { 34 | return ChapterInfo( 35 | dateUpload = this.date_upload, 36 | key = this.url, 37 | name = this.name, 38 | number = this.chapter_number, 39 | scanlator = this.scanlator ?: "" 40 | ) 41 | } 42 | 43 | fun ChapterInfo.toSChapter(): SChapter { 44 | val chapter = this 45 | return SChapter.create().apply { 46 | url = chapter.key 47 | name = chapter.name 48 | date_upload = chapter.dateUpload 49 | chapter_number = chapter.number 50 | scanlator = chapter.scanlator 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/SChapterImpl.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | class SChapterImpl : SChapter { 4 | 5 | override lateinit var url: String 6 | 7 | override lateinit var name: String 8 | 9 | override var date_upload: Long = 0 10 | 11 | override var chapter_number: Float = -1f 12 | 13 | override var scanlator: String? = null 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/SManga.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | import tachiload.tachiyomi.source.model.MangaInfo 4 | import java.io.Serializable 5 | 6 | interface SManga : Serializable { 7 | 8 | var url: String 9 | 10 | var title: String 11 | 12 | var artist: String? 13 | 14 | var author: String? 15 | 16 | var description: String? 17 | 18 | var genre: String? 19 | 20 | var status: Int 21 | 22 | var thumbnail_url: String? 23 | 24 | var initialized: Boolean 25 | 26 | fun copyFrom(other: SManga) { 27 | if (other.author != null) { 28 | author = other.author 29 | } 30 | 31 | if (other.artist != null) { 32 | artist = other.artist 33 | } 34 | 35 | if (other.description != null) { 36 | description = other.description 37 | } 38 | 39 | if (other.genre != null) { 40 | genre = other.genre 41 | } 42 | 43 | if (other.thumbnail_url != null) { 44 | thumbnail_url = other.thumbnail_url 45 | } 46 | 47 | status = other.status 48 | 49 | if (!initialized) { 50 | initialized = other.initialized 51 | } 52 | } 53 | 54 | companion object { 55 | const val UNKNOWN = 0 56 | const val ONGOING = 1 57 | const val COMPLETED = 2 58 | const val LICENSED = 3 59 | 60 | fun create(): SManga { 61 | return SMangaImpl() 62 | } 63 | } 64 | } 65 | 66 | fun SManga.toMangaInfo(): MangaInfo { 67 | return MangaInfo( 68 | key = this.url, 69 | title = this.title, 70 | artist = this.artist ?: "", 71 | author = this.author ?: "", 72 | description = this.description ?: "", 73 | genres = this.genre?.split(", ") ?: emptyList(), 74 | status = this.status, 75 | cover = this.thumbnail_url ?: "" 76 | ) 77 | } 78 | 79 | fun MangaInfo.toSManga(): SManga { 80 | val mangaInfo = this 81 | return SManga.create().apply { 82 | url = mangaInfo.key 83 | title = mangaInfo.title 84 | artist = mangaInfo.artist 85 | author = mangaInfo.author 86 | description = mangaInfo.description 87 | genre = mangaInfo.genres.joinToString(", ") 88 | status = mangaInfo.status 89 | thumbnail_url = mangaInfo.cover 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/model/SMangaImpl.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.model 2 | 3 | class SMangaImpl : SManga { 4 | 5 | override lateinit var url: String 6 | 7 | override lateinit var title: String 8 | 9 | override var artist: String? = null 10 | 11 | override var author: String? = null 12 | 13 | override var description: String? = null 14 | 15 | override var genre: String? = null 16 | 17 | override var status: Int = 0 18 | 19 | override var thumbnail_url: String? = null 20 | 21 | override var initialized: Boolean = false 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/online/HttpSource.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.online 2 | 3 | import tachiload.tachiyomi.network.GET 4 | import tachiload.tachiyomi.network.NetworkHelper 5 | import tachiload.tachiyomi.network.asObservableSuccess 6 | import tachiload.tachiyomi.network.newCallWithProgress 7 | import tachiload.tachiyomi.source.CatalogueSource 8 | import tachiload.tachiyomi.source.model.FilterList 9 | import tachiload.tachiyomi.source.model.MangasPage 10 | import tachiload.tachiyomi.source.model.Page 11 | import tachiload.tachiyomi.source.model.SChapter 12 | import tachiload.tachiyomi.source.model.SManga 13 | 14 | import okhttp3.Headers 15 | import okhttp3.OkHttpClient 16 | import okhttp3.Request 17 | import okhttp3.Response 18 | import rx.Observable 19 | import java.net.URI 20 | import java.net.URISyntaxException 21 | import java.security.MessageDigest 22 | 23 | /** 24 | * A simple implementation for sources from a website. 25 | */ 26 | abstract class HttpSource: CatalogueSource { 27 | 28 | /** 29 | * Network service. 30 | */ 31 | protected val network = NetworkHelper() 32 | 33 | /** 34 | * Preferences that a source may need. 35 | */ 36 | /*val preferences: SharedPreferences by lazy { 37 | Injekt.get().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) 38 | }*/ 39 | 40 | /** 41 | * Base url of the website without the trailing slash, like: http://mysite.com 42 | */ 43 | abstract val baseUrl: String 44 | 45 | /** 46 | * Version id used to generate the source id. If the site completely changes and urls are 47 | * incompatible, you may increase this value and it'll be considered as a new source. 48 | */ 49 | override val versionId = 1 50 | 51 | /** 52 | * Id of the source. By default it uses a generated id using the first 16 characters (64 bits) 53 | * of the MD5 of the string: sourcename/language/versionId 54 | * Note the generated id sets the sign bit to 0. 55 | */ 56 | override val id by lazy { 57 | val key = "${name.toLowerCase()}/$lang/$versionId" 58 | val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) 59 | (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE 60 | } 61 | 62 | /** 63 | * Headers used for requests. 64 | */ 65 | val headers: Headers by lazy { headersBuilder().build() } 66 | 67 | /** 68 | * Default network client for doing requests. 69 | */ 70 | open val client: OkHttpClient 71 | get() = network.client 72 | 73 | /** 74 | * Headers builder for requests. Implementations can override this method for custom headers. 75 | */ 76 | protected open fun headersBuilder() = Headers.Builder().apply { 77 | add("User-Agent", DEFAULT_USERAGENT) 78 | } 79 | 80 | /** 81 | * Visible name of the source. 82 | */ 83 | override fun toString() = "$name (${lang.toUpperCase()})" 84 | 85 | /** 86 | * Returns an observable containing a page with a list of manga. Normally it's not needed to 87 | * override this method. 88 | * 89 | * @param page the page number to retrieve. 90 | */ 91 | override fun fetchPopularManga(page: Int): Observable { 92 | return client.newCall(popularMangaRequest(page)) 93 | .asObservableSuccess() 94 | .map { response -> 95 | popularMangaParse(response) 96 | } 97 | } 98 | 99 | /** 100 | * Returns the request for the popular manga given the page. 101 | * 102 | * @param page the page number to retrieve. 103 | */ 104 | protected abstract fun popularMangaRequest(page: Int): Request 105 | 106 | /** 107 | * Parses the response from the site and returns a [MangasPage] object. 108 | * 109 | * @param response the response from the site. 110 | */ 111 | protected abstract fun popularMangaParse(response: Response): MangasPage 112 | 113 | /** 114 | * Returns an observable containing a page with a list of manga. Normally it's not needed to 115 | * override this method. 116 | * 117 | * @param page the page number to retrieve. 118 | * @param query the search query. 119 | * @param filters the list of filters to apply. 120 | */ 121 | override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { 122 | return client.newCall(searchMangaRequest(page, query, filters)) 123 | .asObservableSuccess() 124 | .map { response -> 125 | searchMangaParse(response) 126 | } 127 | } 128 | 129 | /** 130 | * Returns the request for the search manga given the page. 131 | * 132 | * @param page the page number to retrieve. 133 | * @param query the search query. 134 | * @param filters the list of filters to apply. 135 | */ 136 | protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request 137 | 138 | /** 139 | * Parses the response from the site and returns a [MangasPage] object. 140 | * 141 | * @param response the response from the site. 142 | */ 143 | protected abstract fun searchMangaParse(response: Response): MangasPage 144 | 145 | /** 146 | * Returns an observable containing a page with a list of latest manga updates. 147 | * 148 | * @param page the page number to retrieve. 149 | */ 150 | override fun fetchLatestUpdates(page: Int): Observable { 151 | return client.newCall(latestUpdatesRequest(page)) 152 | .asObservableSuccess() 153 | .map { response -> 154 | latestUpdatesParse(response) 155 | } 156 | } 157 | 158 | /** 159 | * Returns the request for latest manga given the page. 160 | * 161 | * @param page the page number to retrieve. 162 | */ 163 | protected abstract fun latestUpdatesRequest(page: Int): Request 164 | 165 | /** 166 | * Parses the response from the site and returns a [MangasPage] object. 167 | * 168 | * @param response the response from the site. 169 | */ 170 | protected abstract fun latestUpdatesParse(response: Response): MangasPage 171 | 172 | /** 173 | * Returns an observable with the updated details for a manga. Normally it's not needed to 174 | * override this method. 175 | * 176 | * @param manga the manga to be updated. 177 | */ 178 | override fun fetchMangaDetails(manga: SManga): Observable { 179 | return client.newCall(mangaDetailsRequest(manga)) 180 | .asObservableSuccess() 181 | .map { response -> 182 | mangaDetailsParse(response).apply { initialized = true } 183 | } 184 | } 185 | 186 | /** 187 | * Returns the request for the details of a manga. Override only if it's needed to change the 188 | * url, send different headers or request method like POST. 189 | * 190 | * @param manga the manga to be updated. 191 | */ 192 | open fun mangaDetailsRequest(manga: SManga): Request { 193 | return GET(baseUrl + manga.url, headers) 194 | } 195 | 196 | /** 197 | * Parses the response from the site and returns the details of a manga. 198 | * 199 | * @param response the response from the site. 200 | */ 201 | protected abstract fun mangaDetailsParse(response: Response): SManga 202 | 203 | /** 204 | * Returns an observable with the updated chapter list for a manga. Normally it's not needed to 205 | * override this method. If a manga is licensed an empty chapter list observable is returned 206 | * 207 | * @param manga the manga to look for chapters. 208 | */ 209 | override fun fetchChapterList(manga: SManga): Observable> { 210 | return if (manga.status != SManga.LICENSED) { 211 | client.newCall(chapterListRequest(manga)) 212 | .asObservableSuccess() 213 | .map { response -> 214 | chapterListParse(response) 215 | } 216 | } else { 217 | Observable.error(Exception("Licensed - No chapters to show")) 218 | } 219 | } 220 | 221 | /** 222 | * Returns the request for updating the chapter list. Override only if it's needed to override 223 | * the url, send different headers or request method like POST. 224 | * 225 | * @param manga the manga to look for chapters. 226 | */ 227 | protected open fun chapterListRequest(manga: SManga): Request { 228 | return GET(baseUrl + manga.url, headers) 229 | } 230 | 231 | /** 232 | * Parses the response from the site and returns a list of chapters. 233 | * 234 | * @param response the response from the site. 235 | */ 236 | protected abstract fun chapterListParse(response: Response): List 237 | 238 | /** 239 | * Returns an observable with the page list for a chapter. 240 | * 241 | * @param chapter the chapter whose page list has to be fetched. 242 | */ 243 | override fun fetchPageList(chapter: SChapter): Observable> { 244 | return client.newCall(pageListRequest(chapter)) 245 | .asObservableSuccess() 246 | .map { response -> 247 | pageListParse(response) 248 | } 249 | } 250 | 251 | /** 252 | * Returns the request for getting the page list. Override only if it's needed to override the 253 | * url, send different headers or request method like POST. 254 | * 255 | * @param chapter the chapter whose page list has to be fetched. 256 | */ 257 | protected open fun pageListRequest(chapter: SChapter): Request { 258 | return GET(baseUrl + chapter.url, headers) 259 | } 260 | 261 | /** 262 | * Parses the response from the site and returns a list of pages. 263 | * 264 | * @param response the response from the site. 265 | */ 266 | protected abstract fun pageListParse(response: Response): List 267 | 268 | /** 269 | * Returns an observable with the page containing the source url of the image. If there's any 270 | * error, it will return null instead of throwing an exception. 271 | * 272 | * @param page the page whose source image has to be fetched. 273 | */ 274 | open fun fetchImageUrl(page: Page): Observable { 275 | return client.newCall(imageUrlRequest(page)) 276 | .asObservableSuccess() 277 | .map { imageUrlParse(it) } 278 | } 279 | 280 | /** 281 | * Returns the request for getting the url to the source image. Override only if it's needed to 282 | * override the url, send different headers or request method like POST. 283 | * 284 | * @param page the chapter whose page list has to be fetched 285 | */ 286 | protected open fun imageUrlRequest(page: Page): Request { 287 | return GET(page.url, headers) 288 | } 289 | 290 | /** 291 | * Parses the response from the site and returns the absolute url to the source image. 292 | * 293 | * @param response the response from the site. 294 | */ 295 | protected abstract fun imageUrlParse(response: Response): String 296 | 297 | /** 298 | * Returns an observable with the response of the source image. 299 | * 300 | * @param page the page whose source image has to be downloaded. 301 | */ 302 | fun fetchImage(page: Page): Observable { 303 | return client.newCall(imageRequest(page)) 304 | .asObservableSuccess() 305 | } 306 | 307 | /** 308 | * Returns the request for getting the source image. Override only if it's needed to override 309 | * the url, send different headers or request method like POST. 310 | * 311 | * @param page the chapter whose page list has to be fetched 312 | */ 313 | protected open fun imageRequest(page: Page): Request { 314 | return GET(page.imageUrl!!, headers) 315 | } 316 | 317 | /** 318 | * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from 319 | * database and the urls could still work after a domain change. 320 | * 321 | * @param url the full url to the chapter. 322 | */ 323 | fun SChapter.setUrlWithoutDomain(url: String) { 324 | this.url = getUrlWithoutDomain(url) 325 | } 326 | 327 | /** 328 | * Assigns the url of the manga without the scheme and domain. It saves some redundancy from 329 | * database and the urls could still work after a domain change. 330 | * 331 | * @param url the full url to the manga. 332 | */ 333 | fun SManga.setUrlWithoutDomain(url: String) { 334 | this.url = getUrlWithoutDomain(url) 335 | } 336 | 337 | /** 338 | * Returns the url of the given string without the scheme and domain. 339 | * 340 | * @param orig the full url. 341 | */ 342 | private fun getUrlWithoutDomain(orig: String): String { 343 | return try { 344 | val uri = URI(orig) 345 | var out = uri.path 346 | if (uri.query != null) { 347 | out += "?" + uri.query 348 | } 349 | if (uri.fragment != null) { 350 | out += "#" + uri.fragment 351 | } 352 | out 353 | } catch (e: URISyntaxException) { 354 | orig 355 | } 356 | } 357 | 358 | /** 359 | * Called before inserting a new chapter into database. Use it if you need to override chapter 360 | * fields, like the title or the chapter number. Do not change anything to [manga]. 361 | * 362 | * @param chapter the chapter to be added. 363 | * @param manga the manga of the chapter. 364 | */ 365 | open fun prepareNewChapter(chapter: SChapter, manga: SManga) { 366 | } 367 | 368 | /** 369 | * Returns the list of filters for the source. 370 | */ 371 | override fun getFilterList() = FilterList() 372 | 373 | companion object { 374 | const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)" 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/source/online/ParsedHttpSource.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.source.online 2 | 3 | import tachiload.tachiyomi.source.model.MangasPage 4 | import tachiload.tachiyomi.source.model.Page 5 | import tachiload.tachiyomi.source.model.SChapter 6 | import tachiload.tachiyomi.source.model.SManga 7 | import tachiload.tachiyomi.util.asJsoup 8 | import okhttp3.Response 9 | import org.jsoup.nodes.Document 10 | import org.jsoup.nodes.Element 11 | 12 | /** 13 | * A simple implementation for sources from a website using Jsoup, an HTML parser. 14 | */ 15 | abstract class ParsedHttpSource : HttpSource() { 16 | 17 | /** 18 | * Parses the response from the site and returns a [MangasPage] object. 19 | * 20 | * @param response the response from the site. 21 | */ 22 | override fun popularMangaParse(response: Response): MangasPage { 23 | val document = response.asJsoup() 24 | 25 | val mangas = document.select(popularMangaSelector()).map { element -> 26 | popularMangaFromElement(element) 27 | } 28 | 29 | val hasNextPage = popularMangaNextPageSelector()?.let { selector -> 30 | document.select(selector).first() 31 | } != null 32 | 33 | return MangasPage(mangas, hasNextPage) 34 | } 35 | 36 | /** 37 | * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. 38 | */ 39 | protected abstract fun popularMangaSelector(): String 40 | 41 | /** 42 | * Returns a manga from the given [element]. Most sites only show the title and the url, it's 43 | * totally fine to fill only those two values. 44 | * 45 | * @param element an element obtained from [popularMangaSelector]. 46 | */ 47 | protected abstract fun popularMangaFromElement(element: Element): SManga 48 | 49 | /** 50 | * Returns the Jsoup selector that returns the tag linking to the next page, or null if 51 | * there's no next page. 52 | */ 53 | protected abstract fun popularMangaNextPageSelector(): String? 54 | 55 | /** 56 | * Parses the response from the site and returns a [MangasPage] object. 57 | * 58 | * @param response the response from the site. 59 | */ 60 | override fun searchMangaParse(response: Response): MangasPage { 61 | val document = response.asJsoup() 62 | 63 | val mangas = document.select(searchMangaSelector()).map { element -> 64 | searchMangaFromElement(element) 65 | } 66 | 67 | val hasNextPage = searchMangaNextPageSelector()?.let { selector -> 68 | document.select(selector).first() 69 | } != null 70 | 71 | return MangasPage(mangas, hasNextPage) 72 | } 73 | 74 | /** 75 | * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. 76 | */ 77 | protected abstract fun searchMangaSelector(): String 78 | 79 | /** 80 | * Returns a manga from the given [element]. Most sites only show the title and the url, it's 81 | * totally fine to fill only those two values. 82 | * 83 | * @param element an element obtained from [searchMangaSelector]. 84 | */ 85 | protected abstract fun searchMangaFromElement(element: Element): SManga 86 | 87 | /** 88 | * Returns the Jsoup selector that returns the tag linking to the next page, or null if 89 | * there's no next page. 90 | */ 91 | protected abstract fun searchMangaNextPageSelector(): String? 92 | 93 | /** 94 | * Parses the response from the site and returns a [MangasPage] object. 95 | * 96 | * @param response the response from the site. 97 | */ 98 | override fun latestUpdatesParse(response: Response): MangasPage { 99 | val document = response.asJsoup() 100 | 101 | val mangas = document.select(latestUpdatesSelector()).map { element -> 102 | latestUpdatesFromElement(element) 103 | } 104 | 105 | val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> 106 | document.select(selector).first() 107 | } != null 108 | 109 | return MangasPage(mangas, hasNextPage) 110 | } 111 | 112 | /** 113 | * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. 114 | */ 115 | protected abstract fun latestUpdatesSelector(): String 116 | 117 | /** 118 | * Returns a manga from the given [element]. Most sites only show the title and the url, it's 119 | * totally fine to fill only those two values. 120 | * 121 | * @param element an element obtained from [latestUpdatesSelector]. 122 | */ 123 | protected abstract fun latestUpdatesFromElement(element: Element): SManga 124 | 125 | /** 126 | * Returns the Jsoup selector that returns the tag linking to the next page, or null if 127 | * there's no next page. 128 | */ 129 | protected abstract fun latestUpdatesNextPageSelector(): String? 130 | 131 | /** 132 | * Parses the response from the site and returns the details of a manga. 133 | * 134 | * @param response the response from the site. 135 | */ 136 | override fun mangaDetailsParse(response: Response): SManga { 137 | return mangaDetailsParse(response.asJsoup()) 138 | } 139 | 140 | /** 141 | * Returns the details of the manga from the given [document]. 142 | * 143 | * @param document the parsed document. 144 | */ 145 | protected abstract fun mangaDetailsParse(document: Document): SManga 146 | 147 | /** 148 | * Parses the response from the site and returns a list of chapters. 149 | * 150 | * @param response the response from the site. 151 | */ 152 | override fun chapterListParse(response: Response): List { 153 | val document = response.asJsoup() 154 | return document.select(chapterListSelector()).map { chapterFromElement(it) } 155 | } 156 | 157 | /** 158 | * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter. 159 | */ 160 | protected abstract fun chapterListSelector(): String 161 | 162 | /** 163 | * Returns a chapter from the given element. 164 | * 165 | * @param element an element obtained from [chapterListSelector]. 166 | */ 167 | protected abstract fun chapterFromElement(element: Element): SChapter 168 | 169 | /** 170 | * Parses the response from the site and returns the page list. 171 | * 172 | * @param response the response from the site. 173 | */ 174 | override fun pageListParse(response: Response): List { 175 | return pageListParse(response.asJsoup()) 176 | } 177 | 178 | /** 179 | * Returns a page list from the given document. 180 | * 181 | * @param document the parsed document. 182 | */ 183 | protected abstract fun pageListParse(document: Document): List 184 | 185 | /** 186 | * Parse the response from the site and returns the absolute url to the source image. 187 | * 188 | * @param response the response from the site. 189 | */ 190 | override fun imageUrlParse(response: Response): String { 191 | return imageUrlParse(response.asJsoup()) 192 | } 193 | 194 | /** 195 | * Returns the absolute url to the source image from the document. 196 | * 197 | * @param document the parsed document. 198 | */ 199 | protected abstract fun imageUrlParse(document: Document): String 200 | } 201 | -------------------------------------------------------------------------------- /app/src/main/kotlin/tachiload/tachiyomi/util/JsoupExtensions.kt: -------------------------------------------------------------------------------- 1 | package tachiload.tachiyomi.util 2 | 3 | import okhttp3.Response 4 | import org.jsoup.Jsoup 5 | import org.jsoup.nodes.Document 6 | import org.jsoup.nodes.Element 7 | 8 | fun Element.selectText(css: String, defaultValue: String? = null): String? { 9 | return select(css).first()?.text() ?: defaultValue 10 | } 11 | 12 | fun Element.selectInt(css: String, defaultValue: Int = 0): Int { 13 | return select(css).first()?.text()?.toInt() ?: defaultValue 14 | } 15 | 16 | fun Element.attrOrText(css: String): String { 17 | return if (css != "text") attr(css) else text() 18 | } 19 | 20 | /** 21 | * Returns a Jsoup document for this response. 22 | * @param html the body of the response. Use only if the body was read before calling this method. 23 | */ 24 | fun Response.asJsoup(html: String? = null): Document { 25 | return Jsoup.parse(html ?: body!!.string(), request.url.toString()) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drosoCode/tachiload/9b60bd12f784cb1f78f2300ae36aca767c4456d8/app/src/main/resources/.gitkeep -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | // Support convention plugins written in Kotlin. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build. 7 | `kotlin-dsl` 8 | } 9 | 10 | repositories { 11 | // Use the plugin portal to apply community plugins in convention plugins. 12 | gradlePluginPortal() 13 | } 14 | 15 | dependencies { 16 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin") 17 | } 18 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/tachiload.kotlin-application-conventions.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | // Apply the common convention plugin for shared build configuration between library and application projects. 7 | id("tachiload.kotlin-common-conventions") 8 | 9 | // Apply the application plugin to add support for building a CLI application in Java. 10 | application 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/tachiload.kotlin-common-conventions.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. 7 | id("org.jetbrains.kotlin.jvm") 8 | } 9 | 10 | repositories { 11 | // Use JCenter for resolving dependencies. 12 | jcenter() 13 | } 14 | 15 | dependencies { 16 | // Align versions of all Kotlin components 17 | implementation(platform("org.jetbrains.kotlin:kotlin-bom")) 18 | 19 | // Use the Kotlin JDK 8 standard library. 20 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 21 | 22 | // Use JUnit Jupiter API for testing. 23 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2") 24 | 25 | // Use JUnit Jupiter Engine for testing. 26 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 27 | } 28 | 29 | tasks.test { 30 | // Use junit platform for unit tests. 31 | useJUnitPlatform() 32 | } 33 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/tachiload.kotlin-library-conventions.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | // Apply the common convention plugin for shared build configuration between library and application projects. 7 | id("tachiload.kotlin-common-conventions") 8 | 9 | // Apply the java-library plugin for API and implementation separation. 10 | `java-library` 11 | } 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | tachiload: 4 | build: . 5 | restart: unless-stopped 6 | volumes: 7 | - /app/docker/data/tachiload/config.json:/app/config.json 8 | - /nas/Mangas:/app/downloads 9 | environment: 10 | TACHILOAD_CRON: "0 */6 * * *" -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | args=() 4 | 5 | if [ ! -z "$TACHILOAD_WEBHOOK" ]; then 6 | args+=("--webhook" "$TACHILOAD_WEBHOOK") 7 | fi 8 | 9 | if [ ! -z "$TACHILOAD_CRON" ]; then 10 | args+=("--cron" "$TACHILOAD_CRON") 11 | fi 12 | 13 | if [ ! -z "$TACHILOAD_CONFIG" ]; then 14 | args+=("--configFile" "$TACHILOAD_CONFIG") 15 | fi 16 | 17 | if [ ! -z "$TACHILOAD_DOWNLOAD" ]; then 18 | args+=("--downloadPath" "$TACHILOAD_DOWNLOAD") 19 | fi 20 | 21 | exec java -jar app.jar "${args[@]}" "$@" 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drosoCode/tachiload/9b60bd12f784cb1f78f2300ae36aca767c4456d8/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-6.7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /scripts/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import json 4 | import shutil 5 | 6 | basePath = "/app/app/src/main/kotlin/tachiload/extension" 7 | dataPath = "../app/src/main/resources/extensions.json" 8 | buildCmd = "cd ../ && ./gradlew build" 9 | 10 | extensions_error = {} 11 | proc = subprocess.Popen(buildCmd, stderr=subprocess.PIPE, shell=True) 12 | while True: 13 | line = proc.stderr.readline() 14 | if line: 15 | l = line.rstrip().decode("utf-8") 16 | if l.find("e: " + basePath) == 0: 17 | print(l) 18 | arr = l.split("/") 19 | lang = arr[8] 20 | ext = arr[9] 21 | if lang not in extensions_error: 22 | extensions_error[lang] = [] 23 | if ext not in extensions_error[lang]: 24 | extensions_error[lang].append(ext) 25 | else: 26 | break 27 | 28 | with open(dataPath, "r") as f: 29 | ext_data = json.load(f) 30 | 31 | for lang in extensions_error: 32 | i = 0 33 | while i < len(ext_data[lang]): 34 | if ext_data[lang][i]["name"] in extensions_error[lang]: 35 | shutil.rmtree(os.path.join(basePath, lang, ext_data[lang][i]["name"])) 36 | del ext_data[lang][i] 37 | else: 38 | i += 1 39 | 40 | with open(dataPath, "w") as f: 41 | json.dump(ext_data, f) 42 | 43 | os.system(buildCmd) -------------------------------------------------------------------------------- /scripts/compat_build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import subprocess 4 | 5 | runCmd = "java -jar ../app/build/libs/app-all.jar" 6 | 7 | proc = subprocess.Popen(runCmd + " --extensions", stdout=subprocess.PIPE, shell=True) 8 | extensions = json.loads(proc.communicate()[0]) 9 | 10 | with open("compat.json", "r") as f: 11 | ext_data = json.load(f) 12 | 13 | for i in range(len(ext_data)): 14 | if ext_data[i][0] in extensions: 15 | for j in extensions[ext_data[i][0]]: 16 | if j["name"] == ext_data[i][1]: 17 | ext_data[i][3] = True 18 | 19 | with open("compat.json", "w") as f: 20 | json.dump(ext_data, f) -------------------------------------------------------------------------------- /scripts/compat_deps.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | extPath = "../app/src/main/kotlin/tachiload/extension" 5 | 6 | langs = os.listdir(extPath) 7 | with open("compat.json", "r") as f: 8 | ext_data = json.load(f) 9 | 10 | for i in range(len(ext_data)): 11 | if ext_data[i][0] in langs and ext_data[i][1] in os.listdir( 12 | os.path.join(extPath, ext_data[i][0]) 13 | ): 14 | ext_data[i][2] = True 15 | 16 | with open("compat.json", "w") as f: 17 | json.dump(ext_data, f) -------------------------------------------------------------------------------- /scripts/compat_export.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import subprocess 4 | 5 | extensions = [] 6 | 7 | repoPath = "/tmp/tachiyomi-extensions" 8 | extPath = "../app/src/main/kotlin/tachiload/extension" 9 | 10 | 11 | langs = os.listdir(extPath) 12 | with open("compat.json", "r") as f: 13 | ext_data = json.load(f) 14 | 15 | for i in range(len(ext_data)): 16 | if ext_data[i][0] in langs and ext_data[i][1] in os.listdir( 17 | os.path.join(extPath, ext_data[i][0]) 18 | ): 19 | ext_data[i][4] = True 20 | 21 | 22 | compat_data = "# Tachiload Compatibility List\n" 23 | compat_data += "## Tested with this version: [tachiyomi-extensions](https://github.com/tachiyomiorg/tachiyomi-extensions/tree/" 24 | rev = ( 25 | subprocess.Popen( 26 | "cd " + repoPath + " && git rev-parse HEAD", stdout=subprocess.PIPE, shell=True 27 | ) 28 | .communicate()[0] 29 | .decode("utf-8") 30 | ) 31 | compat_data += rev[: len(rev) - 1] + ")\n" 32 | 33 | compat_data += "|Language | Name | Depandancies | Compilation | Tests|\n" 34 | compat_data += "|---------|------|--------------|-------------|------|\n" 35 | 36 | for e in ext_data: 37 | compat_data += ( 38 | "| " 39 | + e[0] 40 | + " | " 41 | + e[1] 42 | + " | " 43 | + ("✔️" if e[2] else "❌") 44 | + " | " 45 | + ("✔️" if e[2] and e[3] else "❌") 46 | + " | " 47 | + ("✔️" if e[2] and e[3] and e[4] else "❌") 48 | + " |\n" 49 | ) 50 | 51 | with open("../COMPATIBILITY.md", "w") as f: 52 | f.write(compat_data) -------------------------------------------------------------------------------- /scripts/compat_init.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | extensions = [] 5 | 6 | basePath = "../app/src/main/kotlin/tachiload/extension" 7 | 8 | for lang in sorted(os.listdir(basePath)): 9 | p = os.path.join(basePath, lang) 10 | if os.path.isdir(p): 11 | for ext in sorted(os.listdir(p)): 12 | extensions.append([lang, ext, False, False, False]) 13 | 14 | with open("compat.json", "w") as f: 15 | json.dump(extensions, f) -------------------------------------------------------------------------------- /scripts/configure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from whiptail import Whiptail 3 | import subprocess 4 | import json 5 | import os 6 | 7 | runCmd = "java -jar /app/app.jar" 8 | dataPath = "/app/config.json" 9 | 10 | tui = Whiptail(title="Tachiload", backtitle="Tachiload configuration utility") 11 | configData = [] 12 | if os.path.exists(dataPath): 13 | with open(dataPath, "r") as f: 14 | configData = json.load(f) 15 | else: 16 | with open(dataPath, "w") as f: 17 | f.write("[]") 18 | 19 | 20 | proc = subprocess.Popen(runCmd + " --extensions", stdout=subprocess.PIPE, shell=True) 21 | save = False 22 | extensions = json.loads(proc.communicate()[0]) 23 | filterExt = extensions.keys() 24 | selectedExts = "" 25 | 26 | # filterExt = ["en", "all"] 27 | 28 | 29 | def selectExt(): 30 | lst = [] 31 | for i in filterExt: 32 | for j in extensions[i]: 33 | lst.append(i + "|" + j["name"]) 34 | o = tui.checklist("Select Extensions", sorted(lst)) 35 | if o[1] == 1: 36 | return "" 37 | else: 38 | l = "" 39 | for i in o[0]: 40 | e = i.split("|") 41 | l += ' "' + e[0] + '" "' + e[1] + '"' 42 | return l 43 | 44 | 45 | def getMangaList(res): 46 | lst = [] 47 | for i in res: 48 | lst.append( 49 | i["language"] + "|" + i["extension"] + " Name: " + i["data"]["title"] 50 | ) 51 | return sorted(lst) 52 | 53 | 54 | def getItemFromRes(i, res): 55 | a = i.split(" Name: ") 56 | b = a[0].split("|") 57 | for r in res: 58 | if ( 59 | b[0] == r["language"] 60 | and b[1] == r["extension"] 61 | and a[1] == r["data"]["title"] 62 | ): 63 | return r 64 | return None 65 | 66 | 67 | def selectManga(extensions, name): 68 | proc = subprocess.Popen( 69 | runCmd + ' --search "' + name + '" ' + extensions, 70 | stdout=subprocess.PIPE, 71 | shell=True, 72 | ) 73 | res = json.loads(proc.communicate()[0]) 74 | r = getMangaList(res) 75 | if len(r) == 0: 76 | tui.msgbox("No result available") 77 | return None 78 | 79 | i = tui.menu("Select Result", r) 80 | if i[1]: 81 | return None 82 | return getItemFromRes(i[0], res) 83 | 84 | 85 | def addManga(): 86 | global selectedExts, configData 87 | if selectedExts == "" or not tui.yesno("Select extensions ?", "no"): 88 | selectedExts = selectExt() 89 | if selectedExts == "": 90 | return 91 | 92 | name = tui.inputbox("Enter manga name") 93 | if name[1] or name[0] == "": 94 | return 95 | 96 | d = selectManga(selectedExts, name[0]) 97 | if d is not None: 98 | if d in configData: 99 | tui.msgbox("Manga already tracked") 100 | else: 101 | configData.append(d) 102 | 103 | 104 | def removeManga(): 105 | l = getMangaList(configData) 106 | if len(l) == 0: 107 | tui.msgbox("No manga available") 108 | return 109 | out = tui.menu("Manga List", l) 110 | if out[1]: 111 | return 112 | delete = getItemFromRes(out[0], configData) 113 | if delete is not None: 114 | if not tui.yesno("Are you sure ?", "no"): 115 | configData.remove(delete) 116 | 117 | 118 | while True: 119 | opt = ["Add Manga", "List Mangas", "Remove Manga", "Save And Exit", "Exit"] 120 | out = tui.menu("Home", opt) 121 | if out[1]: 122 | break 123 | p = opt.index(out[0]) 124 | if p == 0: 125 | addManga() 126 | elif p == 1: 127 | l = getMangaList(configData) 128 | if len(l) == 0: 129 | tui.msgbox("No manga available") 130 | else: 131 | tui.menu("Manga List", l) 132 | elif p == 2: 133 | removeManga() 134 | elif p == 3: 135 | save = True 136 | break 137 | else: 138 | break 139 | 140 | if save: 141 | with open(dataPath, "w") as f: 142 | json.dump(configData, f) 143 | -------------------------------------------------------------------------------- /scripts/prepare.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import io 4 | import shutil 5 | 6 | extensions = {} 7 | 8 | basePath = "../app/src/main/kotlin/tachiload/extension" 9 | dataPath = "../app/src/main/resources/extensions.json" 10 | 11 | 12 | def parseGradle(path: str) -> dict: 13 | data = {"libVersion": None, "extVersionCode": None, "extClass": None} 14 | with open(path, "r") as f: 15 | for l in f.readlines(): 16 | for d in data.keys(): 17 | if l.find(d) != -1: 18 | data[d] = l[l.index("=") + 2 :].strip("\n").strip("'") 19 | return data 20 | 21 | 22 | def findParenthesisEnd(startPos: int, data: str) -> int: 23 | nbOpen = 0 24 | for i in range(startPos + 1, len(data)): 25 | if data[i] == "(": 26 | nbOpen += 1 27 | elif data[i] == ")": 28 | if nbOpen == 0: 29 | return i + 1 30 | else: 31 | nbOpen -= 1 32 | 33 | 34 | def fixKotlin(lang: str, ext: str): 35 | path = os.path.join( 36 | basePath, lang, ext, "src", "eu", "kanade", "tachiyomi", "extension", lang, ext 37 | ) 38 | if not os.path.exists(path): 39 | shutil.rmtree(os.path.join(basePath, lang, ext)) 40 | return 41 | 42 | for f in os.listdir(path): 43 | if f[f.rfind(".") + 1 :] == "kt": 44 | curFile = os.path.join(path, f) 45 | with open(curFile, "r") as f: 46 | # if extension imports android libs, discard it 47 | print(curFile) 48 | data = f.read() 49 | if data.find("import android") != -1: 50 | shutil.rmtree(path) 51 | return 52 | # fix package name 53 | data = data.replace( 54 | "package eu.kanade.tachiyomi.extension", 55 | "package tachiload.extension." 56 | + lang 57 | + "." 58 | + ext 59 | + ".src.eu.kanade.tachiyomi.extension", 60 | ) 61 | # fix imports 62 | data = data.replace("import eu.kanade", "import tachiload") 63 | # replace ducktape by custom implementation 64 | data = data.replace( 65 | "import com.squareup.duktape.Duktape", 66 | "import tachiload.tachiyomi.Duktape", 67 | ) 68 | # fix old okhttp versions 69 | data = data.replace("response.request().url()", "response.request.url") 70 | data = data.replace("response.body()", "response.body") 71 | data = data.replace( 72 | "originalRequest.url().toString()", "originalRequest.url.toString()" 73 | ) 74 | data = data.replace( 75 | "chain.request().url().toString()", "chain.request().url.toString()" 76 | ) 77 | 78 | data = data.replace(".body()!!.", ".body!!.") 79 | data = data.replace(".body()?.", ".body?.") 80 | data = data.replace("response.code()", "response.code") 81 | data = data.replace("genreResponse.code()", "genreResponse.code") 82 | data = data.replace( 83 | ".receivedResponseAtMillis()", 84 | ".receivedResponseAtMillis", 85 | ) 86 | 87 | # replace HttpUrl.parse() by ().toHttpUrlOrNull() 88 | if data.find("HttpUrl.parse") != -1: 89 | data = data.replace( 90 | "import", 91 | "import okhttp3.HttpUrl.Companion.toHttpUrlOrNull\nimport", 92 | 1, 93 | ) 94 | i = data.find("HttpUrl.parse") 95 | while i != -1: 96 | data = data.replace("HttpUrl.parse", "", 1) 97 | j = findParenthesisEnd(i, data) 98 | data = data[:j] + ".toHttpUrlOrNull()" + data[j:] 99 | i = data.find("HttpUrl.parse") 100 | 101 | # replace MediaType.parse() by ()..toMediaType() 102 | if data.find("MediaType.parse") != -1: 103 | data = data.replace( 104 | "import", 105 | "import okhttp3.MediaType.Companion.toMediaType\nimport", 106 | 1, 107 | ) 108 | i = data.find("MediaType.parse") 109 | while i != -1: 110 | data = data.replace("MediaType.parse", "", 1) 111 | j = findParenthesisEnd(i, data) 112 | data = data[:j] + ".toMediaType()" + data[j:] 113 | i = data.find("MediaType.parse") 114 | # replace override fun saveFromResponse(url: HttpUrl, cookies: MutableList) {} by override fun saveFromResponse(url: HttpUrl, cookies: List) {} 115 | data = data.replace("override fun saveFromResponse(url: HttpUrl, cookies: MutableList) {}", "override fun saveFromResponse(url: HttpUrl, cookies: List) {}") 116 | 117 | data = fixExtension(lang, ext, data) 118 | 119 | # persist data 120 | with open(curFile, "w") as f: 121 | f.write(data) 122 | 123 | # create a dict with all extensions data 124 | if lang not in extensions: 125 | extensions[lang] = [] 126 | dat = parseGradle(os.path.join(basePath, lang, ext, "build.gradle")) 127 | dat.update({"name": ext}) 128 | extensions[lang].append(dat) 129 | 130 | 131 | def fixExtension(lang: str, ext: str, data: str): 132 | #fixes specific to some extensions 133 | #Mangahere 134 | if lang == "en" and ext == "mangahere": 135 | data = data.replace('urls.mapIndexed { index: Int, s: String -> Page(index, "", "https:$s") }', 'urls.mapIndexed { index, s -> Page(index, "", "https:$s") }') 136 | elif lang == "all" and ext == "batoto": 137 | data = data.replace('Pair("zu", "zu"),', 'Pair("zu", "zu")') 138 | 139 | return data 140 | 141 | # process 142 | for lang in os.listdir(basePath): 143 | p = os.path.join(basePath, lang) 144 | if os.path.isdir(p): 145 | for ext in os.listdir(p): 146 | if os.path.isdir(os.path.join(p, ext)): 147 | fixKotlin(lang, ext) 148 | 149 | # save file with list of extensions 150 | with open(dataPath, "w") as f: 151 | json.dump(extensions, f) -------------------------------------------------------------------------------- /scripts/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import json 4 | import shutil 5 | 6 | extPath = "../app/src/main/kotlin/tachiload/extension" 7 | dataPath = "../app/src/main/resources/extensions.json" 8 | testCmd = 'java -jar ../app/build/libs/app-all.jar --search "abc" ' 9 | buildCmd = "cd ../ && ./gradlew build" 10 | 11 | with open(dataPath, "r") as f: 12 | ext_data = json.load(f) 13 | 14 | for lang in ext_data.keys(): 15 | i = 0 16 | while i < len(ext_data[lang]): 17 | extensions_error = {} 18 | proc = subprocess.Popen( 19 | testCmd + '"' + lang + '" "' + ext_data[lang][i]["name"] + '"', 20 | stdout=subprocess.PIPE, 21 | stderr=subprocess.PIPE, 22 | shell=True, 23 | ) 24 | 25 | if proc.communicate()[1]: 26 | # if data on stderr, remove the extension 27 | p = os.path.join(extPath, lang, ext_data[lang][i]["name"]) 28 | if os.path.exists(p): 29 | shutil.rmtree(p) 30 | print(" - " + lang + "|" + ext_data[lang][i]["name"] + ": ERR") 31 | del ext_data[lang][i] 32 | else: 33 | print(" - " + lang + "|" + ext_data[lang][i]["name"] + ": OK") 34 | i += 1 35 | 36 | with open(dataPath, "w") as f: 37 | json.dump(ext_data, f) 38 | 39 | os.system(buildCmd) -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.7.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = "tachiload" 11 | include("app", "list", "utilities") 12 | --------------------------------------------------------------------------------