Compare commits

...

51 Commits

Author SHA1 Message Date
AwkwardPeak7 555051eba4 fix build
CI / Prepare job (push) Successful in 4s Details
CI / Build individual modules (push) Successful in 6m3s Details
CI / Publish repo (push) Successful in 45s Details
move file to appropriate directory
2024-03-06 06:25:46 +00:00
nomaxsnx 536e080aec add Source CrystalComics "pt-br" (#1702)
* add CrystalComics

* Update src/pt/crystalcomics/build.gradle

Fiz newline

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/pt/crystalcomics/src/CrystalComics.kt

Fix newline again

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-03-06 06:25:46 +00:00
bapeey 84664872c4 Add Jobsibe (#1704)
Add jobsibe
2024-03-06 06:25:46 +00:00
Claudemirovsky 46a6e5e7f3 New source: ja/RawINU (#1696)
* feat: Create RawINU base

* fix: Fix manga details selector

* fix: Fix chapter list

* fix: Fix page list

* chore: Add source icon

* chore: Add isNsfw flag
2024-03-06 06:25:46 +00:00
mohamedotaku a3ff15c263 add source StellarSaber "ar" (#1700) 2024-03-06 06:25:46 +00:00
Chopper b68bc4cfd0 Add LadyEstelarScan (#1694) 2024-03-06 06:25:46 +00:00
Chopper e78bd29133 Add WinterSun (#1693) 2024-03-06 06:25:46 +00:00
bapeey cf4a208d08 IkigaiMangas: Replace popular and latest endpoints + convention changes (#1689)
* Update

* Follow exactly site order

* Replace popular and latest endpoints
2024-03-06 06:25:46 +00:00
bapeey 5344c62b6b S2Manga: Update domain and use loadMoreRequest (#1687)
Update domain and set loadMoreRequest
2024-03-06 06:25:46 +00:00
bapeey d0edd1855f MangaSwat: Update title selector (#1685)
Update title selector
2024-03-06 06:25:46 +00:00
bapeey 87246f5443 SlimeRead: Update apiUrl (#1683)
Update apiUrl
2024-03-06 06:25:46 +00:00
bapeey d48b870553 Kumanga: Add extra headers and fix ban when fetching chapters (#1681)
Fix ban
2024-03-06 06:25:46 +00:00
bapeey 99f355d65b LeerCapitulo: Add filters (#1679)
* Add filters

* Bump
2024-03-06 06:25:46 +00:00
stevenyomi 8e7c8da40e Renovate: update Gradle and AGP with draft PR 2024-03-06 06:25:46 +00:00
bapeey a8cd270d86 MangaPlus: Add German lang (#1691)
Add GERMAN
2024-03-06 06:25:46 +00:00
BrutuZ 2bb5ef9059 Anchira: Add Entry Bundling (#1643)
* Anchira: Add Entry Bundling
Prefixing queries with `bundle:` will create a single SManga entry with results as chapters.

* Switch default bundle title to first entry's
Fix url used to check for bundles
Default page count of 1

* Enable filters on bundles

* Strip chapter number suffix from bundle title

* Convert RegEx to variable

* Convert RegEx constructor to top-level val
2024-03-06 06:25:46 +00:00
KirinRaikage 58b5aa2f3d FMTeam: Migrate to PizzaReader (#1668)
* FMTeam: Migrate to PizzaReader

* Add missing trailing commas
2024-03-06 06:25:46 +00:00
Chopper 2a0588e8d9 HuntersScans: migrate source (#1650)
* Migrate HuntersScans

* Add filter to novels

* Fix names

* Add newline

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Remove unneeded map function

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Replace statusMap with a 'when' statement

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Add new line in build.gradle

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Move mangaSubString content to in line

* Replace fetch(Manga|Update|Search) overrides to Parses

* Remove unneeded try/catch

* Cleanup

* Rename some functions

* Remove override id

* Fix extra chapters and remove a possible infinite loop

* Remove unneeded regex calls

* Remove unneeded conditional

* Cleanup

* Fix: filter to remove novels

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-03-06 06:25:46 +00:00
stevenyomi 33f95f820c Use `versionCatalogs` in precompiled script plugins 2024-03-06 06:25:46 +00:00
keiyoushi-bot 008fc27cf9 Add Re-Manga (Arabic) back
Closes #1635
2024-03-06 06:25:46 +00:00
stevenyomi b860dcb132 Upgrade Gradle to 8.6 (#1667) 2024-03-06 06:25:41 +00:00
stevenyomi b65648aa6c YakshaScans: fix filename 2024-03-06 06:23:31 +00:00
airis 2725ac93f6 Add Yakshascans (#1661)
* Add Yakshascans

* Update extension and source name, fix class name

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-06 06:23:31 +00:00
Fermín Cirella bb0317bef3 Lanraragi - Add option to open source URL from archive tag in the Web… (#1663)
Lanraragi - Add option to open source URL from archive tag in the WebView
2024-03-06 06:23:31 +00:00
bapeey 882c1d9738 Add ManhuaKO (#1662) 2024-03-06 06:23:31 +00:00
OtakuArab 70ccdbc637 Manhwa Freak: update URL (#1660)
* Update build.gradle

* Update ManhwaFreak.kt
2024-03-06 06:23:31 +00:00
AwkwardPeak7 c2b7c1cb20 OppaiStream: update selectors (#1658) 2024-03-06 06:23:31 +00:00
AwkwardPeak7 feef718f2a GravureBlogger: fix incomplete results due to incorrect totalResults field (#1657) 2024-03-06 06:23:31 +00:00
AwkwardPeak7 e971eb7fbc Remove some sources (#1656)
* remove 247Manga

site redirects to luxmanga.net which already exists as extension

* remove 1st Manhwa

site redirects to Todaymic

* add description selector
2024-03-06 06:23:31 +00:00
nausicaa d60149eb6b NewToki: fix manga details with long title (#1653)
* Modifying the inability to make a chapter when the manga title is too long

* Update src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/NewToki.kt

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* Update src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/NewToki.kt

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-06 06:23:30 +00:00
AwkwardPeak7 88dba59eef Mangathemesia refactor (#1637)
* remove randomua

* i18n

* add other language based selectors

* countviews in background

* small cleanup

* lint

* fix

* bump

* fix genre resetting

* use enqueue instead of coroutinescope

* fix build

* fix build x2

* add back genre missing warning

* Add ES translations

* lint

* Add available language

* lint

I hate lint

* review

- lowercase match for status
- callback on site

* review x2, also fix smol mistake

* lint

:)

* lowercase some translations

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* lowercase some translations

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* remove "人気"

* inline the labels

* lint

thank you lint, very cool

---------

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-06 06:23:30 +00:00
AwkwardPeak7 8f18229563 MangaGalaxy: move to MangaThemesia (#1634) 2024-03-06 06:23:30 +00:00
stevenyomi 19f09c8967 Update Renovate config (#1649)
* Update `config:base` to `config:recommended` (rename)
* Schedule on every Sunday
* Group all GitHub Actions deps in a single PR
* Ignore issue moderator, since we'll manually update it (possibly with config changes)
* Include and exclude Gradle dependencies (will enable ktlint individually in the future)
* Include Gradle itself (Gradle Wrapper)
2024-03-06 06:23:30 +00:00
Eshlender 80c5340a02 [RU] Nude-moon new domain (#1654)
* [RU] Nude-moon new domain

Closes https://github.com/keiyoushi/extensions-source/issues/1633

* extVersionCode
2024-03-06 06:23:30 +00:00
bapeey a0457871e2 Add TMOManga (#1638) 2024-03-06 06:23:30 +00:00
stevenyomi e2f24a8f91 Remove Bilibili Comics (#1626) 2024-03-06 06:23:25 +00:00
stevenyomi 56863063fc Remove dead sources (#1625)
* Remove ZuttoManga, closes #647

* Remove WebtoonsTOP, closes #648
2024-03-06 06:23:25 +00:00
Cuong M. Tran f7cd89926a MangaOwl.to: Fix manga’s URL for other mirrors (#1624)
Fix manga’s URL for other mirrors
2024-03-06 06:23:25 +00:00
Tef ac9f307979 Add Mangamonks (#1599)
* Add MangaMonks

* Fixed page in search

* Update res

* Fixed search exception

* Applied requested changes

* Applied requested changes

* Revert "Applied requested changes"

This reverts commit 237d3313f7ef98e1d695c80385a908367ae425d8.
2024-03-06 06:23:25 +00:00
Rama Bondan Prakoso 420c7d9cd5 Anchira: Add trending filter (#1522)
Anchira: Add trending filters

Signed-off-by: Rama Bondan Prakoso <ramanarubp@gmail.com>
2024-03-06 06:23:25 +00:00
AwkwardPeak7 48ca6558ed Manga Online: add headers to requests (#1619) 2024-03-06 06:23:25 +00:00
Cuong M. Tran c81adc7829 Add MangaOwl.To (#1307)
* Add MangaOwl (mangaowl.to)

* Factory class to separate Comics & Mangas along with their respective genres

* Using API to request for manga’s detail

* Using API to request chapters list

* parse JSON for pages

* Add mirrors

* Rename source to MangaOwl.To

* using DTO

* migrate to full API

* update icon

* cleanup

* Fix: allow reset GenresFilter checkbox

* separate Genre & GenreCheckBox

* Update query builder

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* apiUrl -> baseUrl

* unused fields in dtos

* extra newline

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-03-06 06:23:25 +00:00
AwkwardPeak7 24087b9688 add mangaonline (#1610)
* Add Manga Online

* Fix note about latest manga parse

* Add new line at the end of build.gradle

* Remove blank line

* Fix global search

* Remove extra URL builder in search

* Remove URLBuilder

* Add try/catch in fetchMangaGenre

* Set latestUpdatesNextPageSelector to null

* Rename constante name and remove breakline in GenreList::selected

* Remove fetchLatestUpdate override

* Replace 'runCatching' for try/catch

* Remove 'headers'

* Remove unused import

* Remove last blank line

* Rename constant variable with uppercase

* small cleanup

* lint

---------

Co-authored-by: Chopper <156493704+ttony2chopper@users.noreply.github.com>
2024-03-06 06:23:25 +00:00
Secozzi 5cb0af3b2d add alandal (#1600)
* add alandal

* clarify
2024-03-06 06:23:25 +00:00
Henry 12e3079af3 Void Scans: Update Domain (#1605)
Void Scans: Update domain
2024-03-06 06:23:25 +00:00
Cuong M. Tran 82e6f7b6da New source: Cartoon18 (#1467)
* New source: Cartoon18

isNSFW
lang: zh

* cleanup

* improve
2024-03-06 06:23:10 +00:00
Fermín Cirella 18fab95708 Add Lolivault (#1592)
* Add Lolivault

* Use FoolSlide theme
2024-03-06 06:23:10 +00:00
Secozzi be407aa637 add readmanga (#1505)
* add readmanga

* indentation

* Fix filters note

* replace .run

* prioritize search
2024-03-06 06:23:10 +00:00
Vetle Ledaal 9c064fb3eb MajorScans: fix double image (#1584) 2024-03-06 06:23:10 +00:00
bapeey 5a643095ad Kumanga: Update apiUrl and minor changes (#1586)
Fix Kumanga
2024-03-06 06:23:10 +00:00
stevenyomi 37c80ab2f6 Kemono: fix missing posts (#1587) 2024-03-06 06:23:10 +00:00
237 changed files with 4398 additions and 1380 deletions

24
.github/renovate.json vendored
View File

@ -1,8 +1,30 @@
{ {
"extends": [ "extends": [
"config:base" "config:recommended"
], ],
"schedule": ["on sunday"],
"includePaths": [ "includePaths": [
"buildSrc/gradle/**",
"gradle/**",
".github/**" ".github/**"
],
"ignoreDeps": ["keiyoushi/issue-moderator-action"],
"packageRules": [
{
"matchManagers": ["github-actions"],
"groupName": "{{manager}} dependencies"
},
{
"matchManagers": ["gradle"],
"enabled": false
},
{
"matchPackageNames": [
"com.android.tools.build:gradle",
"gradle"
],
"draftPR": true,
"enabled": true
}
] ]
} }

View File

@ -45,7 +45,7 @@ jobs:
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
- name: Build extensions - name: Build extensions
env: env:

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -18,8 +18,6 @@ android {
} }
} }
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
compileOnly(libs.findBundle("common").get()) compileOnly(versionCatalogs.named("libs").findBundle("common").get())
} }

View File

@ -36,10 +36,8 @@ kotlinter {
) )
} }
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
compileOnly(libs.findBundle("common").get()) compileOnly(versionCatalogs.named("libs").findBundle("common").get())
} }
tasks { tasks {

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

14
gradlew generated vendored
View File

@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -202,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat generated vendored
View File

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 9

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3

View File

@ -10,9 +10,6 @@ class BloggerDto(
@Serializable @Serializable
class BloggerFeedDto( class BloggerFeedDto(
@SerialName("openSearch\$totalResults") val totalResults: BloggerTextDto,
@SerialName("openSearch\$startIndex") val startIndex: BloggerTextDto,
@SerialName("openSearch\$itemsPerPage") val itemsPerPage: BloggerTextDto,
val category: List<BloggerCategoryDto> = emptyList(), val category: List<BloggerCategoryDto> = emptyList(),
val entry: List<BloggerFeedEntryDto> = emptyList(), val entry: List<BloggerFeedEntryDto> = emptyList(),
) )

View File

@ -65,7 +65,7 @@ abstract class GravureBlogger(
initialized = true initialized = true
} }
} }
val hasNextPage = (data.feed.startIndex.t.toInt() + data.feed.itemsPerPage.t.toInt()) <= data.feed.totalResults.t.toInt() val hasNextPage = data.feed.entry.size == MAX_RESULTS
return MangasPage(manga, hasNextPage) return MangasPage(manga, hasNextPage)
} }

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 10 baseVersionCode = 11

View File

@ -60,7 +60,7 @@ class KemonoPostDto(
if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path)) if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
addAll(attachments) addAll(attachments)
}.filter { }.filter {
when (it.name.substringAfterLast('.').lowercase()) { when (it.path.substringAfterLast('.').lowercase()) {
"png", "jpg", "gif", "jpeg", "webp" -> true "png", "jpg", "gif", "jpeg", "webp" -> true
else -> false else -> false
} }
@ -91,6 +91,7 @@ class KemonoPostDto(
@Serializable @Serializable
class KemonoFileDto(val name: String? = null, val path: String? = null) class KemonoFileDto(val name: String? = null, val path: String? = null)
// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4
@Serializable @Serializable
class KemonoAttachmentDto(val name: String, val path: String) { class KemonoAttachmentDto(val name: String, val path: String) {
override fun toString() = "$path?f=$name" override fun toString() = "$path?f=$name"

View File

@ -0,0 +1,30 @@
alt_names_heading=Alternative Names:
author_filter_title=Author
year_filter_title=Year
status_filter_title=Status
status_filter_option_all=All
status_filter_option_ongoing=Ongoing
status_filter_option_completed=Completed
status_filter_option_hiatus=Hiatus
status_filter_option_dropped=Dropped
type_filter_title=Type
type_filter_option_all=All
type_filter_option_manga=Manga
type_filter_option_manhwa=Manhwa
type_filter_option_manhua=Manhua
type_filter_option_comic=Comic
order_by_filter_title=Sort By
order_by_filter_default=Default
order_by_filter_az=A-Z
order_by_filter_za=Z-A
order_by_filter_latest_update=Latest Update
order_by_filter_latest_added=Latest Added
order_by_filter_popular=Popular
project_filter_title=Filter Project
project_filter_all_manga=Show all manga
project_filter_only_project=Show only project manga
genre_filter_title=Genre
genre_missing_warning=Press 'Reset' to attempt to show the genres
genre_exclusion_warning=Genre exclusion is not available for all sources
project_filter_warning=NOTE: Can't be used with other filter!
project_filter_name=%s Project List page

View File

@ -0,0 +1,23 @@
alt_names_heading=Nombres alternativos:
author_filter_title=Autor
year_filter_title=Año
status_filter_title=Estado
status_filter_option_all=Todos
status_filter_option_ongoing=En curso
status_filter_option_completed=Completado
status_filter_option_hiatus=En pausa
status_filter_option_dropped=Abandonado
type_filter_title=Tipo
type_filter_option_all=Todos
order_by_filter_title=Ordenar por
order_by_filter_default=Por defecto
order_by_filter_latest_update=Última actualización
order_by_filter_latest_added=Último añadido
project_filter_title=Filtrar proyectos
project_filter_all_manga=Mostrar todos los mangas
project_filter_only_project=Mostrar solo los proyectos
genre_filter_title=Género
genre_missing_warning=Presione 'Restablecer' para intentar cargar los géneros
genre_exclusion_warning=La exclusión de géneros puede no funcionar correctamente
project_filter_warning=NOTA: ¡No se puede usar con otros filtros!
project_filter_name=%s Página de proyectos

View File

@ -2,8 +2,8 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 28 baseVersionCode = 29
dependencies { dependencies {
api(project(":lib:randomua")) api(project(":lib:i18n"))
} }

View File

@ -1,15 +1,8 @@
package eu.kanade.tachiyomi.multisrc.mangathemesia package eu.kanade.tachiyomi.multisrc.mangathemesia
import android.app.Application import eu.kanade.tachiyomi.lib.i18n.Intl
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -21,64 +14,56 @@ import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.lang.IllegalArgumentException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
// Formerly WPMangaStream & WPMangaReader -> MangaThemesia // Formerly WPMangaStream & WPMangaReader -> MangaThemesia
abstract class MangaThemesia( abstract class MangaThemesia(
override val name: String, override val name: String,
override val baseUrl: String, override val baseUrl: String,
override val lang: String, final override val lang: String,
val mangaUrlDirectory: String = "/manga", val mangaUrlDirectory: String = "/manga",
val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US), val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
) : ParsedHttpSource(), ConfigurableSource { ) : ParsedHttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected open val json: Json by injectLazy() protected open val json: Json by injectLazy()
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient by lazy { override val client = network.cloudflareClient
network.cloudflareClient.newBuilder()
.setRandomUserAgent(
preferences.getPrefUAType(),
preferences.getPrefCustomUA(),
)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/") .set("Referer", "$baseUrl/")
protected val intl = Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "es"),
classLoader = javaClass.classLoader!!,
)
open val projectPageString = "/project" open val projectPageString = "/project"
// Popular (Search with popular order and nothing else) // Popular (Search with popular order and nothing else)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("popular"))) override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
override fun popularMangaParse(response: Response) = searchMangaParse(response) override fun popularMangaParse(response: Response) = searchMangaParse(response)
// Latest (Search with update order and nothing else) // Latest (Search with update order and nothing else)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("update"))) override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response) override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// Search // Search
@ -166,22 +151,96 @@ abstract class MangaThemesia(
override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r" override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
// Manga details // Manga details
private fun selector(selector: String, contains: List<String>): String {
return contains.joinToString(", ") { selector.replace("%s", it) }
}
open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody" open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody"
open val seriesTitleSelector = "h1.entry-title"
open val seriesArtistSelector = ".infotable tr:contains(artist) td:last-child, .tsinfo .imptdt:contains(artist) i, .fmed b:contains(artist)+span, span:contains(artist)" open val seriesTitleSelector = "h1.entry-title, .ts-breadcrumb li:last-child span"
open val seriesAuthorSelector = ".infotable tr:contains(author) td:last-child, .tsinfo .imptdt:contains(author) i, .fmed b:contains(author)+span, span:contains(author)"
open val seriesArtistSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
listOf(
"artist",
"Artiste",
"Artista",
"الرسام",
"الناشر",
"İllüstratör",
"Çizer",
),
)
open val seriesAuthorSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
listOf(
"Author",
"Auteur",
"autor",
"المؤلف",
"Mangaka",
"seniman",
"Pengarang",
"Yazar",
),
)
open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]" open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]"
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt"
open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, span:contains(genre)" open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt, " +
open val seriesTypeSelector = ".infotable tr:contains(type) td:last-child, .tsinfo .imptdt:contains(type) i, .tsinfo .imptdt:contains(type) a, .fmed b:contains(type)+span, span:contains(type) a, a[href*=type\\=]" selector(
open val seriesStatusSelector = ".infotable tr:contains(status) td:last-child, .tsinfo .imptdt:contains(status) i, .fmed b:contains(status)+span span:contains(status)" ".infotable tr:contains(%s) td:last-child",
listOf(
"Alternative",
"Alternatif",
"الأسماء الثانوية",
),
)
open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, " +
selector(
"span:contains(%s)",
listOf(
"genre",
"التصنيف",
),
)
open val seriesTypeSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .tsinfo .imptdt:contains(%s) a, .fmed b:contains(%s)+span, span:contains(%s) a",
listOf(
"type",
"ประเภท",
"النوع",
"tipe",
"Türü",
),
) + ", a[href*=type\\=]"
open val seriesStatusSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span span:contains(%s)",
listOf(
"status",
"Statut",
"Durum",
"連載状況",
"Estado",
"الحالة",
"حالة العمل",
"สถานะ",
"stato",
"Statüsü",
),
)
open val seriesThumbnailSelector = ".infomanga > div[itemprop=image] img, .thumb img" open val seriesThumbnailSelector = ".infomanga > div[itemprop=image] img, .thumb img"
open val altNamePrefix = "Alternative Name: " open val altNamePrefix = "${intl["alt_names_heading"]} "
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails -> document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
title = seriesDetails.selectFirst(seriesTitleSelector)?.text().orEmpty() title = seriesDetails.selectFirst(seriesTitleSelector)!!.text()
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder() artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder() author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim() description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
@ -210,16 +269,32 @@ abstract class MangaThemesia(
} }
protected fun String?.removeEmptyPlaceholder(): String? { protected fun String?.removeEmptyPlaceholder(): String? {
return if (this.isNullOrBlank() || this == "-" || this == "N/A") null else this return if (this.isNullOrBlank() || this == "-" || this == "N/A" || this == "n/a") null else this
} }
open fun String?.parseStatus(): Int = when { open fun String?.parseStatus(): Int {
this == null -> SManga.UNKNOWN if (this == null) return SManga.UNKNOWN
listOf("ongoing", "publishing").any { this.contains(it, ignoreCase = true) } -> SManga.ONGOING
this.contains("hiatus", ignoreCase = true) -> SManga.ON_HIATUS return when (this.lowercase().trim()) {
this.contains("completed", ignoreCase = true) -> SManga.COMPLETED "مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours",
listOf("dropped", "cancelled").any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED "en cours de publication", "đang tiến hành", "em lançamento", "онгоінг", "publishing",
else -> SManga.UNKNOWN "devam ediyor", "em andamento", "in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo", "emision",
"en emision", "مستمر", "curso", "en marcha", "publicandose", "publicando", "连载中", "devam etmekte", "連載中",
-> SManga.ONGOING
"completed", "completo", "complété", "fini", "achevé", "terminé", "tamamlandı", "đã hoàn thành", "hoàn thành",
"مكتملة", "завершено", "finished", "finalizado", "completata", "one-shot", "bitti", "tamat", "completado", "concluído", "完結",
"concluido", "已完结", "bitmiş",
-> SManga.COMPLETED
"canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné",
-> SManga.CANCELLED
"hiatus", "on hold", "pausado", "en espera", "en pause", "en attente",
-> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
} }
// Chapter list // Chapter list
@ -227,6 +302,9 @@ abstract class MangaThemesia(
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
countViews(document)
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) } val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
// Add timestamp to latest chapter, taken from "Updated On". // Add timestamp to latest chapter, taken from "Updated On".
@ -238,8 +316,6 @@ abstract class MangaThemesia(
if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date) if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date)
} }
countViews(document)
return chapters return chapters
} }
@ -267,13 +343,13 @@ abstract class MangaThemesia(
open val pageSelector = "div#readerarea img" open val pageSelector = "div#readerarea img"
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
countViews(document)
val chapterUrl = document.location() val chapterUrl = document.location()
val htmlPages = document.select(pageSelector) val htmlPages = document.select(pageSelector)
.filterNot { it.imgAttr().isEmpty() } .filterNot { it.imgAttr().isEmpty() }
.mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) } .mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) }
countViews(document)
// Some sites also loads pages via javascript // Some sites also loads pages via javascript
if (htmlPages.isNotEmpty()) { return htmlPages } if (htmlPages.isNotEmpty()) { return htmlPages }
@ -320,8 +396,6 @@ abstract class MangaThemesia(
.build() .build()
val newHeaders = headersBuilder() val newHeaders = headersBuilder()
.set("Content-Length", formBody.contentLength().toString())
.set("Content-Type", formBody.contentType().toString())
.set("Referer", document.location()) .set("Referer", document.location())
.build() .build()
@ -339,17 +413,22 @@ abstract class MangaThemesia(
} }
val request = countViewsRequest(document) ?: return val request = countViewsRequest(document) ?: return
runCatching { client.newCall(request).execute().close() } val callback = object : Callback {
override fun onResponse(call: Call, response: Response) = response.close()
override fun onFailure(call: Call, e: IOException) = Unit
}
client.newCall(request).enqueue(callback)
} }
// Filters // Filters
protected class AuthorFilter : Filter.Text("Author") protected class AuthorFilter(name: String) : Filter.Text(name)
protected class YearFilter : Filter.Text("Year") protected class YearFilter(name: String) : Filter.Text(name)
open class SelectFilter( open class SelectFilter(
displayName: String, displayName: String,
val vals: Array<Pair<String, String>>, private val vals: Array<Pair<String, String>>,
defaultValue: String? = null, defaultValue: String? = null,
) : Filter.Select<String>( ) : Filter.Select<String>(
displayName, displayName,
@ -359,63 +438,91 @@ abstract class MangaThemesia(
fun selectedValue() = vals[state].second fun selectedValue() = vals[state].second
} }
protected class StatusFilter : SelectFilter( protected class StatusFilter(
"Status", name: String,
arrayOf( options: Array<Pair<String, String>>,
Pair("All", ""), ) : SelectFilter(
Pair("Ongoing", "ongoing"), name,
Pair("Completed", "completed"), options,
Pair("Hiatus", "hiatus"),
Pair("Dropped", "dropped"),
),
) )
protected class TypeFilter : SelectFilter( protected open val statusOptions = arrayOf(
"Type", Pair(intl["status_filter_option_all"], ""),
arrayOf( Pair(intl["status_filter_option_ongoing"], "ongoing"),
Pair("All", ""), Pair(intl["status_filter_option_completed"], "completed"),
Pair("Manga", "Manga"), Pair(intl["status_filter_option_hiatus"], "hiatus"),
Pair("Manhwa", "Manhwa"), Pair(intl["status_filter_option_dropped"], "dropped"),
Pair("Manhua", "Manhua"),
Pair("Comic", "Comic"),
),
) )
protected class OrderByFilter(defaultOrder: String? = null) : SelectFilter( protected class TypeFilter(
"Sort By", name: String,
arrayOf( options: Array<Pair<String, String>>,
Pair("Default", ""), ) : SelectFilter(
Pair("A-Z", "title"), name,
Pair("Z-A", "titlereverse"), options,
Pair("Latest Update", "update"), )
Pair("Latest Added", "latest"),
Pair("Popular", "popular"), protected open val typeFilterOptions = arrayOf(
), Pair(intl["type_filter_option_all"], ""),
Pair(intl["type_filter_option_manga"], "Manga"),
Pair(intl["type_filter_option_manhwa"], "Manhwa"),
Pair(intl["type_filter_option_manhua"], "Manhua"),
Pair(intl["type_filter_option_comic"], "Comic"),
)
protected class OrderByFilter(
name: String,
options: Array<Pair<String, String>>,
defaultOrder: String? = null,
) : SelectFilter(
name,
options,
defaultOrder, defaultOrder,
) )
protected class ProjectFilter : SelectFilter( protected open val orderByFilterOptions = arrayOf(
"Filter Project", Pair(intl["order_by_filter_default"], ""),
arrayOf( Pair(intl["order_by_filter_az"], "title"),
Pair("Show all manga", ""), Pair(intl["order_by_filter_za"], "titlereverse"),
Pair("Show only project manga", "project-filter-on"), Pair(intl["order_by_filter_latest_update"], "update"),
), Pair(intl["order_by_filter_latest_added"], "latest"),
Pair(intl["order_by_filter_popular"], "popular"),
)
protected val popularFilter by lazy { FilterList(OrderByFilter("", orderByFilterOptions, "popular")) }
protected val latestFilter by lazy { FilterList(OrderByFilter("", orderByFilterOptions, "update")) }
protected class ProjectFilter(
name: String,
options: Array<Pair<String, String>>,
) : SelectFilter(
name,
options,
)
protected open val projectFilterOptions = arrayOf(
Pair(intl["project_filter_all_manga"], ""),
Pair(intl["project_filter_only_project"], "project-filter-on"),
)
protected class GenreData(
val name: String,
val value: String,
val state: Int = Filter.TriState.STATE_IGNORE,
) )
protected class Genre( protected class Genre(
name: String, name: String,
val value: String, val value: String,
state: Int = STATE_IGNORE, state: Int,
) : Filter.TriState(name, state) ) : Filter.TriState(name, state)
protected class GenreListFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres) protected class GenreListFilter(name: String, genres: List<Genre>) : Filter.Group<Genre>(name, genres)
protected var genrelist: List<GenreData>? = null
private var genrelist: List<Genre>? = null
protected open fun getGenreList(): List<Genre> { protected open fun getGenreList(): List<Genre> {
// Filters are fetched immediately once an extension loads return genrelist?.map { Genre(it.name, it.value, it.state) }.orEmpty()
// We're only able to get filters after a loading the manga directory,
// and resetting the filters is the only thing that seems to reinflate the view
return genrelist ?: listOf(Genre("Press reset to attempt to fetch genres", ""))
} }
open val hasProjectPage = false open val hasProjectPage = false
@ -423,21 +530,31 @@ abstract class MangaThemesia(
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>( val filters = mutableListOf<Filter<*>>(
Filter.Separator(), Filter.Separator(),
AuthorFilter(), AuthorFilter(intl["author_filter_title"]),
YearFilter(), YearFilter(intl["year_filter_title"]),
StatusFilter(), StatusFilter(intl["status_filter_title"], statusOptions),
TypeFilter(), TypeFilter(intl["type_filter_title"], typeFilterOptions),
OrderByFilter(), OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
Filter.Header("Genre exclusion is not available for all sources"),
GenreListFilter(getGenreList()),
) )
if (!genrelist.isNullOrEmpty()) {
filters.addAll(
listOf(
Filter.Header(intl["genre_exclusion_warning"]),
GenreListFilter(intl["genre_filter_title"], getGenreList()),
),
)
} else {
filters.add(
Filter.Header(intl["genre_missing_warning"]),
)
}
if (hasProjectPage) { if (hasProjectPage) {
filters.addAll( filters.addAll(
mutableListOf<Filter<*>>( mutableListOf<Filter<*>>(
Filter.Separator(), Filter.Separator(),
Filter.Header("NOTE: Can't be used with other filter!"), Filter.Header(intl["project_filter_warning"]),
Filter.Header("$name Project List page"), Filter.Header(intl.format("project_filter_name", name)),
ProjectFilter(), ProjectFilter(intl["project_filter_title"], projectFilterOptions),
), ),
) )
} }
@ -485,9 +602,9 @@ abstract class MangaThemesia(
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty()) (!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
} }
private fun parseGenres(document: Document): List<Genre>? { private fun parseGenres(document: Document): List<GenreData>? {
return document.selectFirst("ul.genrez")?.select("li")?.map { li -> return document.selectFirst("ul.genrez")?.select("li")?.map { li ->
Genre( GenreData(
li.selectFirst("label")!!.text(), li.selectFirst("label")!!.text(),
li.selectFirst("input[type=checkbox]")!!.attr("value"), li.selectFirst("input[type=checkbox]")!!.attr("value"),
) )
@ -514,15 +631,10 @@ abstract class MangaThemesia(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
addRandomUAPreferenceToScreen(screen)
}
companion object { companion object {
const val URL_SEARCH_PREFIX = "url:" const val URL_SEARCH_PREFIX = "url:"
// More info: https://issuetracker.google.com/issues/36970498 // More info: https://issuetracker.google.com/issues/36970498
@Suppress("RegExpRedundantEscape")
private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex() private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex() private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()

View File

@ -21,7 +21,7 @@ data class PizzaReaderDto(
@Serializable @Serializable
data class PizzaComicDto( data class PizzaComicDto(
val artist: String = "", val artist: String? = null,
val author: String = "", val author: String = "",
val chapters: List<PizzaChapterDto> = emptyList(), val chapters: List<PizzaChapterDto> = emptyList(),
val description: String = "", val description: String = "",

View File

@ -1,9 +0,0 @@
ext {
extName = 'BILIBILI COMICS'
extClass = '.BilibiliComicsFactory'
themePkg = 'bilibili'
baseUrl = 'https://www.bilibilicomics.com'
overrideVersionCode = 3
}
apply from: "$rootDir/common.gradle"

View File

@ -1,411 +0,0 @@
package eu.kanade.tachiyomi.extension.all.bilibilicomics
import eu.kanade.tachiyomi.multisrc.bilibili.Bilibili
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessToken
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessTokenCookie
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliComicDto
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliCredential
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliGetCredential
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliIntl
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliSearchDto
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliTag
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUnlockedEpisode
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUserEpisodes
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.Buffer
import java.io.IOException
import java.net.URLDecoder
import java.util.Calendar
class BilibiliComicsFactory : SourceFactory {
override fun createSources() = listOf(
BilibiliComicsEn(),
BilibiliComicsCn(),
BilibiliComicsId(),
BilibiliComicsEs(),
BilibiliComicsFr(),
)
}
abstract class BilibiliComics(lang: String) : Bilibili(
"BILIBILI COMICS",
"https://www.bilibilicomics.com",
lang,
) {
override val client: OkHttpClient = super.client.newBuilder()
.apply { interceptors().add(0, Interceptor { chain -> signedInIntercept(chain) }) }
.build()
init {
setAccessTokenCookie(baseUrl.toHttpUrl())
}
override val signedIn: Boolean
get() = accessTokenCookie != null
private val globalApiSubDomain: String
get() = GLOBAL_API_SUBDOMAINS[(accessTokenCookie?.area?.toIntOrNull() ?: 1) - 1]
private val globalApiBaseUrl: String
get() = "https://$globalApiSubDomain.bilibilicomics.com"
private var accessTokenCookie: BilibiliAccessTokenCookie? = null
private val dayOfWeek: Int
get() = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1
override fun latestUpdatesRequest(page: Int): Request {
val jsonPayload = buildJsonObject { put("day", dayOfWeek) }
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", requestBody.contentLength().toString())
.add("Content-Type", requestBody.contentType().toString())
.set("Referer", "$baseUrl/schedule")
.build()
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/GetSchedule".toHttpUrl().newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.parseAs<BilibiliSearchDto>()
if (result.code != 0) {
return MangasPage(emptyList(), hasNextPage = false)
}
val comicList = result.data!!.list.map(::latestMangaFromObject)
return MangasPage(comicList, hasNextPage = false)
}
protected open fun latestMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply {
title = comic.title
thumbnail_url = comic.verticalCover + THUMBNAIL_RESOLUTION
url = "/detail/mc${comic.comicId}"
}
override fun chapterListParse(response: Response): List<SChapter> {
if (!signedIn) {
return super.chapterListParse(response)
}
val result = response.parseAs<BilibiliComicDto>()
if (result.code != 0) {
return emptyList()
}
val comic = result.data!!
val userEpisodesRequest = userEpisodesRequest(comic.id)
val userEpisodesResponse = client.newCall(userEpisodesRequest).execute()
val unlockedEpisodes = userEpisodesParse(userEpisodesResponse)
return comic.episodeList.map { ep -> chapterFromObject(ep, comic.id, isUnlocked = ep.id in unlockedEpisodes) }
}
private fun userEpisodesRequest(comicId: Int): Request {
val jsonPayload = buildJsonObject { put("comic_id", comicId) }
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.set("Referer", baseUrl)
.build()
val apiUrl = "$globalApiBaseUrl/$API_COMIC_V1_USER_ENDPOINT/GetUserEpisodes".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
private fun userEpisodesParse(response: Response): List<Int> {
if (!response.isSuccessful) {
return emptyList()
}
val result = response.parseAs<BilibiliUserEpisodes>()
if (result.code != 0) {
return emptyList()
}
return result.data!!.unlockedEpisodes.orEmpty()
.map(BilibiliUnlockedEpisode::id)
}
override fun pageListRequest(chapter: SChapter): Request {
if (!signedIn) {
return super.pageListRequest(chapter)
}
val chapterPaths = (baseUrl + chapter.url).toHttpUrl().pathSegments
val comicId = chapterPaths[0].removePrefix("mc").toInt()
val episodeId = chapterPaths[1].toInt()
val jsonPayload = BilibiliGetCredential(comicId, episodeId, 1)
val requestBody = json.encodeToString(jsonPayload).toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.set("Referer", baseUrl + chapter.url)
.build()
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/GetCredential".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
override fun pageListParse(response: Response): List<Page> {
if (!signedIn) {
return super.pageListParse(response)
}
if (!response.isSuccessful) {
throw Exception(intl.failedToGetCredential)
}
val result = response.parseAs<BilibiliCredential>()
val credential = result.data?.credential ?: ""
val requestPayload = response.request.bodyString
val credentialInfo = json.decodeFromString<BilibiliGetCredential>(requestPayload)
val chapterUrl = "/mc${credentialInfo.comicId}/${credentialInfo.episodeId}"
val imageIndexRequest = imageIndexRequest(chapterUrl, credential)
val imageIndexResponse = client.newCall(imageIndexRequest).execute()
return super.pageListParse(imageIndexResponse)
}
private fun setAccessTokenCookie(url: HttpUrl) {
val authCookie = client.cookieJar.loadForRequest(url)
.firstOrNull { cookie -> cookie.name == ACCESS_TOKEN_COOKIE_NAME }
?.let { cookie -> URLDecoder.decode(cookie.value, "UTF-8") }
?.let { jsonString -> json.decodeFromString<BilibiliAccessTokenCookie>(jsonString) }
if (accessTokenCookie == null) {
accessTokenCookie = authCookie
} else if (authCookie == null) {
accessTokenCookie = null
}
}
private fun signedInIntercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val requestUrl = request.url.toString()
if (!requestUrl.contains("bilibilicomics.com")) {
return chain.proceed(request)
}
setAccessTokenCookie(request.url)
if (!accessTokenCookie?.accessToken.isNullOrEmpty()) {
request = request.newBuilder()
.addHeader("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
.build()
}
val response = chain.proceed(request)
// Try to refresh the token if it expired.
if (response.code == 401 && !accessTokenCookie?.refreshToken.isNullOrEmpty()) {
response.close()
val refreshTokenRequest = refreshTokenRequest(
accessTokenCookie!!.accessToken,
accessTokenCookie!!.refreshToken,
)
val refreshTokenResponse = chain.proceed(refreshTokenRequest)
accessTokenCookie = refreshTokenParse(refreshTokenResponse)
refreshTokenResponse.close()
request = request.newBuilder()
.header("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
.build()
return chain.proceed(request)
}
return response
}
private fun refreshTokenRequest(accessToken: String, refreshToken: String): Request {
val jsonPayload = buildJsonObject { put("refresh_token", refreshToken) }
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Authorization", "Bearer $accessToken")
.set("Referer", baseUrl)
.build()
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/RefreshToken".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
private fun refreshTokenParse(response: Response): BilibiliAccessTokenCookie {
if (!response.isSuccessful) {
throw IOException(intl.failedToRefreshToken)
}
val result = response.parseAs<BilibiliAccessToken>()
if (result.code != 0) {
throw IOException(intl.failedToRefreshToken)
}
val accessToken = result.data!!
return BilibiliAccessTokenCookie(
accessToken.accessToken,
accessToken.refreshToken,
accessTokenCookie!!.area,
)
}
private val Request.bodyString: String
get() {
val requestCopy = newBuilder().build()
val buffer = Buffer()
return runCatching { buffer.apply { requestCopy.body!!.writeTo(this) }.readUtf8() }
.getOrNull() ?: ""
}
companion object {
private const val ACCESS_TOKEN_COOKIE_NAME = "access_token"
private val GLOBAL_API_SUBDOMAINS = arrayOf("us-user", "sg-user")
private const val API_GLOBAL_V1_USER_ENDPOINT = "twirp/global.v1.User"
private const val API_COMIC_V1_USER_ENDPOINT = "twirp/comic.v1.User"
}
}
class BilibiliComicsEn : BilibiliComics(BilibiliIntl.ENGLISH) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("All", -1),
BilibiliTag("Action", 19),
BilibiliTag("Adventure", 22),
BilibiliTag("BL", 3),
BilibiliTag("Comedy", 14),
BilibiliTag("Eastern", 30),
BilibiliTag("Fantasy", 11),
BilibiliTag("GL", 16),
BilibiliTag("Harem", 15),
BilibiliTag("Historical", 12),
BilibiliTag("Horror", 23),
BilibiliTag("Mistery", 17),
BilibiliTag("Romance", 13),
BilibiliTag("Slice of Life", 21),
BilibiliTag("Suspense", 41),
BilibiliTag("Teen", 20),
)
}
class BilibiliComicsCn : BilibiliComics(BilibiliIntl.SIMPLIFIED_CHINESE) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("全部", -1),
BilibiliTag("校园", 18),
BilibiliTag("都市", 9),
BilibiliTag("耽美", 3),
BilibiliTag("少女", 20),
BilibiliTag("恋爱", 13),
BilibiliTag("奇幻", 11),
BilibiliTag("热血", 19),
BilibiliTag("冒险", 22),
BilibiliTag("古风", 12),
BilibiliTag("百合", 16),
BilibiliTag("玄幻", 30),
BilibiliTag("悬疑", 41),
BilibiliTag("科幻", 8),
)
}
class BilibiliComicsId : BilibiliComics(BilibiliIntl.INDONESIAN) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("Semua", -1),
BilibiliTag("Aksi", 19),
BilibiliTag("Fantasi Timur", 30),
BilibiliTag("Fantasi", 11),
BilibiliTag("Historis", 12),
BilibiliTag("Horror", 23),
BilibiliTag("Kampus", 18),
BilibiliTag("Komedi", 14),
BilibiliTag("Menegangkan", 41),
BilibiliTag("Remaja", 20),
BilibiliTag("Romantis", 13),
)
}
class BilibiliComicsEs : BilibiliComics(BilibiliIntl.SPANISH) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("Todos", -1),
BilibiliTag("Adolescencia", 105),
BilibiliTag("BL", 3),
BilibiliTag("Ciberdeportes", 104),
BilibiliTag("Ciencia ficción", 8),
BilibiliTag("Comedia", 14),
BilibiliTag("Fantasía occidental", 106),
BilibiliTag("Fantasía", 11),
BilibiliTag("Ficción Realista", 116),
BilibiliTag("GL", 16),
BilibiliTag("Histórico", 12),
BilibiliTag("Horror", 23),
BilibiliTag("Juvenil", 20),
BilibiliTag("Moderno", 111),
BilibiliTag("Oriental", 30),
BilibiliTag("Romance", 13),
BilibiliTag("Suspenso", 41),
BilibiliTag("Urbano", 9),
BilibiliTag("Wuxia", 103),
)
}
class BilibiliComicsFr : BilibiliComics(BilibiliIntl.FRENCH) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("Tout", -1),
BilibiliTag("BL", 3),
BilibiliTag("Science Fiction", 8),
BilibiliTag("Historique", 12),
BilibiliTag("Romance", 13),
BilibiliTag("GL", 16),
BilibiliTag("Fantasy Orientale", 30),
BilibiliTag("Suspense", 41),
BilibiliTag("Moderne", 111),
)
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'LANraragi' extName = 'LANraragi'
extClass = '.LANraragiFactory' extClass = '.LANraragiFactory'
extVersionCode = 16 extVersionCode = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -82,6 +82,17 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
return archiveToSManga(archive) return archiveToSManga(archive)
} }
override fun getMangaUrl(manga: SManga): String {
val namespace = preferences.getString(URL_TAG_PREFIX_KEY, URL_TAG_PREFIX_DEFAULT)
if (namespace.isNullOrEmpty()) {
return super.getMangaUrl(manga)
}
val tag = manga.genre?.split(", ")?.find { it.startsWith("$namespace") }
return tag?.substringAfter("$namespace") ?: super.getMangaUrl(manga)
}
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url) val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url)
val uri = getApiUriBuilder("/api/archives/$id/metadata").build() val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
@ -310,6 +321,7 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
screen.addPreference(screen.checkBoxPreference(CLEAR_NEW_KEY, "Clear New status", CLEAR_NEW_DEFAULT, "Clear an entry's New status when its details are viewed.")) screen.addPreference(screen.checkBoxPreference(CLEAR_NEW_KEY, "Clear New status", CLEAR_NEW_DEFAULT, "Clear an entry's New status when its details are viewed."))
screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT)) screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT))
screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added.")) screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added."))
screen.addPreference(screen.editTextPreference(URL_TAG_PREFIX_KEY, "Set tag prefix to get WebView URL", URL_TAG_PREFIX_DEFAULT, "Example: 'source:' will try to get the URL from the first tag starting with 'source:' and it will open it in the WebView. Leave empty for the default behavior."))
} }
private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference { private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference {
@ -494,5 +506,7 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
private const val SORT_BY_NS_KEY = "latestNamespacePref" private const val SORT_BY_NS_KEY = "latestNamespacePref"
private const val CLEAR_NEW_KEY = "clearNew" private const val CLEAR_NEW_KEY = "clearNew"
private const val CLEAR_NEW_DEFAULT = true private const val CLEAR_NEW_DEFAULT = true
private const val URL_TAG_PREFIX_KEY = "urlTagPrefix"
private const val URL_TAG_PREFIX_DEFAULT = ""
} }
} }

View File

@ -290,6 +290,7 @@ enum class Language {
RUSSIAN, RUSSIAN,
THAI, THAI,
VIETNAMESE, VIETNAMESE,
GERMAN,
} }
@Serializable @Serializable

View File

@ -13,5 +13,6 @@ class MangaPlusFactory : SourceFactory {
MangaPlus("ru", "rus", Language.RUSSIAN), MangaPlus("ru", "rus", Language.RUSSIAN),
MangaPlus("th", "tha", Language.THAI), MangaPlus("th", "tha", Language.THAI),
MangaPlus("vi", "vie", Language.VIETNAMESE), MangaPlus("vi", "vie", Language.VIETNAMESE),
MangaPlus("de", "deu", Language.GERMAN),
) )
} }

View File

@ -33,10 +33,11 @@ open class MiauScan(lang: String) : MangaThemesia(
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter } val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter }
val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter
?: GenreListFilter(emptyList()) ?: GenreListFilter("", emptyList())
val overloadedGenreFilter = GenreListFilter( val overloadedGenreFilter = GenreListFilter(
genres = genreFilter.state + listOf( genreFilter.name,
genreFilter.state + listOf(
Genre("", PORTUGUESE_GENRE_ID, portugueseMode), Genre("", PORTUGUESE_GENRE_ID, portugueseMode),
), ),
) )

View File

@ -31,8 +31,8 @@ class Mihentai : MangaThemesia("Mihentai", "https://mihentai.com", "all") {
listOf( listOf(
StatusFilter(), StatusFilter(),
TypeFilter(), TypeFilter(),
OrderByFilter(), OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
GenreListFilter(getGenreList()), GenreListFilter(intl["genre_filter_title"], getGenreList()),
), ),
) )
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.extension.ar.areamanga package eu.kanade.tachiyomi.extension.ar.areamanga
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.source.model.SManga
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -10,21 +9,4 @@ class AreaManga : MangaThemesia(
"https://www.areascans.net", "https://www.areascans.net",
"ar", "ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")), dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) { )
override val seriesArtistSelector =
".tsinfo .imptdt:contains(الرسام) i, ${super.seriesArtistSelector}"
override val seriesAuthorSelector =
".tsinfo .imptdt:contains(المؤلف) i, ${super.seriesAuthorSelector}"
override val seriesStatusSelector =
".tsinfo .imptdt:contains(الحالة) i, ${super.seriesStatusSelector}"
override val seriesTypeSelector =
".tsinfo .imptdt:contains(النوع) i, ${super.seriesTypeSelector}"
override fun String?.parseStatus() = when {
this == null -> SManga.UNKNOWN
this.contains("مستمر", ignoreCase = true) -> SManga.ONGOING
this.contains("مكتمل", ignoreCase = true) -> SManga.COMPLETED
this.contains("متوقف", ignoreCase = true) -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.MangaSwat' extClass = '.MangaSwat'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://swatmanhua.com' baseUrl = 'https://swatmanhua.com'
overrideVersionCode = 16 overrideVersionCode = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -7,6 +7,7 @@ import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.BuildConfig import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@ -23,12 +24,14 @@ import java.util.Locale
private const val swatUrl = "https://swatmanhua.com" private const val swatUrl = "https://swatmanhua.com"
class MangaSwat : MangaThemesia( class MangaSwat :
"MangaSwat", MangaThemesia(
swatUrl, "MangaSwat",
"ar", swatUrl,
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")), "ar",
) { dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
),
ConfigurableSource {
private val defaultBaseUrl = swatUrl private val defaultBaseUrl = swatUrl
override val baseUrl by lazy { getPrefBaseUrl() } override val baseUrl by lazy { getPrefBaseUrl() }
@ -58,6 +61,7 @@ class MangaSwat : MangaThemesia(
override fun searchMangaNextPageSelector() = "a[rel=next]" override fun searchMangaNextPageSelector() = "a[rel=next]"
override val seriesTitleSelector = "h1[itemprop=headline]"
override val seriesArtistSelector = "span:contains(الناشر) i" override val seriesArtistSelector = "span:contains(الناشر) i"
override val seriesAuthorSelector = "span:contains(المؤلف) i" override val seriesAuthorSelector = "span:contains(المؤلف) i"
override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a" override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a"
@ -113,8 +117,6 @@ class MangaSwat : MangaThemesia(
} }
} }
screen.addPreference(baseUrlPref) screen.addPreference(baseUrlPref)
super.setupPreferenceScreen(screen)
} }
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!! private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!

View File

@ -0,0 +1,7 @@
ext {
extName = 'RE Manga (Arabic)'
extClass = '.REManga'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,258 @@
package eu.kanade.tachiyomi.extension.ar.remangaarabic
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class REManga : ParsedHttpSource() {
override val name = "RE Manga"
override val baseUrl = "https://re-manga.com"
override val lang = "ar"
override val supportsLatest = true
// Popular
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/manga-list/?title=&order=popular&status=&type=")
override fun popularMangaSelector() = "article.animpost"
override fun popularMangaFromElement(element: Element): SManga =
SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("abs:href"))
element.select("img").let {
thumbnail_url = it.attr("abs:src")
title = it.attr("title")
}
}
override fun popularMangaNextPageSelector(): String? = null
// Latest
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/manga-list/?title=&order=update&status=&type=")
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/manga-list/?".toHttpUrl().newBuilder()
.addQueryParameter("title", query)
filters.forEach { filter ->
when (filter) {
is SortFilter -> url.addQueryParameter("order", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
is TypeFilter -> url.addQueryParameter("type", filter.toUriPart())
is GenreFilter -> {
filter.state
.filter { it.state != Filter.TriState.STATE_IGNORE }
.forEach { url.addQueryParameter("genre[]", it.id) }
}
is YearFilter -> {
filter.state
.filter { it.state != Filter.TriState.STATE_IGNORE }
.forEach { url.addQueryParameter("years[]", it.id) }
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("div.infox").first()!!.let { info ->
title = info.select("h1").text()
}
description = document.select("div.desc > div > p").text()
genre = document.select("div.spe > span:contains(نوع), div.genre-info > a").joinToString { it.text() }
document.select("div.spe > span:contains(الحالة)").first()?.text()?.also { statusText ->
when {
statusText.contains("مستمر", true) -> status = SManga.ONGOING
else -> status = SManga.COMPLETED
}
}
}
}
// Chapters
override fun chapterListSelector() = ".lsteps li"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
val chNum = element.select(".eps > a").first()!!.text()
val chTitle = element.select(".lchx > a").first()!!.text()
name = when {
chTitle.startsWith("الفصل ") -> chTitle
else -> "الفصل $chNum - $chTitle"
}
element.select(".date").first()?.text()?.let { date ->
date_upload = DATE_FORMATTER.parse(date)?.time ?: 0L
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select("div.reader-area img").mapIndexed { i, img ->
Page(i, "", img.attr("abs:src"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// Filters
override fun getFilterList() = FilterList(
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
TypeFilter(getTypeFilter()),
Filter.Separator(),
Filter.Header("exclusion not available for This source"),
GenreFilter(getGenreFilters()),
YearFilter(getYearFilters()),
)
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Sort by", vals)
private class TypeFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Type", vals)
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
class Genre(name: String, val id: String = name) : Filter.TriState(name)
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
class Year(name: String, val id: String = name) : Filter.TriState(name)
private class YearFilter(years: List<Year>) : Filter.Group<Year>("Year", years)
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("title", "A-Z"),
Pair("titlereverse", "Z-A"),
Pair("update", "Latest Update"),
Pair("latest", "Latest Added"),
Pair("popular", "Popular"),
)
private fun getStatusFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("", "All"),
Pair("Publishing", "مستمر"),
Pair("Finished", "تاريخ انتهي"),
)
private fun getTypeFilter(): Array<Pair<String?, String>> = arrayOf(
Pair("", "All"),
Pair("Manga", "Manga"),
Pair("Manhwa", "Manhwa"),
Pair("Manhua", "Manhua"),
)
private fun getGenreFilters(): List<Genre> = listOf(
Genre("Action", "action"),
Genre("Adventure", "adventure"),
Genre("Comedy", "comedy"),
Genre("Dementia", "dementia"),
Genre("Demons", "demons"),
Genre("Drama", "drama"),
Genre("Ecchi", "ecchi"),
Genre("Fantasy", "fantasy"),
Genre("Harem", "harem"),
Genre("Historical", "historical"),
Genre("Horror", "horror"),
Genre("Josei", "josei"),
Genre("Magic", "magic"),
Genre("Martial Arts", "martial-arts"),
Genre("Military", "military"),
Genre("Mystery", "mystery"),
Genre("Parody", "parody"),
Genre("Psychological", "psychological"),
Genre("Romance", "romance"),
Genre("Samurai", "samurai"),
Genre("School", "school"),
Genre("Sci-Fi", "sci-fi"),
Genre("Seinen", "seinen"),
Genre("Shounen", "shounen"),
Genre("Slice of Life", "slice-of-life"),
Genre("Sports", "sports"),
Genre("Super Power", "super-power"),
Genre("Supernatural", "supernatural"),
Genre("Vampire", "vampire"),
)
private fun getYearFilters(): List<Year> = listOf(
Year("1970", "1970"),
Year("1986", "1986"),
Year("1989", "1989"),
Year("1995", "1995"),
Year("1997", "1997"),
Year("1998", "1998"),
Year("1999", "1999"),
Year("2000", "2000"),
Year("2002", "2002"),
Year("2003", "2003"),
Year("2004", "2004"),
Year("2005", "2005"),
Year("2006", "2006"),
Year("2007", "2007"),
Year("2008", "2008"),
Year("2009", "2009"),
Year("2010", "2010"),
Year("2011", "2011"),
Year("2012", "2012"),
Year("2013", "2013"),
Year("2014", "2014"),
Year("2016", "2016"),
Year("2017", "2017"),
Year("2018", "2018"),
Year("2019", "2019"),
Year("2020", "2020"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MMM d, yyy", Locale("ar"))
}
}
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'StellarSaber'
extClass = '.StellarSaber'
themePkg = 'mangathemesia'
baseUrl = 'https://stellarsaber.pro'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.ar.stellarsaber
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
class StellarSaber : MangaThemesia(
"StellarSaber",
"https://stellarsaber.pro",
"ar",
dateFormat = SimpleDateFormat("MMMMM dd, yyyy", Locale("ar")),
)

View File

@ -0,0 +1,7 @@
ext {
extName = 'Alandal'
extClass = '.Alandal'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,189 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class Alandal : HttpSource() {
override val name = "Alandal"
override val baseUrl = "https://alandal.com"
private val apiUrl = "https://qq.alandal.com/api"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1)
.build()
override fun headersBuilder() = super.headersBuilder().apply {
add("Referer", "$baseUrl/")
}
private val apiHeaders by lazy { apiHeadersBuilder.build() }
private val apiHeadersBuilder = headersBuilder().apply {
add("Accept", "application/json")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "same-origin")
}
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(SortFilter("popular")))
override fun popularMangaParse(response: Response): MangasPage =
searchMangaParse(response)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(SortFilter("new")))
override fun latestUpdatesParse(response: Response): MangasPage =
searchMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("series")
if (query.isNotBlank()) {
addQueryParameter("name", query)
}
addQueryParameter("type", "comic")
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
addQueryParameter("page", page.toString())
}.build()
return GET(url, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage {
val data = response.parseAs<ResponseDto<SearchSeriesDto>>().data.series
val mangaList = data.data.map { it.toSManga() }
val hasNextPage = data.currentPage < data.lastPage
return MangasPage(mangaList, hasNextPage)
}
// =============================== Filters ==============================
override fun getFilterList(): FilterList = FilterList(
GenreFilter(),
SortFilter(),
StatusFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga): String =
baseUrl + manga.url.replace("series/", "series/comic-")
override fun mangaDetailsRequest(manga: SManga): Request {
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegments(manga.url.substringAfter("/"))
addQueryParameter("type", "comic")
}.build()
return GET(url, apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga =
response.parseAs<ResponseDto<MangaDetailsDto>>().data.series.toSManga()
// ============================== Chapters ==============================
override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url
.replace("series/", "chapter/comic-")
.replace("chapters/", "")
}
override fun chapterListRequest(manga: SManga): Request {
val url = "$apiUrl${manga.url}".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
addQueryParameter("type", "comic")
addQueryParameter("from", "0")
addQueryParameter("to", "999")
}.build()
return GET(url, apiHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val slug = response.request.url.newBuilder()
.query(null)
.removePathSegment(0) // Remove /api
.build()
.encodedPath
return response.parseAs<ChapterResponseDto>().data.map {
it.toSChapter(slug)
}.reversed()
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.name.startsWith("[LOCKED]")) {
throw Exception("Log in and unlock chapter in webview, then refresh chapter list")
}
val url = "$apiUrl${chapter.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("type", "comic")
addQueryParameter("traveler", "0")
}.build()
return GET(url, apiHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAs<PagesResponseDto>().data.chapter.chapter
return data.pages.mapIndexed { index, s ->
Page(index, imageUrl = s)
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
val pageHeaders = headersBuilder().apply {
add("Accept", "image/avif,image/webp,*/*")
add("Host", page.imageUrl!!.toHttpUrl().host)
}.build()
return GET(page.imageUrl!!, pageHeaders)
}
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
}

View File

@ -0,0 +1,118 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class ResponseDto<T>(
val data: ResultDto<T>,
) {
@Serializable
class ResultDto<T>(
val series: T,
)
}
@Serializable
class SearchSeriesDto(
@SerialName("current_page") val currentPage: Int,
@SerialName("last_page") val lastPage: Int,
val data: List<SearchEntryDto>,
) {
@Serializable
class SearchEntryDto(
val name: String,
val slug: String,
val cover: String,
) {
fun toSManga(): SManga = SManga.create().apply {
title = name
url = "/series/$slug"
thumbnail_url = cover
}
}
}
@Serializable
class MangaDetailsDto(
val name: String,
val summary: String,
val status: NamedObject,
val genres: List<NamedObject>,
val creators: List<NamedObject>,
val cover: String,
) {
@Serializable
class NamedObject(
val name: String,
val type: String? = null,
)
fun toSManga(): SManga = SManga.create().apply {
title = name
thumbnail_url = cover
description = Jsoup.parseBodyFragment(summary).text()
genre = genres.joinToString { it.name }
author = creators.filter { it.type!! == "author" }.joinToString { it.name }
status = this@MangaDetailsDto.status.name.parseStatus()
}
private fun String.parseStatus(): Int = when (this.lowercase()) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class ChapterResponseDto(
val data: List<ChapterDto>,
) {
@Serializable
class ChapterDto(
val name: String,
@SerialName("published_at") val published: String,
val access: Boolean,
) {
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
val prefix = if (access) "" else "[LOCKED] "
name = "${prefix}Chapter ${this@ChapterDto.name}"
date_upload = try {
dateFormat.parse(published)!!.time
} catch (_: ParseException) {
0L
}
url = "$slug/${this@ChapterDto.name}"
}
companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
}
}
}
@Serializable
class PagesResponseDto(
val data: PagesDataDto,
) {
@Serializable
class PagesDataDto(
val chapter: PagesChapterDto,
) {
@Serializable
class PagesChapterDto(
val chapter: PagesChapterImagesDto,
) {
@Serializable
class PagesChapterImagesDto(
val pages: List<String>,
)
}
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
if (checked.isEmpty()) {
builder.addQueryParameter(param, "-1")
} else {
checked.forEach {
builder.addQueryParameter(param, it.value)
}
}
}
}
class GenreFilter : UriMultiSelectFilter(
"Genre",
"genres",
arrayOf(
Pair("Action", "1"),
Pair("Fantasy", "2"),
Pair("Regression", "3"),
Pair("Overpowered", "4"),
Pair("Ascension", "5"),
Pair("Revenge", "6"),
Pair("Martial Arts", "7"),
Pair("Magic", "8"),
Pair("Necromancer", "9"),
Pair("Adventure", "10"),
Pair("Tower", "11"),
Pair("Dungeons", "12"),
Pair("Psychological", "13"),
Pair("Isekai", "14"),
),
)
class SortFilter(defaultSort: String? = null) : UriPartFilter(
"Sort By",
"sort",
arrayOf(
Pair("Popularity", "popular"),
Pair("Name", "name"),
Pair("Chapters", "chapters"),
Pair("Rating", "Rating"),
Pair("New", "new"),
),
defaultSort,
)
class StatusFilter : UriPartFilter(
"Status",
"status",
arrayOf(
Pair("Any", "-1"),
Pair("Ongoing", "1"),
Pair("Coming Soon", "5"),
Pair("Completed", "6"),
),
)

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anchira' extName = 'Anchira'
extClass = '.Anchira' extClass = '.Anchira'
extVersionCode = 8 extVersionCode = 10
isNsfw = true isNsfw = true
} }

View File

@ -6,6 +6,7 @@ import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.createChapter
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -23,6 +24,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -33,6 +35,8 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.ceil
import kotlin.math.min
class Anchira : HttpSource(), ConfigurableSource { class Anchira : HttpSource(), ConfigurableSource {
override val name = "Anchira" override val name = "Anchira"
@ -109,6 +113,23 @@ class Anchira : HttpSource(), ConfigurableSource {
fetchMangaDetails(manga).map { fetchMangaDetails(manga).map {
MangasPage(listOf(it), false) MangasPage(listOf(it), false)
} }
} else if (query.startsWith(SLUG_BUNDLE_PREFIX)) {
// bundle entries as chapters
val url = applyFilters(
page,
query.substringAfter(SLUG_BUNDLE_PREFIX),
filters,
).removeAllQueryParameters("page")
if (
url.build().queryParameter("sort") == "4"
) {
url.removeAllQueryParameters("sort")
}
val manga = SManga.create()
.apply { this.url = "?${url.build().query}" }
fetchMangaDetails(manga).map {
MangasPage(listOf(it), false)
}
} else { } else {
// regular filtering without text search // regular filtering without text search
client.newCall(searchMangaRequest(page, query, filters)) client.newCall(searchMangaRequest(page, query, filters))
@ -116,108 +137,171 @@ class Anchira : HttpSource(), ConfigurableSource {
.map(::searchMangaParse) .map(::searchMangaParse)
} }
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
GET(applyFilters(page, query, filters).build(), headers)
private fun applyFilters(page: Int, query: String, filters: FilterList): HttpUrl.Builder {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val trendingFilter = filterList.findInstance<TrendingFilter>()
val sortTrendingFilter = filters.findInstance<SortTrendingFilter>()
var url = libraryUrl.toHttpUrl().newBuilder() var url = libraryUrl.toHttpUrl().newBuilder()
url.addQueryParameter("page", page.toString()) if (trendingFilter?.state == true) {
val interval = when (sortTrendingFilter?.state) {
1 -> "3"
else -> ""
}
if (query.isNotBlank()) { if (interval.isNotBlank()) url.setQueryParameter("interval", interval)
url.addQueryParameter("s", query)
}
filters.forEach { filter -> url = url.toString().replace("library", "trending").toHttpUrl()
when (filter) { .newBuilder()
is CategoryGroup -> { } else {
var sum = 0 if (query.isNotBlank()) {
url.setQueryParameter("s", query)
}
filter.state.forEach { category -> filters.forEach { filter ->
when (category.name) { when (filter) {
"Manga" -> if (category.state) sum = sum or 1 is CategoryGroup -> {
"Doujinshi" -> if (category.state) sum = sum or 2 var sum = 0
"Illustration" -> if (category.state) sum = sum or 4
filter.state.forEach { category ->
when (category.name) {
"Manga" -> if (category.state) sum = sum or 1
"Doujinshi" -> if (category.state) sum = sum or 2
"Illustration" -> if (category.state) sum = sum or 4
}
}
if (sum > 0) url.setQueryParameter("cat", sum.toString())
}
is SortFilter -> {
val sort = when (filter.state?.index) {
0 -> "1"
1 -> "2"
2 -> "4"
4 -> "32"
else -> ""
}
if (sort.isNotEmpty()) url.setQueryParameter("sort", sort)
if (filter.state?.ascending == true) url.setQueryParameter("order", "1")
}
is FavoritesFilter -> {
if (filter.state) {
if (!isLoggedIn()) {
throw IOException("No login cookie found")
}
url = url.toString().replace("library", "user/favorites").toHttpUrl()
.newBuilder()
} }
} }
if (sum > 0) url.addQueryParameter("cat", sum.toString()) else -> {}
} }
is SortFilter -> {
val sort = when (filter.state?.index) {
0 -> "1"
1 -> "2"
2 -> "4"
4 -> "32"
else -> ""
}
if (sort.isNotEmpty()) url.addQueryParameter("sort", sort)
if (filter.state?.ascending == true) url.addQueryParameter("order", "1")
}
is FavoritesFilter -> {
if (filter.state) {
if (!isLoggedIn()) {
throw IOException("No login cookie found")
}
url = url.toString().replace("library", "user/favorites").toHttpUrl()
.newBuilder()
}
}
else -> {}
} }
} }
return GET(url.build(), headers) if (page > 1) {
url.setQueryParameter("page", page.toString())
}
return url
} }
override fun searchMangaParse(response: Response) = latestUpdatesParse(response) override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
// Details // Details
override fun mangaDetailsRequest(manga: SManga) = override fun mangaDetailsRequest(manga: SManga): Request {
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers) return if (manga.url.startsWith("?")) {
GET(libraryUrl + manga.url, headers)
override fun mangaDetailsParse(response: Response): SManga { } else {
val data = json.decodeFromString<Entry>(response.body.string()) GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
return SManga.create().apply {
url = "/g/${data.id}/${data.key}"
title = data.title
thumbnail_url =
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
genre = prepareTags(data.tags, preferences.useTagGrouping)
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
} }
} }
override fun getMangaUrl(manga: SManga) = if (preferences.openSource) { override fun mangaDetailsParse(response: Response): SManga {
val id = manga.url.split("/").reversed()[1].toInt() return if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}" val manga = latestUpdatesParse(response).mangas.first()
} else { val query = response.request.url.queryParameter("s")
"$baseUrl${manga.url}" val cleanTitle = CHAPTER_SUFFIX_RE.replace(manga.title, "").trim()
manga.apply {
url = "?${response.request.url.query}"
description = "Bundled from $query"
title = "[Bundle] $cleanTitle"
update_strategy = UpdateStrategy.ALWAYS_UPDATE
}
} else {
val data = json.decodeFromString<Entry>(response.body.string())
SManga.create().apply {
url = "/g/${data.id}/${data.key}"
title = data.title
thumbnail_url =
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
genre = prepareTags(data.tags, preferences.useTagGrouping)
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
}
}
} }
override fun getMangaUrl(manga: SManga) =
if (preferences.openSource && !manga.url.startsWith("?")) {
val id = manga.url.split("/").reversed()[1].toInt()
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
} else {
"$baseUrl${manga.url}"
}
// Chapter // Chapter
override fun chapterListRequest(manga: SManga) = override fun chapterListRequest(manga: SManga): Request {
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers) return if (manga.url.startsWith("?")) {
GET(libraryUrl + manga.url, headers)
} else {
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
}
}
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val data = json.decodeFromString<Entry>(response.body.string()) val chapterList = mutableListOf<SChapter>()
if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
return listOf( var results = json.decodeFromString<LibraryResponse>(response.body.string())
SChapter.create().apply { val pages = min(5, ceil((results.total.toFloat() / results.limit)).toInt())
url = "/g/${data.id}/${data.key}" for (page in 1..pages) {
name = "Chapter" results.entries.forEach { data ->
date_upload = data.publishedAt * 1000 chapterList.add(
chapter_number = 1f createChapter(data, response, anchiraData),
}, )
) }
if (page < pages) {
results = json.decodeFromString<LibraryResponse>(
client.newCall(
GET(
response.request.url.newBuilder()
.setQueryParameter("page", (page + 1).toString()).build(),
headers,
),
).execute().body.string(),
)
}
}
} else {
val data = json.decodeFromString<Entry>(response.body.string())
chapterList.add(
createChapter(data, response, anchiraData),
)
}
return chapterList
} }
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}" override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}"
@ -278,14 +362,16 @@ class Anchira : HttpSource(), ConfigurableSource {
val openSourcePref = SwitchPreferenceCompat(screen.context).apply { val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
key = OPEN_SOURCE_PREF key = OPEN_SOURCE_PREF
title = "Open source website in WebView" title = "Open source website in WebView"
summary = "Enable to open the original source website of the gallery (if available) instead of Anchira." summary =
"Enable to open the original source website of the gallery (if available) instead of Anchira."
setDefaultValue(false) setDefaultValue(false)
} }
val useTagGrouping = SwitchPreferenceCompat(screen.context).apply { val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
key = USE_TAG_GROUPING key = USE_TAG_GROUPING
title = "Group tags" title = "Group tags"
summary = "Enable to group tags together by artist, circle, parody, magazine and general tags" summary =
"Enable to group tags together by artist, circle, parody, magazine and general tags"
setDefaultValue(false) setDefaultValue(false)
} }
@ -298,6 +384,10 @@ class Anchira : HttpSource(), ConfigurableSource {
CategoryGroup(), CategoryGroup(),
SortFilter(), SortFilter(),
FavoritesFilter(), FavoritesFilter(),
Filter.Separator(),
Filter.Header("Others are ignored if trending only"),
TrendingFilter(),
SortTrendingFilter(),
) )
private class CategoryFilter(name: String) : Filter.CheckBox(name, false) private class CategoryFilter(name: String) : Filter.CheckBox(name, false)
@ -317,6 +407,18 @@ class Anchira : HttpSource(), ConfigurableSource {
Selection(2, false), Selection(2, false),
) )
private class TrendingFilter : Filter.CheckBox(
"Show only trending",
)
private class SortTrendingFilter : PartFilter(
"Sort By",
arrayOf("Trending: Weekly", "Trending: Monthly"),
)
private open class PartFilter(displayName: String, value: Array<String>) :
Filter.Select<String>(displayName, value)
private val SharedPreferences.imageQuality private val SharedPreferences.imageQuality
get() = getString(IMAGE_QUALITY_PREF, "b")!! get() = getString(IMAGE_QUALITY_PREF, "b")!!
@ -362,8 +464,11 @@ class Anchira : HttpSource(), ConfigurableSource {
.use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) } .use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) }
} }
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
companion object { companion object {
const val SLUG_SEARCH_PREFIX = "id:" const val SLUG_SEARCH_PREFIX = "id:"
const val SLUG_BUNDLE_PREFIX = "bundle:"
private const val IMAGE_QUALITY_PREF = "image_quality" private const val IMAGE_QUALITY_PREF = "image_quality"
private const val OPEN_SOURCE_PREF = "use_manga_source" private const val OPEN_SOURCE_PREF = "use_manga_source"
private const val USE_TAG_GROUPING = "use_tag_grouping" private const val USE_TAG_GROUPING = "use_tag_grouping"
@ -371,3 +476,5 @@ class Anchira : HttpSource(), ConfigurableSource {
"https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json" "https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
} }
} }
val CHAPTER_SUFFIX_RE = Regex("(?<!20\\d\\d-)\\b[\\d.]{1,4}$")

View File

@ -3,15 +3,6 @@ package eu.kanade.tachiyomi.extension.en.anchira
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
data class ListEntry(
val id: Int,
val key: String,
val title: String,
@SerialName("thumb_index") val thumbnailIndex: Int,
val tags: List<Tag> = emptyList(),
)
@Serializable @Serializable
data class Tag( data class Tag(
var name: String, var name: String,
@ -20,7 +11,7 @@ data class Tag(
@Serializable @Serializable
data class LibraryResponse( data class LibraryResponse(
val entries: List<ListEntry> = emptyList(), val entries: List<Entry> = emptyList(),
val total: Int, val total: Int,
val page: Int, val page: Int,
val limit: Int, val limit: Int,
@ -30,11 +21,12 @@ data class LibraryResponse(
data class Entry( data class Entry(
val id: Int, val id: Int,
val key: String, val key: String,
@SerialName("published_at") val publishedAt: Long, @SerialName("published_at") val publishedAt: Long = 0L,
val title: String, val title: String,
@SerialName("thumb_index") val thumbnailIndex: Int, @SerialName("thumb_index") val thumbnailIndex: Int = 1,
val tags: List<Tag> = emptyList(), val tags: List<Tag> = emptyList(),
val url: String? = null, val url: String? = null,
val pages: Int = 1,
) )
@Serializable @Serializable

View File

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.extension.en.anchira package eu.kanade.tachiyomi.extension.en.anchira
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.Response
import java.util.Locale
object AnchiraHelper { object AnchiraHelper {
fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}" fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
@ -25,4 +29,31 @@ object AnchiraHelper {
} }
} }
.joinToString(", ") { it } .joinToString(", ") { it }
fun createChapter(entry: Entry, response: Response, anchiraData: List<EntryKey>) =
SChapter.create().apply {
val ch =
CHAPTER_SUFFIX_RE.find(entry.title)?.value?.trim('.') ?: "1"
val source = anchiraData.find { it.id == entry.id }?.url
?: response.request.url.toString()
url = "/g/${entry.id}/${entry.key}"
name = "$ch. ${entry.title.removeSuffix(" $ch")}"
date_upload = entry.publishedAt * 1000
chapter_number = ch.toFloat()
scanlator = buildString {
append(
Regex("fakku|irodori|anchira").find(source)?.value.orEmpty()
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.getDefault(),
)
} else {
it.toString()
}
},
)
append(" - ${entry.pages} pages")
}
}
} }

View File

@ -1,107 +0,0 @@
package eu.kanade.tachiyomi.extension.en.anchira
object XXTEA {
private const val DELTA = -0x61c88647
@Suppress("NOTHING_TO_INLINE", "FunctionName")
private inline fun MX(sum: Int, y: Int, z: Int, p: Int, e: Int, k: IntArray): Int {
return (z.ushr(5) xor (y shl 2)) + (y.ushr(3) xor (z shl 4)) xor (sum xor y) + (k[p and 3 xor e] xor z)
}
private fun decrypt(data: ByteArray, key: ByteArray): ByteArray =
data.takeIf { it.isNotEmpty() }
?.let {
decrypt(data.toIntArray(false), key.fixKey().toIntArray(false))
.toByteArray(true)
} ?: data
fun decrypt(data: ByteArray, key: String): ByteArray? =
kotlin.runCatching { decrypt(data, key.toByteArray(Charsets.UTF_8)) }.getOrNull()
fun decryptToString(data: ByteArray, key: String): String? =
kotlin.runCatching { decrypt(data, key)?.toString(Charsets.UTF_8) }.getOrNull()
private fun decrypt(v: IntArray, k: IntArray): IntArray {
val n = v.size - 1
if (n < 1) {
return v
}
var p: Int
val q = 6 + 52 / (n + 1)
var z: Int
var y = v[0]
var sum = q * DELTA
var e: Int
while (sum != 0) {
e = sum.ushr(2) and 3
p = n
while (p > 0) {
z = v[p - 1]
v[p] -= MX(sum, y, z, p, e, k)
y = v[p]
p--
}
z = v[n]
v[0] -= MX(sum, y, z, p, e, k)
y = v[0]
sum -= DELTA
}
return v
}
private fun ByteArray.fixKey(): ByteArray {
if (size == 16) return this
val fixedKey = ByteArray(16)
if (size < 16) {
copyInto(fixedKey)
} else {
copyInto(fixedKey, endIndex = 16)
}
return fixedKey
}
private fun ByteArray.toIntArray(includeLength: Boolean): IntArray {
var n = if (size and 3 == 0) {
size.ushr(2)
} else {
size.ushr(2) + 1
}
val result: IntArray
if (includeLength) {
result = IntArray(n + 1)
result[n] = size
} else {
result = IntArray(n)
}
n = size
for (i in 0 until n) {
result[i.ushr(2)] =
result[i.ushr(2)] or (0x000000ff and this[i].toInt() shl (i and 3 shl 3))
}
return result
}
private fun IntArray.toByteArray(includeLength: Boolean): ByteArray? {
var n = size shl 2
if (includeLength) {
val m = this[size - 1]
n -= 4
if (m < n - 3 || m > n) {
return null
}
n = m
}
val result = ByteArray(n)
for (i in 0 until n) {
result[i] = this[i.ushr(2)].ushr(i and 3 shl 3).toByte()
}
return result
}
}

View File

@ -6,6 +6,7 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -26,12 +27,14 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class AsuraScans : MangaThemesia( class AsuraScans :
"Asura Scans", MangaThemesia(
"https://asuratoon.com", "Asura Scans",
"en", "https://asuratoon.com",
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US), "en",
) { dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
),
ConfigurableSource {
private val preferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
@ -281,8 +284,6 @@ class AsuraScans : MangaThemesia(
summary = PREF_PERM_MANGA_URL_SUMMARY summary = PREF_PERM_MANGA_URL_SUMMARY
setDefaultValue(true) setDefaultValue(true)
}.also(screen::addPreference) }.also(screen::addPreference)
super.setupPreferenceScreen(screen)
} }
private val SharedPreferences.permaUrlPref private val SharedPreferences.permaUrlPref

View File

@ -8,3 +8,7 @@ ext {
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:randomua"))
}

View File

@ -2,11 +2,14 @@ package eu.kanade.tachiyomi.extension.en.constellarscans
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
@ -19,22 +22,29 @@ import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcomic.com", "en") { class ConstellarScans :
MangaThemesia(
"Constellar Scans",
"https://constellarcomic.com",
"en",
),
ConfigurableSource {
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
addRandomUAPreferenceToScreen(screen)
}
override val client: OkHttpClient by lazy { override val client: OkHttpClient by lazy {
network.cloudflareClient.newBuilder() network.cloudflareClient.newBuilder()
.setRandomUserAgent( .setRandomUserAgent(
preferences.getPrefUAType(), preferences.getPrefUAType(),
preferences.getPrefCustomUA(), preferences.getPrefCustomUA(),
) )
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(1, 1) .rateLimit(1, 1)
.build() .build()
} }
@ -61,6 +71,8 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcom
.build() .build()
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
countViews(document)
val html = document.toString() val html = document.toString()
if (!html.contains("ts_rea_der_._run(\"")) { if (!html.contains("ts_rea_der_._run(\"")) {
return super.pageListParse(document) return super.pageListParse(document)
@ -80,7 +92,6 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcom
} }
.joinToString("") .joinToString("")
countViews(document)
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it -> return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
Page(idx, imageUrl = it.jsonPrimitive.content) Page(idx, imageUrl = it.jsonPrimitive.content)
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,9 +0,0 @@
package eu.kanade.tachiyomi.extension.en.firstmanhwa
import eu.kanade.tachiyomi.multisrc.madara.Madara
class FirstManhwa : Madara("1st Manhwa", "https://1stmanhwa.com", "en") {
override val useNewChapterEndpoint = true
override val filterNonMangaItems = false
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(Status) + div.summary-content"
}

View File

@ -9,6 +9,7 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -25,12 +26,14 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
class FlameComics : MangaThemesia( class FlameComics :
"Flame Comics", MangaThemesia(
"https://flamecomics.com", "Flame Comics",
"en", "https://flamecomics.com",
mangaUrlDirectory = "/series", "en",
) { mangaUrlDirectory = "/series",
),
ConfigurableSource {
// Flame Scans -> Flame Comics // Flame Scans -> Flame Comics
override val id = 6350607071566689772 override val id = 6350607071566689772

View File

@ -2,8 +2,8 @@ ext {
extName = 'Infernal Void Scans' extName = 'Infernal Void Scans'
extClass = '.InfernalVoidScans' extClass = '.InfernalVoidScans'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://void-scans.com' baseUrl = 'https://hivescans.com'
overrideVersionCode = 5 overrideVersionCode = 6
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -2,6 +2,6 @@ package eu.kanade.tachiyomi.extension.en.infernalvoidscans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://void-scans.com", "en") { class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://hivescans.com", "en") {
override val pageSelector = "div#readerarea > p > img" override val pageSelector = "div#readerarea > p > img"
} }

View File

@ -6,6 +6,7 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@ -20,7 +21,15 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException import java.io.IOException
class LuminousScans : MangaThemesia("Luminous Scans", "https://lumitoon.com", "en", mangaUrlDirectory = "/series") { class LuminousScans :
MangaThemesia(
"Luminous Scans",
"https://lumitoon.com",
"en",
mangaUrlDirectory = "/series",
),
ConfigurableSource {
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.addInterceptor(::urlChangeInterceptor) .addInterceptor(::urlChangeInterceptor)
.rateLimit(2) .rateLimit(2)
@ -201,8 +210,6 @@ class LuminousScans : MangaThemesia("Luminous Scans", "https://lumitoon.com", "e
summary = PREF_PERM_MANGA_URL_SUMMARY summary = PREF_PERM_MANGA_URL_SUMMARY
setDefaultValue(true) setDefaultValue(true)
}.also(screen::addPreference) }.also(screen::addPreference)
super.setupPreferenceScreen(screen)
} }
private val SharedPreferences.permaUrlPref private val SharedPreferences.permaUrlPref

View File

@ -33,19 +33,29 @@ class LunarScans : MangaThemesia(
val filters = mutableListOf<Filter<*>>( val filters = mutableListOf<Filter<*>>(
Filter.Header("Note: Can't be used with text search!"), Filter.Header("Note: Can't be used with text search!"),
Filter.Separator(), Filter.Separator(),
StatusFilter(), StatusFilter(intl["status_filter_title"], statusOptions),
TypeFilter(), TypeFilter(intl["type_filter_title"], typeFilterOptions),
OrderByFilter(), OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
Filter.Header("Genre exclusion is not available for all sources"),
GenreListFilter(getGenreList()),
) )
if (!genrelist.isNullOrEmpty()) {
filters.addAll(
listOf(
Filter.Header(intl["genre_exclusion_warning"]),
GenreListFilter(intl["genre_filter_title"], getGenreList()),
),
)
} else {
filters.add(
Filter.Header(intl["genre_missing_warning"]),
)
}
if (hasProjectPage) { if (hasProjectPage) {
filters.addAll( filters.addAll(
mutableListOf<Filter<*>>( mutableListOf<Filter<*>>(
Filter.Separator(), Filter.Separator(),
Filter.Header("NOTE: Can't be used with other filter!"), Filter.Header(intl["project_filter_warning"]),
Filter.Header("$name Project List page"), Filter.Header(intl.format("project_filter_name", name)),
ProjectFilter(), ProjectFilter(intl["project_filter_title"], projectFilterOptions),
), ),
) )
} }

View File

@ -1,9 +0,0 @@
ext {
extName = '247Manga'
extClass = '.Manga247'
themePkg = 'madara'
baseUrl = 'https://247manga.com'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,5 +0,0 @@
package eu.kanade.tachiyomi.extension.en.manga247
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Manga247 : Madara("247Manga", "https://247manga.com", "en")

View File

@ -1,9 +1,9 @@
ext { ext {
extName = 'Manga Galaxy' extName = 'Manga Galaxy'
extClass = '.MangaGalaxy' extClass = '.MangaGalaxy'
themePkg = 'madara' themePkg = 'mangathemesia'
baseUrl = 'https://mangagalaxy.me' baseUrl = 'https://mangagalaxy.me'
overrideVersionCode = 1 overrideVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,15 +1,13 @@
package eu.kanade.tachiyomi.extension.en.mangagalaxy package eu.kanade.tachiyomi.extension.en.mangagalaxy
import eu.kanade.tachiyomi.multisrc.madara.Madara import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
class MangaGalaxy : Madara( class MangaGalaxy : MangaThemesia(
"Manga Galaxy", "Manga Galaxy",
"https://mangagalaxy.me", "https://mangagalaxy.me",
"en", "en",
dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US), mangaUrlDirectory = "/series",
) { ) {
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(status) + div.summary-content" // moved from Madara to MangaThemesia
override val mangaDetailsSelectorDescription = "div.summary-heading:contains(Summary) + div" override val versionId = 2
} }

View File

@ -0,0 +1,8 @@
ext {
extName = 'MangaMonks'
extClass = '.MangaMonks'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,267 @@
package eu.kanade.tachiyomi.extension.en.mangamonks
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.buildApiHeaders
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toDate
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toFormRequestBody
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toStatus
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.MissingFieldException
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
class MangaMonks : ParsedHttpSource() {
override val name = "MangaMonks"
override val baseUrl = "https://mangamonks.com"
override val lang = "en"
override val supportsLatest = true
// popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/popular-manga/$page", headers)
}
override fun popularMangaSelector() = ".main-slide"
override fun popularMangaNextPageSelector() = "li:nth-last-child(2) a.page-btn"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.selectFirst(".detail a")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
thumbnail_url = element.select("img").attr("data-src")
}
}
// latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/latest-releases/$page", headers)
}
override fun latestUpdatesSelector() = ".tab-pane .row .col-12"
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
// search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it }
return if (query.isNotEmpty()) {
val requestBody = query.toFormRequestBody()
val requestHeaders = headersBuilder().buildApiHeaders(requestBody)
POST("$baseUrl/search/live", requestHeaders, requestBody)
} else {
val url = "$baseUrl/genre/".toHttpUrl().newBuilder()
filterList.forEach { filter ->
when (filter) {
is GenreFilter -> filter.toUriPart().let {
url.apply {
addPathSegment(it)
addQueryParameter("include[]", filter.toGenreValue())
}
}
is StatusFilter -> filter.toUriPart().let {
url.apply {
addQueryParameter("term", query)
addQueryParameter("status[]", it)
}
}
else -> {}
}
}
url.addPathSegment(page.toString())
GET(url.build(), headers)
}
}
override fun searchMangaSelector() = ".main-slide .item"
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
private val json: Json by injectLazy()
override fun searchMangaParse(response: Response): MangasPage {
val isJson = response.header("Content-Type")?.contains("application/json") ?: false
if (isJson) {
return try {
val result = json.decodeFromString<MangaList>(response.body.string())
val mangaList = result.manga.map {
SManga.create().apply {
title = it.title
setUrlWithoutDomain(it.url)
thumbnail_url = it.image
}
}
val hasNextPage = false
MangasPage(mangaList, hasNextPage)
} catch (_: MissingFieldException) {
MangasPage(emptyList(), false)
}
} else {
val document = response.asJsoup()
val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector().let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
}
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
// details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
author = document.selectFirst(".publisher a")!!.text()
status = document.selectFirst(".info-detail .source")!!.text().toStatus()
genre = document.select(".info-detail .tags a").joinToString { it.text() }
description = document.select(".info-desc p").text()
thumbnail_url = document.select(".img-holder img").attr("data-src")
}
}
// chapters
override fun chapterListSelector() = ".chapter-list li"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
name = element.select(".chapter-number").text()
date_upload = element.select(".time").text().trim().toDate()
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// pages
override fun pageListParse(document: Document): List<Page> {
return document.select("#zoomContainer .image img").mapIndexed { i, it ->
val src = it.attr("src")
val imageUrl = if (src.startsWith("https")) src else baseUrl + src
Page(i, imageUrl = imageUrl)
}
}
// filters
override fun getFilterList() = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
StatusFilter(),
GenreFilter(),
)
private class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
),
)
private class GenreFilter : GenreValueFilter(
"Genre",
arrayOf(
Triple("Action", "2", "action"),
Triple("Adventure", "3", "adventure"),
Triple("Comedy", "5", "comedy"),
Triple("Cooking", "6", "cooking"),
Triple("Doujinshi", "7", "doujinshi"),
Triple("Drama", "8", "drama"),
Triple("Ecchi", "9", "ecchi"),
Triple("Yaoi", "11", "yaoi"),
Triple("Fantasy", "12", "fantasy"),
Triple("Gender Bender", "13", "gender-bender"),
Triple("Harem", "14", "harem"),
Triple("Historical", "15", "historical"),
Triple("Horror", "16", "horror"),
Triple("Josei", "17", "josei"),
Triple("Manhua", "18", "manhua"),
Triple("Manhwa", "19", "manhwa"),
Triple("Mecha", "21", "mecha"),
Triple("Mystery", "24", "mystery"),
Triple("One Shot", "25", "one-shot"),
Triple("Psychological", "26", "psychological"),
Triple("Romance", "27", "romance"),
Triple("School Life", "28", "school-life"),
Triple("Sci-fi", "29", "sci-fi"),
Triple("Seinen", "30", "seinen"),
Triple("Yuri", "31", "yuri"),
Triple("Shoujo", "32", "shoujo"),
Triple("Shounen", "34", "shounen"),
Triple("Shounen Ai", "35", "shounen-ai"),
Triple("Slice of Life", "36", "slice-of-life"),
Triple("Sports", "37", "sports"),
Triple("Supernatural", "38", "supernatural"),
Triple("Tragedy", "39", "tragedy"),
Triple("Webtoons", "40", "webtoons"),
Triple("Full Color", "42", "full-color"),
Triple("Isekai", "44", "isekai"),
Triple("Reincarnation", "45", "reincarnation"),
Triple("Time Travel", "46", "time-travel"),
Triple("Martial arts", "48", "martial-arts"),
Triple("Monsters", "49", "monsters-monsters"),
Triple("Thriller", "51", "thriller"),
Triple("Adaptation", "52", "adaptation"),
Triple("Reverse Harem", "53", "reverse-harem"),
Triple("Cross-dressing", "54", "cross-dressing"),
Triple("Zombies", "55", "zombies"),
Triple("Crime", "56", "crime"),
Triple("Ghosts", "57", "ghosts"),
Triple("Magic", "58", "magic"),
Triple("Gore", "59", "gore"),
Triple("+18", "84", "18"),
Triple("LGBT", "47", "lgbt"),
Triple("erotic", "62", "erotic"),
Triple("Harem", "63", "harem-harem"),
Triple("MILF", "64", "milf"),
Triple("Yaoi/boy's love", "65", "yaoiboys-love"),
Triple("Yuri/girl's love", "66", "yurigirls-love"),
Triple("BBW", "67", "bbw"),
Triple("Shota", "68", "shota"),
Triple("NTR/cheating", "69", "ntrcheating"),
Triple("BDSM", "70", "bdsm"),
Triple("tentacle", "71", "tentacle"),
Triple("Oyasumi/sleeping", "72", "oyasumisleeping"),
Triple("Elf Hentai", "74", "elf-hentai"),
Triple("Rape", "75", "rape"),
Triple("Incest", "76", "incest"),
Triple("Inseki", "77", "inseki"),
Triple("LGBTQ", "78", "lgbtq"),
Triple("Beastiality", "79", "bestiality"),
Triple("Defloration", "80", "defloration"),
Triple("loli", "81", "loli"),
Triple("Raw", "83", "raw"),
),
)
private open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private open class GenreValueFilter(displayName: String, private val vals: Array<Triple<String, String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].third
fun toGenreValue() = vals[state].second
}
@Serializable
class MangaList(val manga: List<MangaItem>)
@Serializable
class MangaItem(val title: String, val url: String, val image: String)
}

View File

@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.extension.en.mangamonks
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.RequestBody
import java.util.Calendar
object MangaMonksHelper {
fun Headers.Builder.buildApiHeaders(requestBody: RequestBody) = this
.add("Content-Length", requestBody.contentLength().toString())
.add("Content-Type", requestBody.contentType().toString())
.add("Accept", "application/json")
.add("X-Requested-With", "XMLHttpRequest")
.build()
inline fun <reified T : Any> T.toFormRequestBody(): RequestBody {
return FormBody.Builder()
.add("dataType", "json")
.add("phrase", this.toString())
.build()
}
fun String?.toStatus(): Int {
return when {
this == null -> SManga.UNKNOWN
this.contains("Ongoing", true) -> SManga.ONGOING
this.contains("Completed", true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
fun String?.toDate(): Long {
val trimmedDate = this!!.substringBefore(" ago").removeSuffix("s").split(" ")
val calendar = Calendar.getInstance()
when {
trimmedDate[1].contains(
"Year",
ignoreCase = true,
) -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Month",
ignoreCase = true,
) -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Week",
ignoreCase = true,
) -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Day",
ignoreCase = true,
) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Hour",
ignoreCase = true,
) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Minute",
ignoreCase = true,
) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
}
return calendar.timeInMillis
}
}

View File

@ -0,0 +1,7 @@
ext {
extName = 'MangaOwl.To'
extClass = '.MangaOwlToFactory'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,168 @@
package eu.kanade.tachiyomi.extension.en.mangaowlto
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class MangaOwlTo(
private val collection: String,
extraName: String,
private val genresList: List<Genre>,
) : ConfigurableSource, HttpSource() {
override val name: String = "MangaOwl.To $extraName"
override val lang = "en"
override val supportsLatest = true
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
private val defaultDomain: String =
preferences.getString(MIRROR_PREF_KEY, MIRROR_PREF_DEFAULT_VALUE)!!
override val baseUrl = "https://$defaultDomain"
private val apiUrl = "https://api.$defaultDomain/v1"
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val mirrorPref = ListPreference(screen.context).apply {
key = MIRROR_PREF_KEY
title = "Mirror (Requires Restart)"
entries = MIRROR_PREF_ENTRIES
entryValues = MIRROR_PREF_ENTRY_VALUES
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
summary = "%s"
}
screen.addPreference(mirrorPref)
}
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int) =
GET("$apiUrl/stories?type=$collection&ordering=-view_count&page=$page".toHttpUrl(), headers)
override fun popularMangaParse(response: Response) =
json.decodeFromString<MangaOwlToStories>(response.body.string()).toMangasPage()
// Latest
override fun latestUpdatesRequest(page: Int) =
GET("$apiUrl/stories?type=$collection&ordering=-modified_at&page=$page".toHttpUrl(), headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotEmpty() || filters.isEmpty()) {
// Search won't work together with filter
val url = "$apiUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.build()
GET(url, headers)
} else {
val url = "$apiUrl/stories?type=$collection".toHttpUrl().newBuilder()
filters.forEach { filter ->
when (filter) {
is SortFilter -> if (!filter.toUriPart().isNullOrEmpty()) {
url.addQueryParameter("ordering", filter.toUriPart())
}
is StatusFilter -> if (!filter.toUriPart().isNullOrEmpty()) {
url.addQueryParameter("status", filter.toUriPart())
}
is GenresFilter ->
filter.state
.filter { it.state }
.forEach { url.addQueryParameter("genres", it.uriPart) }
else -> {}
}
}
url.addQueryParameter("page", page.toString())
GET(url.build(), headers)
}
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
// Manga summary page
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$apiUrl/stories/${manga.url}", headers)
}
override fun mangaDetailsParse(response: Response) =
json.decodeFromString<MangaOwlToStory>(response.body.string()).toSManga()
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl/comic/${manga.url}"
}
// Chapters
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response) =
json.decodeFromString<MangaOwlToStory>(response.body.string()).chaptersList
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
// Pages
override fun pageListRequest(chapter: SChapter): Request {
val id = chapter.url.substringAfterLast("/")
return GET("$apiUrl/chapters/$id/images?page_size=1000", headers)
}
override fun pageListParse(response: Response) =
json.decodeFromString<MangaOwlToChapterPages>(response.body.string()).toPages()
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
// Filters
override fun getFilterList() = FilterList(
Filter.Header("Search query won't use filters"),
GenresFilter(genresList),
StatusFilter(),
SortFilter(),
)
companion object {
private const val MIRROR_PREF_KEY = "MIRROR"
private val MIRROR_PREF_ENTRIES get() = arrayOf(
"mangaowl.to",
"mangabuddy.to",
"mangafreak.to",
"toonily.to",
"manganato.so",
"mangakakalot.so", // Redirected from mangago.to
)
private val MIRROR_PREF_ENTRY_VALUES get() = arrayOf(
"mangaowl.to",
"mangabuddy.to",
"mangafreak.to",
"toonily.to",
"manganato.so",
"mangago.to", // API for domain mangakakalot.so
)
private val MIRROR_PREF_DEFAULT_VALUE get() = MIRROR_PREF_ENTRY_VALUES[0]
const val ONGOING = "ongoing"
const val COMPLETED = "completed"
}
}

View File

@ -0,0 +1,113 @@
package eu.kanade.tachiyomi.extension.en.mangaowlto
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class MangaOwlToStories(
private val next: String?,
private val results: List<MangaOwlToStory>,
) {
fun toMangasPage() = MangasPage(
mangas = results.map { it.toSManga() },
hasNextPage = !next.isNullOrEmpty(),
)
}
@Serializable
class MangaOwlToStory(
private val name: String,
private val slug: String,
@SerialName("status") private val titleStatus: String?, // ongoing & completed
@SerialName("thumbnail") private val thumbnailUrl: String,
@SerialName("al_name") private val altName: String?,
private val rating: Float?,
@SerialName("view_count") private val views: Int,
private val description: String?,
private val genres: List<MangaOwlToGenre> = emptyList(),
private val authors: List<MangaOwlToAuthor> = emptyList(),
private val chapters: List<MangaOwlToChapter> = emptyList(),
) {
private val fullDescription: String
get() = buildString {
append(description)
altName?.let { append("\n\n $it") }
append("\n\nRating: $rating")
append("\nViews: $views")
}
val chaptersList: List<SChapter>
get() = chapters.reversed().map { it.toSChapter(slug) }
fun toSManga(): SManga = SManga.create().apply {
title = name
author = authors.joinToString { it.name }
description = fullDescription.trim()
genre = genres.joinToString { it.name }
status = when (titleStatus) {
MangaOwlTo.ONGOING -> SManga.ONGOING
MangaOwlTo.COMPLETED -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = thumbnailUrl
url = slug
}
}
@Serializable
class MangaOwlToGenre(
val name: String,
)
@Serializable
class MangaOwlToAuthor(
val name: String,
)
@Serializable
class MangaOwlToChapter(
private val id: Int,
@SerialName("name") private val title: String,
@SerialName("created_at") private val createdAt: String,
) {
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
name = title
date_upload = parseDate()
url = "/reading/$slug/$id"
}
companion object {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US)
}
private fun parseDate(): Long = try {
dateFormat.parse(createdAt)!!.time
} catch (_: ParseException) {
0L
}
}
@Serializable
class MangaOwlToChapterPages(
@SerialName("results") private val pages: List<MangaOwlToPage> = emptyList(),
) {
fun toPages() =
pages.mapIndexed { idx, page ->
Page(
index = idx,
imageUrl = page.imageUrl,
)
}
}
@Serializable
class MangaOwlToPage(
@SerialName("image") val imageUrl: String,
)

View File

@ -0,0 +1,103 @@
package eu.kanade.tachiyomi.extension.en.mangaowlto
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class MangaOwlToFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
MangaOwlTo(
collection = "manga",
extraName = "Manga",
genresList = listOf(
Genre("Action", "13"),
Genre("Adult", "29"),
Genre("Adventure", "31"),
Genre("Comedy", "14"),
Genre("Cooking", "37"),
Genre("Doujinshi", "39"),
Genre("Drama", "17"),
Genre("Ecchi", "15"),
Genre("Fantasy", "16"),
Genre("Gender bender", "35"),
Genre("Harem", "27"),
Genre("Historical", "18"),
Genre("Horror", "28"),
Genre("Isekai", "40"),
Genre("Josei", "23"),
Genre("Manhua", "38"),
Genre("Manhwa", "36"),
Genre("Martial arts", "30"),
Genre("Mature", "20"),
Genre("Mecha", "33"),
Genre("Medical", "41"),
Genre("Mystery", "7"),
Genre("One shot", "1"),
Genre("Psychological", "8"),
Genre("Romance", "10"),
Genre("School life", "3"),
Genre("Sci fi", "19"),
Genre("Seinen", "25"),
Genre("Shoujo ai", "5"),
Genre("Shoujo", "12"),
Genre("Shounen ai", "4"),
Genre("Shounen", "9"),
Genre("Slice of life", "26"),
Genre("Smut", "32"),
Genre("Sports", "21"),
Genre("Supernatural", "24"),
Genre("Tragedy", "22"),
Genre("Webtoons", "34"),
Genre("Yaoi", "2"),
Genre("Yuri", "6"),
),
),
MangaOwlTo(
collection = "comic",
extraName = "Comic",
genresList = listOf(
Genre("215 Ink", "189"),
Genre("Ablaze", "98"),
Genre("Action Lab", "204"),
Genre("Aftershock Comics", "68"),
Genre("American Mythology", "130"),
Genre("Antartic Press", "261"),
Genre("Archie", "178"),
Genre("Aspen", "487"),
Genre("Avatar Press", "177"),
Genre("Black Mask", "107"),
Genre("Boom Studios", "65"),
Genre("Comics Experience", "159"),
Genre("Dark Horse", "92"),
Genre("DC Comics", "133"),
Genre("Devils Due", "290"),
Genre("Dynamite", "173"),
Genre("Europe Comics", "67"),
Genre("Heavy Metal", "55"),
Genre("Humanoids", "85"),
Genre("IDW", "110"),
Genre("Image Comics", "60"),
Genre("Inverse", "384"),
Genre("Lion Forge", "162"),
Genre("Mad Cave", "96"),
Genre("MAD", "485"),
Genre("Magnetic Press", "114"),
Genre("Marvel Comics", "45"),
Genre("One Shots &amp; TPBs", "136"),
Genre("Oni Press", "338"),
Genre("Rebellion", "50"),
Genre("Red 5", "88"),
Genre("SAF Comics", "378"),
Genre("Soleil", "156"),
Genre("Source Point Press", "57"),
Genre("Space Goat Productions", "421"),
Genre("Top Cow", "138"),
Genre("Top Shelf", "101"),
Genre("Upshot", "396"),
Genre("Valiant", "87"),
Genre("Vault", "360"),
Genre("Vertigo", "457"),
Genre("Zenescope", "119"),
),
),
)
}

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.extension.en.mangaowlto
import eu.kanade.tachiyomi.source.model.Filter
class Genre(val name: String, val uriPart: String)
class GenreCheckBox(name: String, val uriPart: String) : Filter.CheckBox(name)
class GenresFilter(genres: List<Genre>) :
Filter.Group<GenreCheckBox>("Genres", genres.map { GenreCheckBox(it.name, it.uriPart) })
class SortFilter : UriPartFilter(
"Sort by",
arrayOf(
Pair("Default", null),
Pair("Most view", "-view_count"),
Pair("Added", "created_at"),
Pair("Last update", "-modified_at"),
Pair("High rating", "rating"),
),
)
class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("Any", null),
Pair("Completed", MangaOwlTo.COMPLETED),
Pair("Ongoing", MangaOwlTo.ONGOING),
),
)
open class UriPartFilter(displayName: String, private val pairs: Array<Pair<String, String?>>) :
Filter.Select<String>(displayName, pairs.map { it.first }.toTypedArray()) {
fun toUriPart() = pairs[state].second
}

Some files were not shown because too many files have changed in this diff Show More