From c20c30c0ee505a27f8905e0b017f720bce393a57 Mon Sep 17 00:00:00 2001 From: Draken <131387159+dragonx943@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:31:05 +0700 Subject: [PATCH 1/5] Migrate to new parsers structure (#416) * Migrate to new parsers structure * chore: Update summary after compiling the library * Potential fix for pull request finding 'CodeQL / Unread local variable' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Unread local variable' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Unread local variable' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: YakaBot Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- build.gradle.kts | 12 +- gradle/libs.versions.toml | 9 +- .../build.gradle.kts | 0 .../kotatsu/parsers/ksp/ParserProcessor.kt | 133 +++++++- .../parsers/ksp/ParserProcessorProvider.kt | 0 .../org/koitharu/kotatsu/parsers/ksp/Utils.kt | 0 ...ols.ksp.processing.SymbolProcessorProvider | 0 settings.gradle.kts | 7 +- .../org/koitharu/kotatsu/parsers/Broken.kt | 14 - .../koitharu/kotatsu/parsers/ErrorMessages.kt | 18 -- .../kotatsu/parsers/InternalParsersApi.kt | 14 - .../kotatsu/parsers/MangaLoaderContext.kt | 78 ----- .../koitharu/kotatsu/parsers/MangaParser.kt | 88 ----- .../parsers/MangaParserAuthProvider.kt | 30 -- .../kotatsu/parsers/MangaSourceParser.kt | 28 -- .../koitharu/kotatsu/parsers/bitmap/Bitmap.kt | 9 - .../koitharu/kotatsu/parsers/bitmap/Rect.kt | 15 - .../parsers/compat/TachiyomiSourceAdapter.kt | 242 ++++++++++++++ .../kotatsu/parsers/config/ConfigKey.kt | 37 --- .../parsers/config/MangaSourceConfig.kt | 6 - .../parsers/core/AbstractMangaParser.kt | 107 ------- .../parsers/core/FlexibleMangaParser.kt | 96 ------ .../parsers/core/FlexiblePagedMangaParser.kt | 63 ---- .../parsers/core/MangaParserWrapper.kt | 79 ----- .../kotatsu/parsers/core/PagedMangaParser.kt | 57 ---- .../parsers/core/SinglePageMangaParser.kt | 24 -- .../exception/AuthRequiredException.kt | 13 - .../exception/ContentUnavailableException.kt | 3 - .../parsers/exception/GraphQLException.kt | 16 - .../parsers/exception/NotFoundException.kt | 9 - .../parsers/exception/ParseException.kt | 9 - .../exception/TooManyRequestExceptions.kt | 31 -- .../kotatsu/parsers/model/Constants.kt | 11 - .../kotatsu/parsers/model/ContentRating.kt | 7 - .../kotatsu/parsers/model/ContentType.kt | 36 --- .../kotatsu/parsers/model/Demographic.kt | 10 - .../koitharu/kotatsu/parsers/model/Favicon.kt | 28 -- .../kotatsu/parsers/model/Favicons.kt | 59 ---- .../koitharu/kotatsu/parsers/model/Manga.kt | 203 ------------ .../kotatsu/parsers/model/MangaChapter.kt | 65 ---- .../kotatsu/parsers/model/MangaListFilter.kt | 88 ----- .../model/MangaListFilterCapabilities.kt | 56 ---- .../parsers/model/MangaListFilterOptions.kt | 45 --- .../kotatsu/parsers/model/MangaPage.kt | 22 -- .../kotatsu/parsers/model/MangaSource.kt | 6 - .../kotatsu/parsers/model/MangaState.kt | 5 - .../kotatsu/parsers/model/MangaTag.kt | 16 - .../kotatsu/parsers/model/SortOrder.kt | 22 -- .../koitharu/kotatsu/parsers/model/WordSet.kt | 11 - .../parsers/model/search/MangaSearchQuery.kt | 92 ------ .../search/MangaSearchQueryCapabilities.kt | 50 --- .../parsers/model/search/QueryCriteria.kt | 108 ------- .../parsers/model/search/SearchCapability.kt | 36 --- .../parsers/model/search/SearchableField.kt | 24 -- .../parsers/network/CloudFlareHelper.kt | 58 ---- .../kotatsu/parsers/network/CommonHeaders.kt | 77 ----- .../parsers/network/OkHttpWebClient.kt | 133 -------- .../parsers/network/RateLimitHelper.kt | 41 --- .../kotatsu/parsers/network/UserAgents.kt | 27 -- .../kotatsu/parsers/network/WebClient.kt | 117 ------- .../parsers/network/utils/RateLimiter.kt | 46 --- .../kotatsu/parsers/site/all/ComicKLive.kt | 7 +- .../parsers/site/all/ExHentaiParser.kt | 5 +- .../parsers/site/all/HitomiLaParser.kt | 9 +- .../kotatsu/parsers/site/all/ImHentai.kt | 5 +- .../kotatsu/parsers/site/all/Koharu.kt | 16 +- .../parsers/site/all/LineWebtoonsParser.kt | 5 +- .../parsers/site/all/MangaFireParser.kt | 7 +- .../kotatsu/parsers/site/all/MangaPark.kt | 5 +- .../parsers/site/all/MangaPlusParser.kt | 5 +- .../parsers/site/all/MangaReaderToParser.kt | 5 +- .../kotatsu/parsers/site/all/Manhwa210.kt | 7 +- .../kotatsu/parsers/site/all/Misskon.kt | 5 +- .../kotatsu/parsers/site/all/Multporn.kt | 5 +- .../parsers/site/all/MyReadingManga.kt | 5 +- .../parsers/site/all/NineMangaParser.kt | 7 +- .../site/all/NineNineNineHentaiParser.kt | 5 +- .../parsers/site/all/WebtoonsParser.kt | 5 +- .../kotatsu/parsers/site/all/WeebDex.kt | 10 +- .../animebootstrap/AnimeBootstrapParser.kt | 5 +- .../parsers/site/animebootstrap/fr/PapScan.kt | 5 +- .../kotatsu/parsers/site/ar/AzoraMoon.kt | 5 +- .../kotatsu/parsers/site/ar/FlixScans.kt | 5 +- .../kotatsu/parsers/site/ar/HentaMan.kt | 5 +- .../kotatsu/parsers/site/ar/MangaStorm.kt | 5 +- .../kotatsu/parsers/site/ar/TeamXNovel.kt | 5 +- .../kotatsu/parsers/site/be/AnibelParser.kt | 11 +- .../parsers/site/cupfox/CupFoxParser.kt | 5 +- .../parsers/site/en/AsuraScansParser.kt | 10 +- .../kotatsu/parsers/site/en/Atsumaru.kt | 5 +- .../kotatsu/parsers/site/en/BatCave.kt | 5 +- .../kotatsu/parsers/site/en/BeeToon.kt | 5 +- .../kotatsu/parsers/site/en/ComicExtra.kt | 5 +- .../kotatsu/parsers/site/en/DemonicScans.kt | 5 +- .../kotatsu/parsers/site/en/DynastyScans.kt | 5 +- .../kotatsu/parsers/site/en/FanFoxParser.kt | 5 +- .../kotatsu/parsers/site/en/FlameComics.kt | 5 +- .../kotatsu/parsers/site/en/FlixScansOrg.kt | 3 +- .../kotatsu/parsers/site/en/Hentalk.kt | 12 +- .../kotatsu/parsers/site/en/HeyToonParser.kt | 5 +- .../kotatsu/parsers/site/en/KdtScans.kt | 5 +- .../kotatsu/parsers/site/en/MangaGeko.kt | 5 +- .../parsers/site/en/MangaHomeParser.kt | 5 +- .../kotatsu/parsers/site/en/MangaKatana.kt | 5 +- .../kotatsu/parsers/site/en/MangaKawaiiEn.kt | 5 +- .../kotatsu/parsers/site/en/MangaPill.kt | 7 +- .../parsers/site/en/MangaTownParser.kt | 7 +- .../kotatsu/parsers/site/en/Mangago.kt | 5 +- .../kotatsu/parsers/site/en/Mangaowl.kt | 5 +- .../kotatsu/parsers/site/en/Manhwa18Com.kt | 9 +- .../kotatsu/parsers/site/en/Manhwa18Parser.kt | 5 +- .../kotatsu/parsers/site/en/ManhwasMen.kt | 5 +- .../kotatsu/parsers/site/en/MyComicList.kt | 5 +- .../kotatsu/parsers/site/en/Po2Scans.kt | 5 +- .../kotatsu/parsers/site/en/Pururin.kt | 5 +- .../kotatsu/parsers/site/en/RoliaScan.kt | 3 +- .../kotatsu/parsers/site/en/VioletScans.kt | 5 +- .../kotatsu/parsers/site/en/VyManga.kt | 9 +- .../kotatsu/parsers/site/en/WeebCentral.kt | 7 +- .../kotatsu/parsers/site/en/comix/Comix.kt | 5 +- .../parsers/site/es/HouseMangasParser.kt | 1 + .../kotatsu/parsers/site/es/InMangaParser.kt | 3 +- .../parsers/site/es/TuMangaOnlineParser.kt | 5 +- .../kotatsu/parsers/site/es/ZonaTmoParser.kt | 3 +- .../parsers/site/fmreader/FmreaderParser.kt | 10 +- .../parsers/site/foolslide/FoolSlideParser.kt | 5 +- .../kotatsu/parsers/site/fr/AnimeSama.kt | 3 +- .../kotatsu/parsers/site/fr/AstralManga.kt | 23 +- .../parsers/site/fr/BentomangaParser.kt | 5 +- .../kotatsu/parsers/site/fr/BigSolo.kt | 8 +- .../kotatsu/parsers/site/fr/FuryoSociety.kt | 3 +- .../kotatsu/parsers/site/fr/InovaScanManga.kt | 3 +- .../parsers/site/fr/LegacyScansParser.kt | 5 +- .../kotatsu/parsers/site/fr/LireScan.kt | 5 +- .../kotatsu/parsers/site/fr/LugnicaScans.kt | 3 +- .../kotatsu/parsers/site/fr/MangaKawaii.kt | 5 +- .../kotatsu/parsers/site/fr/MangaMana.kt | 5 +- .../kotatsu/parsers/site/fr/MangaMoins.kt | 3 +- .../kotatsu/parsers/site/fr/OrtegaScans.kt | 1 + .../parsers/site/fr/PhenixscansParser.kt | 5 +- .../kotatsu/parsers/site/fr/PoseidonScans.kt | 9 +- .../kotatsu/parsers/site/fr/PunkRecordz.kt | 4 +- .../kotatsu/parsers/site/fr/ScansMangasMe.kt | 5 +- .../kotatsu/parsers/site/fr/ScantradUnion.kt | 5 +- .../site/fuzzydoodle/FuzzyDoodleParser.kt | 5 +- .../parsers/site/gallery/GalleryParser.kt | 3 +- .../site/galleryadults/GalleryAdultsParser.kt | 9 +- .../parsers/site/galleryadults/all/Hentai3.kt | 19 +- .../site/galleryadults/all/HentaiEnvy.kt | 17 +- .../site/galleryadults/all/HentaiEra.kt | 21 +- .../site/galleryadults/all/HentaiForce.kt | 19 +- .../site/galleryadults/all/HentaiFox.kt | 19 +- .../site/galleryadults/all/NHentaiParser.kt | 5 +- .../site/galleryadults/all/NHentaiToParser.kt | 7 +- .../galleryadults/all/NHentaiXxxParser.kt | 5 +- .../site/galleryadults/en/HentaiNexus.kt | 10 +- .../site/galleryadults/en/HentaiRead.kt | 17 +- .../parsers/site/gattsu/GattsuParser.kt | 5 +- .../kotatsu/parsers/site/guya/GuyaParser.kt | 5 +- .../kotatsu/parsers/site/heancms/HeanCms.kt | 7 +- .../parsers/site/heancms/en/TempleScan.kt | 5 +- .../parsers/site/heancms/fr/PerfScan.kt | 3 +- .../parsers/site/heancmsalt/HeanCmsAlt.kt | 3 +- .../parsers/site/hotcomics/HotComicsParser.kt | 5 +- .../parsers/site/id/DoujinDesuParser.kt | 7 +- .../kotatsu/parsers/site/id/HentaiCrot.kt | 5 +- .../kotatsu/parsers/site/id/Komikcast.kt | 5 +- .../kotatsu/parsers/site/id/Kumapage.kt | 5 +- .../kotatsu/parsers/site/id/PixHentai.kt | 5 +- .../kotatsu/parsers/site/id/Shinigami.kt | 10 +- .../kotatsu/parsers/site/id/ThemangaParser.kt | 3 +- .../parsers/site/id/WestmangaParser.kt | 7 +- .../kotatsu/parsers/site/iken/IkenParser.kt | 5 +- .../koitharu/kotatsu/parsers/site/ja/Klz9.kt | 5 +- .../parsers/site/ja/NicovideoSeigaParser.kt | 23 +- .../parsers/site/keyoapp/KeyoappParser.kt | 8 +- .../parsers/site/likemanga/LikeMangaParser.kt | 5 +- .../parsers/site/liliana/LilianaParser.kt | 5 +- .../parsers/site/madara/MadaraParser.kt | 20 +- .../parsers/site/madara/all/Manga18Fx.kt | 5 +- .../parsers/site/madara/all/Manhwa18Cc.kt | 5 +- .../parsers/site/madara/ar/ArabsHentai.kt | 5 +- .../parsers/site/madara/ar/RocksManga.kt | 5 +- .../parsers/site/madara/en/AdultWebtoon.kt | 5 +- .../parsers/site/madara/en/Hentai4Free.kt | 5 +- .../parsers/site/madara/en/HentaiManga.kt | 5 +- .../parsers/site/madara/en/HentaiWebtoon.kt | 5 +- .../parsers/site/madara/en/IsekaiScan.kt | 5 +- .../site/madara/en/IsekaiScanEuParser.kt | 5 +- .../parsers/site/madara/en/MangaDass.kt | 5 +- .../parsers/site/madara/en/MangaDna.kt | 5 +- .../parsers/site/madara/en/MangaPure.kt | 5 +- .../kotatsu/parsers/site/madara/en/Manhwaz.kt | 5 +- .../parsers/site/madara/en/ManyToon.kt | 5 +- .../parsers/site/madara/es/DoujinHentaiNet.kt | 5 +- .../parsers/site/madara/es/TmoManga.kt | 5 +- .../parsers/site/madara/fr/RaijinScans.kt | 3 +- .../parsers/site/madara/id/ManhwaHub.kt | 5 +- .../parsers/site/madara/vi/HentaiCube.kt | 10 +- .../parsers/site/madara/vi/HentaiVnPlus.kt | 10 +- .../parsers/site/madara/vi/Saytruyenhay.kt | 5 +- .../parsers/site/madtheme/MadthemeParser.kt | 5 +- .../parsers/site/madtheme/en/ManhuaScan.kt | 8 +- .../parsers/site/manga18/Manga18Parser.kt | 7 +- .../parsers/site/mangabox/MangaboxParser.kt | 7 +- .../site/mangadventure/MangAdventureParser.kt | 5 +- .../site/mangareader/MangaReaderParser.kt | 5 +- .../parsers/site/mangareader/ar/Normoyun.kt | 5 +- .../parsers/site/mangareader/en/RizzComic.kt | 7 +- .../parsers/site/mangareader/en/Zahard.kt | 5 +- .../site/mangareader/es/CerberuSeries.kt | 5 +- .../site/mangareader/es/HentaiReader.kt | 5 +- .../site/mangareader/es/LectorHentai.kt | 5 +- .../parsers/site/mangareader/es/MangaTv.kt | 5 +- .../parsers/site/mangareader/es/TenkaiScan.kt | 3 +- .../parsers/site/mangareader/es/TuManhwas.kt | 5 +- .../site/mangareader/fr/RevolutionScantrad.kt | 3 +- .../parsers/site/mangareader/id/KomikIndo.kt | 5 +- .../parsers/site/mangareader/id/KomikSan.kt | 5 +- .../site/mangaworld/MangaWorldParser.kt | 10 +- .../kotatsu/parsers/site/manhwaz/ManhwaZ.kt | 5 +- .../parsers/site/mmrcms/MmrcmsParser.kt | 7 +- .../kotatsu/parsers/site/natsu/NatsuParser.kt | 10 +- .../parsers/site/nepnep/NepnepParser.kt | 7 +- .../otakusanctuary/OtakuSanctuaryParser.kt | 5 +- .../site/pizzareader/PizzaReaderParser.kt | 5 +- .../kotatsu/parsers/site/pt/AnimeXNovel.kt | 3 +- .../kotatsu/parsers/site/pt/BrMangas.kt | 7 +- .../kotatsu/parsers/site/pt/InstaHentai.kt | 5 +- .../kotatsu/parsers/site/pt/LerManga.kt | 3 +- .../kotatsu/parsers/site/pt/LerMangaOnline.kt | 5 +- .../parsers/site/pt/LuratoonScansParser.kt | 3 +- .../kotatsu/parsers/site/pt/MangaOnline.kt | 5 +- .../kotatsu/parsers/site/pt/Manhastro.kt | 5 +- .../kotatsu/parsers/site/pt/MediocreToons.kt | 8 +- .../kotatsu/parsers/site/pt/MuitoHentai.kt | 5 +- .../kotatsu/parsers/site/pt/SssScanlator.kt | 5 +- .../kotatsu/parsers/site/pt/SussyScan.kt | 6 +- .../kotatsu/parsers/site/pt/TaiyoParser.kt | 2 +- .../kotatsu/parsers/site/pt/YugenMangas.kt | 5 +- .../kotatsu/parsers/site/ru/AComics.kt | 5 +- .../kotatsu/parsers/site/ru/ComXParser.kt | 5 +- .../kotatsu/parsers/site/ru/DesuMeParser.kt | 7 +- .../kotatsu/parsers/site/ru/MangaWtfParser.kt | 5 +- .../kotatsu/parsers/site/ru/NudeMoonParser.kt | 5 +- .../kotatsu/parsers/site/ru/RemangaParser.kt | 7 +- .../kotatsu/parsers/site/ru/ZenMangaParser.kt | 10 +- .../parsers/site/ru/grouple/GroupleParser.kt | 4 +- .../parsers/site/ru/multichan/ChanParser.kt | 6 +- .../site/ru/multichan/HenChanParser.kt | 47 +-- .../parsers/site/ru/rulib/LibSocialParser.kt | 5 +- .../kotatsu/parsers/site/scan/ScanParser.kt | 11 +- .../kotatsu/parsers/site/sinmh/SinmhParser.kt | 5 +- .../kotatsu/parsers/site/tr/ElderManga.kt | 5 +- .../kotatsu/parsers/site/tr/MangaAy.kt | 5 +- .../kotatsu/parsers/site/tr/SadScans.kt | 5 +- .../kotatsu/parsers/site/tr/TenshiManga.kt | 5 +- .../kotatsu/parsers/site/tr/TrWebtoon.kt | 7 +- .../kotatsu/parsers/site/tr/UzayManga.kt | 5 +- .../parsers/site/uk/HentaiUkrParser.kt | 13 +- .../parsers/site/uk/HoneyMangaParser.kt | 7 +- .../parsers/site/uk/MangaInUaParser.kt | 5 +- .../kotatsu/parsers/site/vi/BFANGTeam.kt | 10 +- .../parsers/site/vi/BlogTruyenParser.kt | 5 +- .../kotatsu/parsers/site/vi/BlogTruyenVN.kt | 5 +- .../kotatsu/parsers/site/vi/CMangaParser.kt | 5 +- .../parsers/site/vi/CuuTruyenParser.kt | 7 +- .../kotatsu/parsers/site/vi/DamCoNuong.kt | 5 +- .../kotatsu/parsers/site/vi/DuaLeoTruyen.kt | 5 +- .../kotatsu/parsers/site/vi/GocTruyenTranh.kt | 3 +- .../parsers/site/vi/GocTruyenTranhVui.kt | 5 +- .../kotatsu/parsers/site/vi/HangTruyen.kt | 5 +- .../kotatsu/parsers/site/vi/Hentai18VN.kt | 5 +- .../kotatsu/parsers/site/vi/HentaiVNParser.kt | 5 +- .../kotatsu/parsers/site/vi/HentaiVnBuzz.kt | 5 +- .../kotatsu/parsers/site/vi/HentaiVnSU.kt | 5 +- .../kotatsu/parsers/site/vi/KuroNeko.kt | 10 +- .../kotatsu/parsers/site/vi/LangGeekParser.kt | 5 +- .../kotatsu/parsers/site/vi/LxManga.kt | 7 +- .../kotatsu/parsers/site/vi/MimiHentai.kt | 14 +- .../kotatsu/parsers/site/vi/NhentaiWorld.kt | 10 +- .../kotatsu/parsers/site/vi/OTruyenParser.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenGG.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenHentai18.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenHentaiVN.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenQQ.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenTranh3Q.kt | 5 +- .../kotatsu/parsers/site/vi/TuSachXinhXinh.kt | 7 +- .../kotatsu/parsers/site/vi/VcomycsParser.kt | 5 +- .../parsers/site/vi/YuriGardenParser.kt | 10 +- .../kotatsu/parsers/site/vi/YurinekoParser.kt | 5 +- .../kotatsu/parsers/site/vmp/VmpParser.kt | 5 +- .../parsers/site/wpcomics/WpComicsParser.kt | 5 +- .../parsers/site/wpcomics/en/XoxoComics.kt | 5 +- .../parsers/site/wpcomics/ja/MangaRaw.kt | 5 +- .../parsers/site/wpcomics/vi/DocTruyen3Q.kt | 5 +- .../parsers/site/wpcomics/vi/MeHentaiVN.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyen1975.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenFE.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenHE.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenLL.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenSSR.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenUU.kt | 5 +- .../parsers/site/wpcomics/vi/TopTruyen.kt | 5 +- .../site/zeistmanga/ZeistMangaParser.kt | 5 +- .../kotatsu/parsers/site/zh/Baozimh.kt | 5 +- .../kotatsu/parsers/site/zh/KomiicParser.kt | 7 +- .../parsers/site/zh/ManhuaguiParser.kt | 5 +- .../parsers/site/zmanga/ZMangaParser.kt | 9 +- .../koitharu/kotatsu/parsers/util/Assert.kt | 8 - .../kotatsu/parsers/util/CSSBackground.kt | 52 --- .../koitharu/kotatsu/parsers/util/Chapters.kt | 75 ----- .../kotatsu/parsers/util/Collection.kt | 101 ------ .../parsers/util/ContinuationCallCallback.kt | 34 -- .../kotatsu/parsers/util/CookieJar.kt | 48 --- .../kotatsu/parsers/util/Coroutines.kt | 35 -- .../kotatsu/parsers/util/CryptoAES.kt | 127 -------- .../org/koitharu/kotatsu/parsers/util/Enum.kt | 13 - .../kotatsu/parsers/util/FaviconParser.kt | 92 ------ .../koitharu/kotatsu/parsers/util/Jsoup.kt | 225 ------------- .../kotatsu/parsers/util/LinkResolver.kt | 11 +- .../kotatsu/parsers/util/MangaParserEnv.kt | 96 ------ .../kotatsu/parsers/util/MangaParserExt.kt | 20 ++ .../kotatsu/parsers/util/MangaParsersUtils.kt | 18 -- .../koitharu/kotatsu/parsers/util/Number.kt | 85 ----- .../koitharu/kotatsu/parsers/util/OkHttp.kt | 55 ---- .../kotatsu/parsers/util/Paginator.kt | 25 -- .../koitharu/kotatsu/parsers/util/Parse.kt | 101 ------ .../kotatsu/parsers/util/RateLimit.kt | 81 ----- .../parsers/util/RelatedMangaFinder.kt | 57 ---- .../koitharu/kotatsu/parsers/util/Result.kt | 41 --- .../parsers/util/SearchQueryConverter.kt | 250 --------------- .../koitharu/kotatsu/parsers/util/String.kt | 237 -------------- .../kotatsu/parsers/util/WebViewHelper.kt | 2 +- .../kotatsu/parsers/util/json/EscapeUtils.kt | 38 --- .../util/json/JSONArrayTypedIterator.kt | 33 -- .../util/json/JSONArrayTypedListWrapper.kt | 53 ---- .../json/JSONObjectTypedIterableWrapper.kt | 24 -- .../kotatsu/parsers/util/json/JsonExt.kt | 167 ---------- .../kotatsu/parsers/util/json/NextJs.kt | 220 ------------- .../util/suspendlazy/SoftSuspendLazyImpl.kt | 41 --- .../parsers/util/suspendlazy/SuspendLazy.kt | 39 --- .../util/suspendlazy/SuspendLazyImpl.kt | 47 --- .../kotatsu/parsers/AuthCheckExtension.kt | 31 -- .../kotatsu/parsers/CloudFlareInterceptor.kt | 22 -- .../parsers/CloudFlareProtectedException.kt | 9 - .../parsers/CommonHeadersInterceptor.kt | 40 --- .../kotatsu/parsers/InMemoryCookieJar.kt | 55 ---- .../kotatsu/parsers/MangaLoaderContextMock.kt | 107 ------- .../kotatsu/parsers/MangaParserTest.kt | 300 ------------------ .../koitharu/kotatsu/parsers/MangaSources.kt | 8 - .../kotatsu/parsers/SourceConfigMock.kt | 9 - .../model/search/MangaSearchQueryTest.kt | 72 ----- .../parsers/util/IntentFilterGenerator.kt | 37 --- .../kotatsu/parsers/util/LinkResolverTest.kt | 29 -- .../ListFilterToSearchQueryConverterTest.kt | 77 ----- .../kotatsu/parsers/util/PaginatorTest.kt | 42 --- .../SearchQueryToListFilterConverterTest.kt | 94 ------ .../kotatsu/test_util/BitmapTestImpl.kt | 35 -- .../org/koitharu/kotatsu/test_util/Util.kt | 79 ----- src/test/resources/junit-platform.properties | 2 - 361 files changed, 1341 insertions(+), 6571 deletions(-) rename {kotatsu-parsers-ksp => plugins-ksp}/build.gradle.kts (100%) rename {kotatsu-parsers-ksp => plugins-ksp}/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt (52%) rename {kotatsu-parsers-ksp => plugins-ksp}/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt (100%) rename {kotatsu-parsers-ksp => plugins-ksp}/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt (100%) rename {kotatsu-parsers-ksp => plugins-ksp}/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider (100%) delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt delete mode 100644 src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt delete mode 100644 src/test/resources/junit-platform.properties diff --git a/build.gradle.kts b/build.gradle.kts index 37f640c47..c18cdd566 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,7 @@ plugins { `maven-publish` alias(libs.plugins.kotlin.jvm) alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.serialization) } group = "org.koitharu" @@ -20,6 +21,9 @@ ksp { tasks.jar { archiveFileName.set("kotatsu-parsers.jar") + exclude("android/**") + exclude("androidx/annotation/**") + exclude("androidx/preference/**") } tasks.withType().configureEach { @@ -36,7 +40,6 @@ tasks.withType().configureEach kotlin { jvmToolchain(11) - explicitApiWarning() sourceSets["main"].kotlin.srcDirs("build/generated/ksp/main/kotlin") } @@ -50,13 +53,18 @@ publishing { dependencies { implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) implementation(libs.okhttp) implementation(libs.okio) implementation(libs.json) implementation(libs.androidx.collection) + + api(libs.core.parsers) api(libs.jsoup) - ksp(project(":kotatsu-parsers-ksp")) + compileOnly(libs.android.stubs) + + ksp(project(":plugins-ksp")) testImplementation(libs.junit.api) testImplementation(libs.junit.engine) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77ce34a61..4fa61a4f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,7 @@ [versions] +androidx-collection = "1.6.0" +android-stubs = "4.1.1.4" +core = "b62a0681d7" kotlin = "2.2.10" ksp = "2.2.10-2.0.2" coroutines = "1.10.2" @@ -6,22 +9,26 @@ junit = "5.14.3" okhttp = "5.3.2" okio = "3.17.0" json = "20251224" -androidx-collection = "1.6.0" # Keep Android 5, 6 compatible (with SDK 21 - 23) #noinspection NewerVersionAvailable jsoup = "1.17.2" quickjs = "1.1.0" korte = "4.0.10" +serialization = "1.8.0" simplexml = "2.7.1" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } [libraries] +android-stubs = { module = "com.google.android:android", version.ref = "android-stubs" } +core-parsers = { module = "com.github.UsagiApp:core-parsers", version.ref = "core"} ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } diff --git a/kotatsu-parsers-ksp/build.gradle.kts b/plugins-ksp/build.gradle.kts similarity index 100% rename from kotatsu-parsers-ksp/build.gradle.kts rename to plugins-ksp/build.gradle.kts diff --git a/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt b/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt similarity index 52% rename from kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt rename to plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt index c44795ca1..bacd2079c 100644 --- a/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt +++ b/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt @@ -20,10 +20,14 @@ class ParserProcessor( private val sourceNamePattern = Regex("[A-Z_][A-Z0-9_]{3,}") override fun process(resolver: Resolver): List { - val symbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.MangaSourceParser") - val ret = symbols.filterNot { it.validate() }.toList() - if (!symbols.iterator().hasNext()) { - return ret + val kotatsuSymbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.MangaSourceParser") + val tachiyomiSymbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.TachiyomiSource") + + val kotatsuRet = kotatsuSymbols.filterNot { it.validate() }.toList() + val tachiyomiRet = tachiyomiSymbols.filterNot { it.validate() }.toList() + + if (!kotatsuSymbols.iterator().hasNext() && !tachiyomiSymbols.iterator().hasNext()) { + return kotatsuRet + tachiyomiRet } val dependencies = Dependencies.ALL_FILES val factoryFile = @@ -50,17 +54,18 @@ class ParserProcessor( } val totalCount = sourcesFile?.writer().use { sourcesWriter -> factoryFile?.writer().use { factoryWriter -> - writeContent(sourcesWriter, factoryWriter, symbols) + writeContent(sourcesWriter, factoryWriter, kotatsuSymbols, tachiyomiSymbols) } } writeSummary(totalCount) - return ret + return kotatsuRet + tachiyomiRet } private fun writeContent( sourcesWriter: Writer?, factoryWriter: Writer?, - symbols: Sequence, + kotatsuSymbols: Sequence, + tachiyomiSymbols: Sequence, ): Int { if (sourcesWriter == null && factoryWriter == null) { return 0 @@ -71,6 +76,7 @@ class ParserProcessor( import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.core.MangaParserWrapper + import org.koitharu.kotatsu.parsers.compat.TachiyomiSourceAdapter internal fun MangaParserSource.newParser(context: MangaLoaderContext): MangaParser = when (this) { @@ -81,19 +87,27 @@ class ParserProcessor( package org.koitharu.kotatsu.parsers.model public enum class MangaParserSource( - public val title: String, - public val locale: String, - public val contentType: ContentType, - public val isBroken: Boolean, + override val title: String, + override val locale: String, + override val contentType: ContentType, + override val isBroken: Boolean, ): MangaSource { """.trimIndent(), ) - val visitor = ParserVisitor(sourcesWriter, factoryWriter) - val totalCount = symbols + // Process native Kotatsu parsers + val kotatsuVisitor = ParserVisitor(sourcesWriter, factoryWriter) + val kotatsuCount = kotatsuSymbols + .filter { it is KSClassDeclaration && it.validate() } + .onEach { it.accept(kotatsuVisitor, Unit) } + .count() + + // Process Tachiyomi source bridges + val tachiyomiVisitor = TachiyomiVisitor(sourcesWriter, factoryWriter, kotatsuVisitor.titles) + val tachiyomiCount = tachiyomiSymbols .filter { it is KSClassDeclaration && it.validate() } - .onEach { it.accept(visitor, Unit) } + .onEach { it.accept(tachiyomiVisitor, Unit) } .count() factoryWriter?.write( @@ -112,7 +126,7 @@ class ParserProcessor( } """.trimIndent(), ) - return totalCount + return kotatsuCount + tachiyomiCount } private fun writeSummary(totalCount: Int) { @@ -124,7 +138,7 @@ class ParserProcessor( private val sourcesWriter: Writer?, private val factoryWriter: Writer?, ) : KSVisitorVoid() { - private val titles = HashMap() + val titles = HashMap() override fun visitClassDeclaration( classDeclaration: KSClassDeclaration, @@ -183,4 +197,91 @@ class ParserProcessor( ) } } + + /** + * Visitor for @TachiyomiSource annotations. + * Generates factory entries that instantiate the Tachiyomi HttpSource and wrap it with TachiyomiSourceAdapter. + */ + private inner class TachiyomiVisitor( + private val sourcesWriter: Writer?, + private val factoryWriter: Writer?, + private val titles: HashMap, + ) : KSVisitorVoid() { + + override fun visitClassDeclaration( + classDeclaration: KSClassDeclaration, + data: Unit, + ) { + val annotation = classDeclaration.annotations.single { it.shortName.asString() == "TachiyomiSource" } + + // Get annotation arguments with defaults + val nameArg = annotation.arguments.find { it.name?.asString() == "name" }?.value as? String ?: "" + val titleArg = annotation.arguments.find { it.name?.asString() == "title" }?.value as? String ?: "" + val localeArg = annotation.arguments.find { it.name?.asString() == "locale" }?.value as? String ?: "" + val typeArg = annotation.arguments.find { it.name?.asString() == "type" }?.value?.toString() + val type = if (typeArg.isNullOrBlank() || typeArg == "null") "ContentType.MANGA" else typeArg + + // Auto-detect values from class if not provided + val className = classDeclaration.simpleName.asString() + val qualifiedClassName = checkNotNull(classDeclaration.qualifiedName?.asString()) { + "Class qualified name is null" + } + + // Generate source name from class name (UPPER_CASE) + val sourceName = nameArg.takeIf { it.isNotBlank() } + ?: className.uppercase().replace(Regex("[^A-Z0-9_]"), "_").let { + // Ensure it starts with a letter + if (it.firstOrNull()?.isLetter() == true) it else "SOURCE_$it" + } + + // Use provided title or class name + val sourceTitle = titleArg.takeIf { it.isNotBlank() } ?: className + + // Auto-detect locale from package name if not provided + val locale = localeArg.takeIf { it.isNotBlank() } + ?: detectLocaleFromPackage(qualifiedClassName) + + val localeString = "\"$locale\"" + val localeObj = if (locale.isEmpty()) null else Locale(locale) + val localeTitle = localeObj?.getDisplayLanguage(localeObj) + + if (localeObj != null && localeObj !in availableLocales) { + logger.error("Tachiyomi source $sourceName has wrong locale: $localeTitle") + } + + if (!sourceNamePattern.matches(sourceName)) { + logger.error("Tachiyomi source name must be uppercase: $sourceName") + } + + val prevTitleClass = titles.put(sourceTitle, qualifiedClassName) + if (prevTitleClass != null) { + logger.warn("Source title duplication: \"$sourceTitle\" is assigned to both $prevTitleClass and $qualifiedClassName") + } + + // Factory: wrap the Tachiyomi source with TachiyomiSourceAdapter + factoryWriter?.write( + "\tMangaParserSource.$sourceName -> TachiyomiSourceAdapter(context, MangaParserSource.$sourceName, $qualifiedClassName())\n" + ) + + // Enum entry + val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty() + sourcesWriter?.write( + "\t$sourceName(\"$sourceTitle\", $localeString$localeComment, $type, false),\n", + ) + } + + private fun detectLocaleFromPackage(qualifiedName: String): String { + // Common locale patterns in package names + val localePatterns = mapOf( + ".vi." to "vi", ".en." to "en", ".ja." to "ja", ".ko." to "ko", + ".zh." to "zh", ".es." to "es", ".fr." to "fr", ".de." to "de", + ".it." to "it", ".pt." to "pt", ".ru." to "ru", ".ar." to "ar", + ".th." to "th", ".id." to "id", ".tr." to "tr", ".pl." to "pl", + ".all." to "", ".multi." to "" + ) + + return localePatterns.entries.find { qualifiedName.contains(it.key) }?.value + ?: "" + } + } } diff --git a/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt b/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt similarity index 100% rename from kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt rename to plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt diff --git a/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt b/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt similarity index 100% rename from kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt rename to plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt diff --git a/kotatsu-parsers-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/plugins-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider similarity index 100% rename from kotatsu-parsers-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider rename to plugins-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider diff --git a/settings.gradle.kts b/settings.gradle.kts index 834fbf540..4cd34da13 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,8 +11,13 @@ dependencyResolutionManagement { google() mavenCentral() gradlePluginPortal() + maven { + url = uri( + "https://jitpack.io" + ) + } } } rootProject.name = "kotatsu-parsers" -include("kotatsu-parsers-ksp") +include("plugins-ksp") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt deleted file mode 100644 index 74d03077c..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.koitharu.kotatsu.parsers - -/** - * Annotate [MangaParser] implementation to mark this parser as broken instead of removing it - */ -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.SOURCE) -internal annotation class Broken( - - /** - * Reason why this parser is broken - */ - val message: String = "", -) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt deleted file mode 100644 index d20006eb7..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.koitharu.kotatsu.parsers - -public object ErrorMessages { - - public const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED: String = "Multiple states are not supported by this source" - public const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED: String = "Multiple genres are not supported by this source" - public const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED: String = - "Multiple Content ratings are not supported by this source" - public const val FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED: String = - "Multiple Content types are not supported by this source" - public const val FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED: String = - "Multiple Demographics are not supported by this source" - public const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED: String = - "Filtering by both genres and locale is not supported by this source" - public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String = - "Filtering by both genres and states is not supported by this source" - public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source" -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt deleted file mode 100644 index b3efbd678..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.koitharu.kotatsu.parsers - -/** - * This marker distinguishes the internal API and is used to opt-in for that feature when parsers developing. - * - * Any usage of a declaration annotated with `@InternalParsersApi` must be accepted either by - * annotating that usage with the [OptIn] annotation, e.g. `@OptIn(InternalParsersApi::class)`, - * or by using the compiler argument `-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi`. - */ -@Retention(AnnotationRetention.BINARY) -@SinceKotlin("1.3") -@RequiresOptIn -@MustBeDocumented -public annotation class InternalParsersApi diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt deleted file mode 100644 index 438cb1a59..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import okhttp3.CookieJar -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.OkHttpClient -import okhttp3.Response -import org.koitharu.kotatsu.parsers.bitmap.Bitmap -import org.koitharu.kotatsu.parsers.config.MangaSourceConfig -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.LinkResolver -import java.util.* - -public abstract class MangaLoaderContext { - - public abstract val httpClient: OkHttpClient - - public abstract val cookieJar: CookieJar - - public fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this) - - public fun newLinkResolver(link: HttpUrl): LinkResolver = LinkResolver(this, link) - - public fun newLinkResolver(link: String): LinkResolver = newLinkResolver(link.toHttpUrl()) - - public open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data) - - public open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data) - - public open fun getPreferredLocales(): List = listOf(Locale.getDefault()) - - /** - * Execute JavaScript code and return result - * @param script JavaScript source code - * @return execution result as string, may be null - */ - @Deprecated("Provide a base url") - public abstract suspend fun evaluateJs(script: String): String? - - /** - * Execute JavaScript code and return result - * @param script JavaScript source code - * @param baseUrl url of page script will be executed in context of - * @return execution result as string, may be null - */ - public abstract suspend fun evaluateJs(baseUrl: String, script: String): String? - - /** - * Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization) - */ - public open fun requestBrowserAction(parser: MangaParser, url: String): Nothing { - throw UnsupportedOperationException("Browser is not available") - } - - public abstract fun getConfig(source: MangaSource): MangaSourceConfig - - public abstract fun getDefaultUserAgent(): String - - /** - * Helper function to be used in an interceptor - * to descramble images - * @param response Image response - * @param redraw lambda function to implement descrambling logic - */ - public abstract fun redrawImageResponse( - response: Response, - redraw: (image: Bitmap) -> Bitmap, - ): Response - - /** - * create a new empty Bitmap with given dimensions - */ - public abstract fun createBitmap( - width: Int, - height: Int, - ): Bitmap -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt deleted file mode 100644 index 24fff1db4..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt +++ /dev/null @@ -1,88 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers - -import okhttp3.Headers -import okhttp3.HttpUrl -import okhttp3.Interceptor -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.config.MangaSourceConfig -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities -import org.koitharu.kotatsu.parsers.util.LinkResolver -import java.util.* - -public interface MangaParser : Interceptor { - - public val source: MangaParserSource - - /** - * Supported [SortOrder] variants. Must not be empty. - * - * For better performance use [EnumSet] for more than one item. - */ - public val availableSortOrders: Set - - @Deprecated("Too complex. Use filterCapabilities instead") - public val searchQueryCapabilities: MangaSearchQueryCapabilities - - public val filterCapabilities: MangaListFilterCapabilities - - public val config: MangaSourceConfig - - public val authorizationProvider: MangaParserAuthProvider? - get() = this as? MangaParserAuthProvider - - /** - * Provide default domain and available alternatives, if any. - * - * Never hardcode domain in requests, use [domain] instead. - */ - public val configKeyDomain: ConfigKey.Domain - - public val domain: String - - @Deprecated("Too complex. Use getList with filter instead") - public suspend fun getList(query: MangaSearchQuery): List - - public suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List - - /** - * Parse details for [Manga]: chapters list, description, large cover, etc. - * Must return the same manga, may change any fields excepts id, url and source - * @see Manga.copy - */ - public suspend fun getDetails(manga: Manga): Manga - - /** - * Parse pages list for specified chapter. - * @see MangaPage for details - */ - public suspend fun getPages(chapter: MangaChapter): List - - /** - * Fetch direct link to the page image. - */ - public suspend fun getPageUrl(page: MangaPage): String - - public suspend fun getFilterOptions(): MangaListFilterOptions - - /** - * Parse favicons from the main page of the source`s website - */ - public suspend fun getFavicons(): Favicons - - public fun onCreateConfig(keys: MutableCollection>) - - public suspend fun getRelatedManga(seed: Manga): List - - public fun getRequestHeaders(): Headers - - /** - * Return [Manga] object by web link to it - * @see [Manga.publicUrl] - */ - @InternalParsersApi - public suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt deleted file mode 100644 index dc25d5862..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import org.koitharu.kotatsu.parsers.exception.AuthRequiredException -import org.koitharu.kotatsu.parsers.exception.ParseException - -/** - * Implement this in your parser for authorization support - */ -public interface MangaParserAuthProvider { - - /** - * Return link to the login page, which will be opened in browser. - * Must be an absolute url - */ - public val authUrl: String - - /** - * Quick check if user is logged in. - * In most case you should check for cookies in [MangaLoaderContext.cookieJar]. - */ - public suspend fun isAuthorized(): Boolean - - /** - * Fetch and return current user`s name or login. - * Normally should not be called if [isAuthorized] returns false - * @throws [AuthRequiredException] if user is not logged in or authorization is expired - * @throws [ParseException] on parsing error - */ - public suspend fun getUsername(): String -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt deleted file mode 100644 index b708d547e..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import org.koitharu.kotatsu.parsers.model.ContentType - -/** - * Annotate each [MangaParser] implementation with this annotation, used by codegen - */ -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.SOURCE) -internal annotation class MangaSourceParser( - /** - * Name of manga source. Used as an Enum value, must be UPPER_CASE and unique. - */ - val name: String, - /** - * User-friendly title of manga source. In most case equals the website name. - * Avoid extra whitespaces between the words if it is not required. - */ - val title: String, - /** - * Language code (for example "en" or "ru") or blank if parser provide manga on different languages. - */ - val locale: String = "", - /** - * Type of content provided by parser. See [ContentType] for more info - */ - val type: ContentType = ContentType.MANGA, -) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt deleted file mode 100644 index f39d29dee..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.parsers.bitmap - -public interface Bitmap { - - public val width: Int - public val height: Int - - public fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt deleted file mode 100644 index 602182967..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.koitharu.kotatsu.parsers.bitmap - -public data class Rect( - val left: Int = 0, - val top: Int = 0, - val right: Int = 0, - val bottom: Int = 0, -) { - - val width: Int - get() = right - left - - val height: Int - get() = bottom - top -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt new file mode 100644 index 000000000..b1a7ab6fb --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt @@ -0,0 +1,242 @@ +@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") + +package org.koitharu.kotatsu.parsers.compat + +import android.app.Application +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.core.AbstractMangaParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.util.generateUid +import uy.kohesive.injekt.Injekt + +/** + * Adapter that wraps a Tachiyomi [HttpSource] and exposes it as a Kotatsu parser via [AbstractMangaParser]. + * Enables Tachiyomi extension sources to be used within the Kotatsu/Usagi ecosystem. + */ +@OptIn(InternalParsersApi::class) +internal class TachiyomiSourceAdapter( + context: MangaLoaderContext, + source: MangaParserSource, + private val tachiyomiSource: HttpSource, +) : AbstractMangaParser(context, source) { + + init { + // Inject MangaLoaderContext's OkHttpClient into Tachiyomi's NetworkHelper + // so extensions use the same client (cookies, CF-bypass interceptors, etc.) + NetworkHelper.setClient(context.httpClient) + + // Register Application context so Injekt.get() works + // (used by extensions that access WebView or SharedPreferences) + // We use reflection to grab the real Android Application context from ActivityThread + runCatching { + val activityThreadClass = Class.forName("android.app.ActivityThread") + val currentApplicationMethod = activityThreadClass.getMethod("currentApplication") + val app = currentApplicationMethod.invoke(null) as? Application + if (app != null) { + Injekt.addSingleton(app) + } else { + Injekt.addSingleton(Application()) + } + }.onFailure { + Injekt.addSingleton(Application()) + } + } + + override val configKeyDomain: ConfigKey.Domain by lazy { + val host = runCatching { + java.net.URI(tachiyomiSource.baseUrl).host + ?: tachiyomiSource.baseUrl.toHostOnly() + }.getOrDefault(tachiyomiSource.baseUrl.toHostOnly()) + ConfigKey.Domain(host) + } + + override val availableSortOrders: Set = buildSet { + add(SortOrder.POPULARITY) + add(SortOrder.UPDATED) + if (tachiyomiSource.supportsLatest) { + add(SortOrder.NEWEST) + } + } + + @InternalParsersApi + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + isMultipleTagsSupported = false, + ) + + // ============================== List =================================== + + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val page = (offset / PAGE_SIZE) + 1 + + val mangasPage: MangasPage = withContext(Dispatchers.IO) { + when { + !query.isNullOrEmpty() -> { + val filterList = tachiyomiSource.getFilterList() + tachiyomiSource.fetchSearchManga(page, query, filterList) + .toBlocking().single() + } + order == SortOrder.NEWEST && tachiyomiSource.supportsLatest -> { + tachiyomiSource.fetchLatestUpdates(page).toBlocking().single() + } + else -> { + tachiyomiSource.fetchPopularManga(page).toBlocking().single() + } + } + } + + return mangasPage.mangas.map { it.toManga() } + } + + // ============================== Details ================================ + + override suspend fun getDetails(manga: Manga): Manga { + val sManga = manga.toSManga() + + return withContext(Dispatchers.IO) { + val details = tachiyomiSource.fetchMangaDetails(sManga).toBlocking().single() + val chapters = tachiyomiSource.fetchChapterList(sManga).toBlocking().single() + + manga.copy( + title = details.title.ifEmpty { manga.title }, + altTitles = emptySet(), + coverUrl = details.thumbnail_url ?: manga.coverUrl, + largeCoverUrl = details.thumbnail_url, + description = details.description, + authors = setOfNotNull(details.author, details.artist) + .filter { it.isNotBlank() }.toSet(), + tags = parseGenreTags(details.genre), + state = mapStatus(details.status), + chapters = chapters.mapIndexed { index, sChapter -> + sChapter.toMangaChapter(index, chapters.size) + }, + ) + } + } + + // ============================== Pages ================================== + + override suspend fun getPages(chapter: MangaChapter): List { + val sChapter = SChapter.create().apply { + url = chapter.url + name = chapter.title ?: "" + chapter_number = chapter.number + } + + val pages = withContext(Dispatchers.IO) { + tachiyomiSource.fetchPageList(sChapter).toBlocking().single() + } + + return pages.map { page -> + val imgUrl = page.imageUrl ?: page.url + MangaPage( + id = generateUid(imgUrl.ifEmpty { "${chapter.url}#${page.index}" }), + url = imgUrl, + preview = null, + source = source, + ) + } + } + + override suspend fun getPageUrl(page: MangaPage): String { + val url = page.url + if (url.startsWith("http://") || url.startsWith("https://")) return url + return "https://$domain$url" + } + + // ============================== Filter Options ========================= + + override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() + + // ============================== Headers ================================ + + override fun getRequestHeaders(): Headers = tachiyomiSource.headers + + // ============================== Interceptor ============================ + + override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) + + // ============================== Private Helpers ======================== + + private fun SManga.toManga(): Manga { + val mangaUrl = this.url + val publicUrl = if (mangaUrl.startsWith("http")) mangaUrl + else "${tachiyomiSource.baseUrl}$mangaUrl" + + return Manga( + id = generateUid(mangaUrl), + title = this.title, + altTitles = emptySet(), + url = mangaUrl, + publicUrl = publicUrl, + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = this.thumbnail_url, + tags = parseGenreTags(this.genre), + state = mapStatus(this.status), + authors = setOfNotNull(this.author, this.artist) + .filter { it.isNotBlank() }.toSet(), + source = source, + ) + } + + private fun Manga.toSManga(): SManga = SManga.create().apply { + url = this@toSManga.url + title = this@toSManga.title + thumbnail_url = this@toSManga.coverUrl + } + + private fun SChapter.toMangaChapter(index: Int, total: Int): MangaChapter { + return MangaChapter( + id = generateUid(this.url), + title = this.name.ifBlank { null }, + number = if (this.chapter_number > 0f) this.chapter_number else (total - index).toFloat(), + volume = 0, + url = this.url, + scanlator = this.scanlator, + uploadDate = this.date_upload, + branch = null, + source = source, + ) + } + + private fun parseGenreTags(genreStr: String?): Set { + if (genreStr.isNullOrBlank()) return emptySet() + return genreStr.split(",") + .map { it.trim() } + .filter { it.isNotBlank() } + .mapTo(mutableSetOf()) { genre -> + MangaTag(title = genre, key = genre.lowercase(), source = source) + } + } + + private fun mapStatus(status: Int): MangaState? = when (status) { + SManga.ONGOING -> MangaState.ONGOING + SManga.COMPLETED -> MangaState.FINISHED + SManga.ON_HIATUS -> MangaState.PAUSED + SManga.CANCELLED -> MangaState.ABANDONED + else -> null + } + + private fun String.toHostOnly(): String = + removePrefix("https://").removePrefix("http://").substringBefore("/") + + companion object { + private const val PAGE_SIZE = 20 + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt deleted file mode 100644 index cd233f1d9..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.koitharu.kotatsu.parsers.config - -public sealed class ConfigKey( - @JvmField public val key: String, -) { - - public abstract val defaultValue: T - - public class Domain( - @JvmField @JvmSuppressWildcards public vararg val presetValues: String, - ) : ConfigKey("domain") { - - init { - require(presetValues.isNotEmpty()) { "You must provide at least one domain" } - } - - override val defaultValue: String - get() = presetValues.first() - } - - public class ShowSuspiciousContent( - override val defaultValue: Boolean, - ) : ConfigKey("show_suspicious") - - public class UserAgent( - override val defaultValue: String, - ) : ConfigKey("user_agent") - - public class SplitByTranslations( - override val defaultValue: Boolean, - ) : ConfigKey("split_translations") - - public class PreferredImageServer( - public val presetValues: Map, - override val defaultValue: String?, - ) : ConfigKey("img_server") -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt deleted file mode 100644 index d4e11c49f..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.koitharu.kotatsu.parsers.config - -public interface MangaSourceConfig { - - public operator fun get(key: ConfigKey): T -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt deleted file mode 100644 index 9edbd6b8c..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt +++ /dev/null @@ -1,107 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.core - -import androidx.annotation.CallSuper -import okhttp3.Headers -import okhttp3.HttpUrl -import okhttp3.Interceptor -import okhttp3.Response -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaParser -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.config.MangaSourceConfig -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities -import org.koitharu.kotatsu.parsers.network.CommonHeaders -import org.koitharu.kotatsu.parsers.network.OkHttpWebClient -import org.koitharu.kotatsu.parsers.network.WebClient -import org.koitharu.kotatsu.parsers.util.* -import java.util.* - -@Suppress("OVERRIDE_DEPRECATION") -@InternalParsersApi -public abstract class AbstractMangaParser @InternalParsersApi constructor( - @property:InternalParsersApi public val context: MangaLoaderContext, - public final override val source: MangaParserSource, -) : MangaParser { - - public final override val searchQueryCapabilities: MangaSearchQueryCapabilities - get() = filterCapabilities.toMangaSearchQueryCapabilities() - - public override val config: MangaSourceConfig by lazy { context.getConfig(source) } - - public open val sourceLocale: Locale - get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale) - - protected val sourceContentRating: ContentRating? - get() = if (source.contentType == ContentType.HENTAI) { - ContentRating.ADULT - } else { - null - } - - protected val isNsfwSource: Boolean = source.contentType == ContentType.HENTAI - - protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent()) - - override fun getRequestHeaders(): Headers = Headers.Builder() - .add(CommonHeaders.USER_AGENT, config[userAgentKey]) - .build() - - /** - * Used as fallback if value of `order` passed to [getList] is null - */ - public open val defaultSortOrder: SortOrder - get() { - val supported = availableSortOrders - return SortOrder.entries.first { it in supported } - } - - final override val domain: String - get() = config[configKeyDomain] - - protected open val webClient: WebClient = OkHttpWebClient(context.httpClient, source) - - /** - * Search list of manga by specified searchQuery - * - * @param query searchQuery - */ - public final override suspend fun getList(query: MangaSearchQuery): List = getList( - offset = query.offset, - order = query.order ?: defaultSortOrder, - filter = convertToMangaListFilter(query), - ) - - /** - * Fetch direct link to the page image. - */ - public override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain) - - /** - * Parse favicons from the main page of the source`s website - */ - public override suspend fun getFavicons(): Favicons { - return FaviconParser(webClient, domain).parseFavicons() - } - - @CallSuper - public override fun onCreateConfig(keys: MutableCollection>) { - keys.add(configKeyDomain) - } - - public override suspend fun getRelatedManga(seed: Manga): List { - return RelatedMangaFinder(listOf(this)).invoke(seed) - } - - /** - * Return [Manga] object by web link to it - * @see [Manga.publicUrl] - */ - override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null - - override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt deleted file mode 100644 index 1346246a1..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt +++ /dev/null @@ -1,96 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.core - -import androidx.annotation.CallSuper -import okhttp3.Headers -import okhttp3.HttpUrl -import okhttp3.Interceptor -import okhttp3.Response -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaParser -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.config.MangaSourceConfig -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.network.CommonHeaders -import org.koitharu.kotatsu.parsers.network.OkHttpWebClient -import org.koitharu.kotatsu.parsers.network.WebClient -import org.koitharu.kotatsu.parsers.util.* -import java.util.* - -@Deprecated("Too complex. Use AbstractMangaParser instead") -internal abstract class FlexibleMangaParser @InternalParsersApi constructor( - @property:InternalParsersApi val context: MangaLoaderContext, - final override val source: MangaParserSource, -) : MangaParser { - - override val config: MangaSourceConfig by lazy { context.getConfig(source) } - - open val sourceLocale: Locale - get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale) - - protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent()) - - final override val filterCapabilities: MangaListFilterCapabilities - get() = searchQueryCapabilities.toMangaListFilterCapabilities() - - protected val sourceContentRating: ContentRating? - get() = if (source.contentType == ContentType.HENTAI) { - ContentRating.ADULT - } else { - null - } - - final override val domain: String - get() = config[configKeyDomain] - - @Deprecated("Override intercept() instead") - override fun getRequestHeaders(): Headers = Headers.Builder() - .add(CommonHeaders.USER_AGENT, config[userAgentKey]) - .build() - - /** - * Used as fallback if value of `order` passed to [getList] is null - */ - open val defaultSortOrder: SortOrder - get() { - val supported = availableSortOrders - return SortOrder.entries.first { it in supported } - } - - protected open val webClient: WebClient = OkHttpWebClient(context.httpClient, source) - - /** - * Fetch direct link to the page image. - */ - override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain) - - final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - return getList(convertToMangaSearchQuery(offset, order, filter)) - } - - /** - * Parse favicons from the main page of the source`s website - */ - override suspend fun getFavicons(): Favicons { - return FaviconParser(webClient, domain).parseFavicons() - } - - @CallSuper - override fun onCreateConfig(keys: MutableCollection>) { - keys.add(configKeyDomain) - } - - override suspend fun getRelatedManga(seed: Manga): List { - return RelatedMangaFinder(listOf(this)).invoke(seed) - } - - /** - * Return [Manga] object by web link to it - * @see [Manga.publicUrl] - */ - override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null - - override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt deleted file mode 100644 index 369cb23f0..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt +++ /dev/null @@ -1,63 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.core - -import androidx.annotation.VisibleForTesting -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.model.search.SearchableField -import org.koitharu.kotatsu.parsers.util.Paginator - -@Deprecated("Too complex. Use PagedMangaParser instead") -internal abstract class FlexiblePagedMangaParser( - context: MangaLoaderContext, - source: MangaParserSource, - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int, - searchPageSize: Int = pageSize, -) : FlexibleMangaParser(context, source) { - - @JvmField - protected val paginator: Paginator = Paginator(pageSize) - - @JvmField - protected val searchPaginator: Paginator = Paginator(searchPageSize) - - @Deprecated("Too complex. Use getList with filter instead") - final override suspend fun getList(query: MangaSearchQuery): List { - var containTitleNameCriteria = false - query.criteria.forEach { - if (it.field == SearchableField.TITLE_NAME) { - containTitleNameCriteria = true - } - } - - return searchManga( - paginator = if (containTitleNameCriteria) { - paginator - } else { - searchPaginator - }, - query = query, - ) - } - - public abstract suspend fun getListPage(query: MangaSearchQuery, page: Int): List - - protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) { - paginator.firstPage = firstPage - searchPaginator.firstPage = firstPageForSearch - } - - private suspend fun searchManga( - paginator: Paginator, - query: MangaSearchQuery, - ): List { - val offset: Int = query.offset - val page = paginator.getPage(offset) - val list = getListPage(query, page) - paginator.onListReceived(offset, page, list.size) - return list - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt deleted file mode 100644 index 39abc6536..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt +++ /dev/null @@ -1,79 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.core - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import org.koitharu.kotatsu.parsers.MangaParser -import org.koitharu.kotatsu.parsers.MangaParserAuthProvider -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.util.mergeWith - -internal class MangaParserWrapper( - private val delegate: MangaParser, -) : MangaParser by delegate { - - override val authorizationProvider: MangaParserAuthProvider? - get() = delegate as? MangaParserAuthProvider - - @Deprecated("Too complex. Use getList with filter instead") - override suspend fun getList(query: MangaSearchQuery): List = withContext(Dispatchers.Default) { - if (!query.skipValidation) { - searchQueryCapabilities.validate(query) - } - delegate.getList(query) - } - - override suspend fun getList( - offset: Int, - order: SortOrder, - filter: MangaListFilter, - ): List = withContext(Dispatchers.Default) { - delegate.getList(offset, order, filter) - } - - override suspend fun getDetails(manga: Manga): Manga = withContext(Dispatchers.Default) { - delegate.getDetails(manga) - } - - override suspend fun getPages(chapter: MangaChapter): List = withContext(Dispatchers.Default) { - delegate.getPages(chapter) - } - - override suspend fun getPageUrl(page: MangaPage): String = withContext(Dispatchers.Default) { - delegate.getPageUrl(page) - } - - override suspend fun getFilterOptions(): MangaListFilterOptions = withContext(Dispatchers.Default) { - delegate.getFilterOptions() - } - - override suspend fun getFavicons(): Favicons = withContext(Dispatchers.Default) { - delegate.getFavicons() - } - - override suspend fun getRelatedManga(seed: Manga): List = withContext(Dispatchers.Default) { - delegate.getRelatedManga(seed) - } - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val headers = request.headers.newBuilder() - .mergeWith(delegate.getRequestHeaders(), replaceExisting = false) - .build() - val newRequest = request.newBuilder().headers(headers).build() - return delegate.intercept(ProxyChain(chain, newRequest)) - } - - private class ProxyChain( - private val delegate: Interceptor.Chain, - private val request: Request, - ) : Interceptor.Chain by delegate { - - override fun request(): Request = request - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt deleted file mode 100644 index d8744eae0..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.koitharu.kotatsu.parsers.core - -import androidx.annotation.VisibleForTesting -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaListFilter -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.util.Paginator - -@InternalParsersApi -public abstract class PagedMangaParser( - context: MangaLoaderContext, - source: MangaParserSource, - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int, - searchPageSize: Int = pageSize, -) : AbstractMangaParser(context, source) { - - @JvmField - protected val paginator: Paginator = Paginator(pageSize) - - @JvmField - protected val searchPaginator: Paginator = Paginator(searchPageSize) - - final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - return getList( - paginator = if (filter.query.isNullOrEmpty()) { - paginator - } else { - searchPaginator - }, - offset = offset, - order = order, - filter = filter, - ) - } - - public abstract suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List - - protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) { - paginator.firstPage = firstPage - searchPaginator.firstPage = firstPageForSearch - } - - private suspend fun getList( - paginator: Paginator, - offset: Int, - order: SortOrder, - filter: MangaListFilter, - ): List { - val page = paginator.getPage(offset) - val list = getListPage(page, order, filter) - paginator.onListReceived(offset, page, list.size) - return list - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt deleted file mode 100644 index 5e06b69e4..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.koitharu.kotatsu.parsers.core - -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaListFilter -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.SortOrder - -@InternalParsersApi -public abstract class SinglePageMangaParser( - context: MangaLoaderContext, - source: MangaParserSource, -) : AbstractMangaParser(context, source) { - - final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - if (offset > 0) { - return emptyList() - } - return getList(order, filter) - } - - public abstract suspend fun getList(order: SortOrder, filter: MangaListFilter): List -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt deleted file mode 100644 index caa291b84..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.koitharu.kotatsu.parsers.exception - -import okio.IOException -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.model.MangaSource - -/** - * Authorization is required for access to the requested content - */ -public class AuthRequiredException @InternalParsersApi @JvmOverloads constructor( - public val source: MangaSource, - cause: Throwable? = null, -) : IOException("Authorization required", cause) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt deleted file mode 100644 index 65f47f693..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.koitharu.kotatsu.parsers.exception - -public class ContentUnavailableException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt deleted file mode 100644 index c36da842f..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.koitharu.kotatsu.parsers.exception - -import okio.IOException -import org.json.JSONArray -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull - -public class GraphQLException @InternalParsersApi constructor(errors: JSONArray) : IOException() { - - public val messages: List = errors.mapJSONNotNull { - it.getString("message") - } - - override val message: String - get() = messages.joinToString("\n") -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt deleted file mode 100644 index fb1a03e2c..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.parsers.exception - -import org.jsoup.HttpStatusException -import java.net.HttpURLConnection - -public class NotFoundException( - message: String, - url: String, -) : HttpStatusException(message, HttpURLConnection.HTTP_NOT_FOUND, url) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt deleted file mode 100644 index 4ba7c53f8..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.parsers.exception - -import org.koitharu.kotatsu.parsers.InternalParsersApi - -public class ParseException @InternalParsersApi @JvmOverloads constructor( - public val shortMessage: String?, - public val url: String, - cause: Throwable? = null, -) : RuntimeException("$shortMessage at $url", cause) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt deleted file mode 100644 index cf35fe0ac..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.koitharu.kotatsu.parsers.exception - -import okio.IOException -import java.time.Instant -import java.time.temporal.ChronoUnit - -public class TooManyRequestExceptions( - public val url: String, - retryAfter: Long, -) : IOException("Too man requests") { - - public val retryAt: Instant? = if (retryAfter > 0 && retryAfter < Long.MAX_VALUE) { - Instant.now().plusMillis(retryAfter) - } else { - null - } - - public fun getRetryDelay(): Long { - if (retryAt == null) { - return -1L - } - return Instant.now().until(retryAt, ChronoUnit.MILLIS).coerceAtLeast(0L) - } - - override val message: String? - get() = if (retryAt != null) { - "${super.message}, retry at $retryAt" - } else { - super.message - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt deleted file mode 100644 index a1ed22c82..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt +++ /dev/null @@ -1,11 +0,0 @@ -@file:JvmName("Constants") - -package org.koitharu.kotatsu.parsers.model - -public const val RATING_UNKNOWN: Float = -1f - -public const val YEAR_UNKNOWN: Int = 0 - -public const val YEAR_MIN: Int = 1900 - -public const val YEAR_MAX: Int = 2099 diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt deleted file mode 100644 index 6658f1e8f..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -public enum class ContentRating { - SAFE, - SUGGESTIVE, - ADULT -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt deleted file mode 100644 index 2027fe7a1..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -public enum class ContentType { - - /** - * Standard manga, manhua, webtoons, etc - */ - MANGA, - - MANHWA, - - MANHUA, - - /** - * Use this if the source provides mostly nsfw content. - */ - HENTAI, - - /** - * Western comics - */ - COMICS, - - NOVEL, - - /** - * Use this type if no other suits your needs. For example, for an indie manga - */ - - ONE_SHOT, - DOUJINSHI, - IMAGE_SET, - ARTIST_CG, - GAME_CG, - OTHER, -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt deleted file mode 100644 index 6d32d8bbe..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -public enum class Demographic { - SHOUNEN, - SHOUJO, - SEINEN, - JOSEI, - KODOMO, - NONE, -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt deleted file mode 100644 index 28d7c717e..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import okhttp3.HttpUrl.Companion.toHttpUrl - -public data class Favicon( - @JvmField public val url: String, - @JvmField public val size: Int, - @JvmField internal val rel: String?, -) : Comparable { - - @JvmField - public val type: String = url.toHttpUrl().pathSegments.last() - .substringAfterLast('.', "").lowercase() - - override fun compareTo(other: Favicon): Int { - val res = size.compareTo(other.size) - if (res != 0) { - return res - } - return relWeightOf(rel).compareTo(relWeightOf(other.rel)) - } - - private fun relWeightOf(rel: String?) = when (rel) { - "apple-touch-icon" -> 1 // Prefer apple-touch-icon because it has a better quality - "mask-icon" -> -1 - else -> 0 - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt deleted file mode 100644 index b11add359..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -public class Favicons( - favicons: Collection, - @JvmField public val referer: String?, -) : Collection { - - private val icons = favicons.sortedDescending() - - override val size: Int - get() = icons.size - - override fun contains(element: Favicon): Boolean = icons.contains(element) - - override fun containsAll(elements: Collection): Boolean = icons.containsAll(elements) - - override fun isEmpty(): Boolean = icons.isEmpty() - - override fun iterator(): Iterator = icons.iterator() - - public operator fun minus(victim: Favicon): Favicons = Favicons( - favicons = icons.filterNot { it == victim }, - referer = referer, - ) - - /** - * Finds a favicon whose size in pixels is greater than or equal to the specified size. - * If such icon is not available returns the largest icon - * @param size in pixels - * @param types supported file types, e.g. png, svg, ico. May be null but not empty - */ - @JvmOverloads - public fun find(size: Int, types: Set? = null): Favicon? { - if (icons.isEmpty()) { - return null - } - var result: Favicon? = null - for (icon in icons) { - if (types != null && icon.type !in types) { - continue - } - if (result == null || icon.size >= size) { - result = icon - } else { - break - } - } - return result - } - - public companion object { - - @JvmStatic - public val EMPTY: Favicons = Favicons(emptySet(), null) - - @JvmStatic - public fun single(url: String): Favicons = Favicons(setOf(Favicon(url, 0, null)), null) - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt deleted file mode 100644 index 7ff1874da..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt +++ /dev/null @@ -1,203 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import androidx.collection.ArrayMap -import org.koitharu.kotatsu.parsers.util.findById -import org.koitharu.kotatsu.parsers.util.nullIfEmpty - -public data class Manga( - /** - * Unique identifier for manga - */ - @JvmField public val id: Long, - /** - * Manga title, human-readable - */ - @JvmField public val title: String, - /** - * Alternative titles (for example on other language), may be empty - */ - @JvmField public val altTitles: Set, - /** - * Relative url to manga (**without** a domain) or any other uri. - * Used principally in parsers - */ - @JvmField public val url: String, - /** - * Absolute url to manga, must be ready to open in browser - */ - @JvmField public val publicUrl: String, - /** - * Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown - * @see hasRating - */ - @JvmField public val rating: Float, - /** - * Indicates that manga may contain sensitive information (18+, NSFW) - */ - @JvmField public val contentRating: ContentRating?, - /** - * Absolute link to the cover - * @see largeCoverUrl - */ - @JvmField public val coverUrl: String?, - /** - * Tags (genres) of the manga - */ - @JvmField public val tags: Set, - /** - * Manga status (ongoing, finished) or null if unknown - */ - @JvmField public val state: MangaState?, - /** - * Authors of the manga - */ - @JvmField public val authors: Set, - /** - * Large cover url (absolute), null if is no large cover - * @see coverUrl - */ - @JvmField public val largeCoverUrl: String? = null, - /** - * Manga description, may be html or null - */ - @JvmField public val description: String? = null, - /** - * List of chapters - */ - @JvmField public val chapters: List? = null, - /** - * Manga source - */ - @JvmField public val source: MangaSource, -) { - - @Deprecated("Use other constructor") - public constructor( - /** - * Unique identifier for manga - */ - id: Long, - /** - * Manga title, human-readable - */ - title: String, - /** - * Alternative title (for example on other language), may be null - */ - altTitle: String?, - /** - * Relative url to manga (**without** a domain) or any other uri. - * Used principally in parsers - */ - url: String, - /** - * Absolute url to manga, must be ready to open in browser - */ - publicUrl: String, - /** - * Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown - * @see hasRating - */ - rating: Float, - /** - * Indicates that manga may contain sensitive information (18+, NSFW) - */ - isNsfw: Boolean, - /** - * Absolute link to the cover - * @see largeCoverUrl - */ - coverUrl: String?, - /** - * Tags (genres) of the manga - */ - tags: Set, - /** - * Manga status (ongoing, finished) or null if unknown - */ - state: MangaState?, - /** - * Authors of the manga - */ - author: String?, - /** - * Large cover url (absolute), null if is no large cover - * @see coverUrl - */ - largeCoverUrl: String? = null, - /** - * Manga description, may be html or null - */ - description: String? = null, - /** - * List of chapters - */ - chapters: List? = null, - /** - * Manga source - */ - source: MangaSource, - ) : this( - id = id, - title = title, - altTitles = setOfNotNull(altTitle?.nullIfEmpty()), - url = url, - publicUrl = publicUrl, - rating = rating, - contentRating = if (isNsfw) ContentRating.ADULT else null, - coverUrl = coverUrl?.nullIfEmpty(), - tags = tags, - state = state, - authors = setOfNotNull(author), - largeCoverUrl = largeCoverUrl?.nullIfEmpty(), - description = description?.nullIfEmpty(), - chapters = chapters, - source = source, - ) - - /** - * Author of the manga, may be null - */ - @Deprecated("Please use authors") - public val author: String? - get() = authors.firstOrNull() - - /** - * Alternative title (for example on other language), may be null - */ - @Deprecated("Please use altTitles") - public val altTitle: String? - get() = altTitles.firstOrNull() - - /** - * Return if manga has a specified rating - * @see rating - */ - public val hasRating: Boolean - get() = rating > 0f && rating <= 1f - - @Deprecated("Use contentRating instead", ReplaceWith("contentRating == ContentRating.ADULT")) - public val isNsfw: Boolean - get() = contentRating == ContentRating.ADULT - - public fun getChapters(branch: String?): List { - return chapters?.filter { x -> x.branch == branch }.orEmpty() - } - - public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id) - - public fun requireChapterById(id: Long): MangaChapter = findChapterById(id) - ?: throw NoSuchElementException("Chapter with id $id not found") - - public fun getBranches(): Map { - if (chapters.isNullOrEmpty()) { - return emptyMap() - } - val result = ArrayMap() - chapters.forEach { - val key = it.branch - result[key] = result.getOrDefault(key, 0) + 1 - } - return result - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt deleted file mode 100644 index 8790b8dae..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt +++ /dev/null @@ -1,65 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import org.koitharu.kotatsu.parsers.util.formatSimple -import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty - -public data class MangaChapter( - /** - * An unique id of chapter - */ - @JvmField public val id: Long, - /** - * User-readable name of chapter if provided by parser or null instead - * Do not pass manga title or chapter number here - */ - @JvmField public val title: String?, - /** - * Chapter number starting from 1, 0 if unknown - */ - @JvmField public val number: Float, - /** - * Volume number starting from 1, 0 if unknown - */ - @JvmField public val volume: Int, - /** - * Relative url to chapter (**without** a domain) or any other uri. - * Used principally in parsers - */ - @JvmField public val url: String, - /** - * User-readable name of scanlator (releaser) or null if unknown - */ - @JvmField public val scanlator: String?, - /** - * Chapter upload date in milliseconds - */ - @JvmField public val uploadDate: Long, - /** - * User-readable name of branch. - * A branch is a group of chapters that overlap (e.g. different languages) - */ - @JvmField public val branch: String?, - @JvmField public val source: MangaSource, -) { - - @Deprecated("Use title instead", ReplaceWith("title")) - val name: String - get() = title.ifNullOrEmpty { - buildString { - if (volume > 0) append("Vol ").append(volume).append(' ') - if (number > 0) append("Chapter ").append(number.formatSimple()) else append("Unnamed") - } - } - - public fun numberString(): String? = if (number > 0f) { - number.formatSimple() - } else { - null - } - - public fun volumeString(): String? = if (volume > 0) { - volume.toString() - } else { - null - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt deleted file mode 100644 index d643e5e27..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt +++ /dev/null @@ -1,88 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import java.util.* - -public data class MangaListFilter( - @JvmField val query: String? = null, - @JvmField val tags: Set = emptySet(), - @JvmField val tagsExclude: Set = emptySet(), - @JvmField val locale: Locale? = null, - @JvmField val originalLocale: Locale? = null, - @JvmField val states: Set = emptySet(), - @JvmField val contentRating: Set = emptySet(), - @JvmField val types: Set = emptySet(), - @JvmField val demographics: Set = emptySet(), - @JvmField val year: Int = YEAR_UNKNOWN, - @JvmField val yearFrom: Int = YEAR_UNKNOWN, - @JvmField val yearTo: Int = YEAR_UNKNOWN, - @JvmField val author: String? = null, -) { - - private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() && - tagsExclude.isEmpty() && - locale == null && - originalLocale == null && - states.isEmpty() && - contentRating.isEmpty() && - year == YEAR_UNKNOWN && - yearFrom == YEAR_UNKNOWN && - yearTo == YEAR_UNKNOWN && - types.isEmpty() && - demographics.isEmpty() && - author.isNullOrEmpty() - - public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty() - - public fun isNotEmpty(): Boolean = !isEmpty() - - public fun hasNonSearchOptions(): Boolean = !isNonSearchOptionsEmpty() - - public companion object { - - @JvmStatic - public val EMPTY: MangaListFilter = MangaListFilter() - } - - internal class Builder { - private var query: String? = null - private val tags: MutableSet = mutableSetOf() - private val tagsExclude: MutableSet = mutableSetOf() - private var locale: Locale? = null - private var originalLocale: Locale? = null - private val states: MutableSet = mutableSetOf() - private val contentRating: MutableSet = mutableSetOf() - private val types: MutableSet = mutableSetOf() - private val demographics: MutableSet = mutableSetOf() - private var year: Int = YEAR_UNKNOWN - private var yearFrom: Int = YEAR_UNKNOWN - private var yearTo: Int = YEAR_UNKNOWN - - fun query(query: String?): Builder = apply { this.query = query } - fun addTag(tag: MangaTag): Builder = apply { tags.add(tag) } - fun addTags(tags: Collection): Builder = apply { this.tags.addAll(tags) } - fun excludeTag(tag: MangaTag): Builder = apply { tagsExclude.add(tag) } - fun excludeTags(tags: Collection): Builder = apply { this.tagsExclude.addAll(tags) } - fun locale(locale: Locale?): Builder = apply { this.locale = locale } - fun originalLocale(locale: Locale?): Builder = apply { this.originalLocale = locale } - fun addState(state: MangaState): Builder = apply { states.add(state) } - fun addStates(states: Collection): Builder = apply { this.states.addAll(states) } - fun addContentRating(rating: ContentRating): Builder = apply { contentRating.add(rating) } - fun addContentRatings(ratings: Collection): Builder = - apply { this.contentRating.addAll(ratings) } - - fun addType(type: ContentType): Builder = apply { types.add(type) } - fun addTypes(types: Collection): Builder = apply { this.types.addAll(types) } - fun addDemographic(demographic: Demographic): Builder = apply { demographics.add(demographic) } - fun addDemographics(demographics: Collection): Builder = - apply { this.demographics.addAll(demographics) } - - fun year(year: Int): Builder = apply { this.year = year } - fun yearFrom(year: Int): Builder = apply { this.yearFrom = year } - fun yearTo(year: Int): Builder = apply { this.yearTo = year } - - fun build(): MangaListFilter = MangaListFilter( - query, tags, tagsExclude, locale, originalLocale, states, - contentRating, types, demographics, year, yearFrom, yearTo, - ) - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt deleted file mode 100644 index d0daa06d3..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import org.koitharu.kotatsu.parsers.InternalParsersApi - -public data class MangaListFilterCapabilities @InternalParsersApi constructor( - - /** - * Whether parser supports filtering by more than one tag - * @see [MangaListFilter.tags] - * @see [MangaListFilterOptions.availableTags] - */ - val isMultipleTagsSupported: Boolean = false, - - /** - * Whether parser supports tagsExclude field in filter - * @see [MangaListFilter.tagsExclude] - * @see [MangaListFilterOptions.availableTags] - */ - val isTagsExclusionSupported: Boolean = false, - - /** - * Whether parser supports searching by string query - * @see [MangaListFilter.query] - */ - val isSearchSupported: Boolean = false, - - /** - * Whether parser supports searching by string query combined within other filters - */ - val isSearchWithFiltersSupported: Boolean = false, - - /** - * Whether parser supports searching/filtering by year - * @see [MangaListFilter.year] - */ - val isYearSupported: Boolean = false, - - /** - * Whether parser supports searching by year range - * @see [MangaListFilter.yearFrom] and [MangaListFilter.yearTo] - */ - val isYearRangeSupported: Boolean = false, - - /** - * Whether parser supports searching Original Languages - * @see [MangaListFilter.originalLocale] - * @see [MangaListFilterOptions.availableLocales] - */ - val isOriginalLocaleSupported: Boolean = false, - - /** - * Whether parser supports searching by author name - * @see [MangaListFilter.author] - */ - val isAuthorSearchSupported: Boolean = false, -) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt deleted file mode 100644 index 6dbc69ee4..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import org.koitharu.kotatsu.parsers.InternalParsersApi -import java.util.* - -public data class MangaListFilterOptions @InternalParsersApi constructor( - - /** - * Available tags (genres) - */ - public val availableTags: Set = emptySet(), - - /** - * Supported [MangaState] variants for filtering. May be empty. - * - * For better performance use [EnumSet] for more than one item. - */ - public val availableStates: Set = emptySet(), - - /** - * Supported [ContentRating] variants for filtering. May be empty. - * - * For better performance use [EnumSet] for more than one item. - */ - public val availableContentRating: Set = emptySet(), - - /** - * Supported [ContentType] variants for filtering. May be empty. - * - * For better performance use [EnumSet] for more than one item. - */ - public val availableContentTypes: Set = emptySet(), - - /** - * Supported [Demographic] variants for filtering. May be empty. - * - * For better performance use [EnumSet] for more than one item. - */ - public val availableDemographics: Set = emptySet(), - - /** - * Supported content locales for multilingual sources - */ - public val availableLocales: Set = emptySet(), -) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt deleted file mode 100644 index 259541cef..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import org.koitharu.kotatsu.parsers.MangaParser - -public data class MangaPage( - /** - * Unique identifier for page - */ - @JvmField public val id: Long, - /** - * Relative url to page (**without** a domain) or any other uri. - * Used principally in parsers. - * May contain link to image or html page. - * @see MangaParser.getPageUrl - */ - @JvmField public val url: String, - /** - * Absolute url of the small page image if exists, null otherwise - */ - @JvmField public val preview: String?, - @JvmField public val source: MangaSource, -) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt deleted file mode 100644 index cbed3dc1a..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -public interface MangaSource { - - public val name: String -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt deleted file mode 100644 index 710c2b404..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -public enum class MangaState { - ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING, RESTRICTED -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt deleted file mode 100644 index 69e969778..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import org.koitharu.kotatsu.parsers.MangaParser - -public data class MangaTag( - /** - * User-readable tag title, should be in Title case - */ - @JvmField public val title: String, - /** - * Identifier of a tag, must be unique among the source. - * @see MangaParser.getList - */ - @JvmField public val key: String, - @JvmField public val source: MangaSource, -) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt deleted file mode 100644 index 6754405f5..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -public enum class SortOrder { - UPDATED, - UPDATED_ASC, - POPULARITY, - POPULARITY_ASC, - RATING, - RATING_ASC, - NEWEST, - NEWEST_ASC, - ALPHABETICAL, - ALPHABETICAL_DESC, - ADDED, - ADDED_ASC, - RELEVANCE, - POPULARITY_HOUR, - POPULARITY_TODAY, - POPULARITY_WEEK, - POPULARITY_MONTH, - POPULARITY_YEAR, -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt deleted file mode 100644 index 4f5c7cd75..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.koitharu.kotatsu.parsers.model - -import org.koitharu.kotatsu.parsers.InternalParsersApi - -@InternalParsersApi -public class WordSet(private vararg val words: String) { - - public fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) } - public fun startsWith(dateString: String): Boolean = words.any { dateString.startsWith(it, ignoreCase = true) } - public fun endsWith(dateString: String): Boolean = words.any { dateString.endsWith(it, ignoreCase = true) } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt deleted file mode 100644 index 9b3ef638f..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt +++ /dev/null @@ -1,92 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.model.search - -import androidx.collection.ArrayMap -import androidx.collection.ArraySet -import org.koitharu.kotatsu.parsers.model.SortOrder - -/** - * Represents a search query for filtering and sorting manga search results. - * This class is immutable and must be constructed using the [Builder]. - * - * @property criteria The set of search criteria applied to the query. - * @property order The sorting order for the results (optional). - * @property offset The offset number for paginated search results (optional). - */ - -@Deprecated("Too complex. Use MangaListFilter instead") -@ConsistentCopyVisibility -public data class MangaSearchQuery private constructor( - @JvmField public val criteria: Set>, - @JvmField public val order: SortOrder?, - @JvmField public val offset: Int, - @JvmField public val skipValidation: Boolean, -) { - - public fun newBuilder(): Builder = Builder(this) - - public class Builder { - - private val criteria = ArraySet>() - private var order: SortOrder? = null - private var offset: Int = 0 - private var skipValidation: Boolean = false - - public constructor() - - public constructor(query: MangaSearchQuery) : this() { - criteria.addAll(query.criteria) - order = query.order - offset = query.offset - } - - public fun criterion(criterion: QueryCriteria<*>): Builder = apply { criteria.add(criterion) } - - public fun order(order: SortOrder?): Builder = apply { this.order = order } - - public fun offset(offset: Int): Builder = apply { this.offset = offset } - - public fun skipValidation(skip: Boolean): Builder = apply { this.skipValidation = skip } - - @Throws(IllegalArgumentException::class) - public fun build(): MangaSearchQuery { - return MangaSearchQuery(deduplicateCriteria(criteria), order, offset, skipValidation) - } - - private fun deduplicateCriteria(criteria: Set>): Set> { - val uniqueCriteria = - ArrayMap>>, QueryCriteria<*>>(criteria.size) - - for (criterion in criteria) { - val key = criterion.field to criterion::class.java - val existing = uniqueCriteria[key] - - when { - existing == null -> uniqueCriteria[key] = criterion - - existing is QueryCriteria.Include<*> && criterion is QueryCriteria.Include<*> -> { - uniqueCriteria[key] = - QueryCriteria.Include(criterion.field, existing.values union criterion.values) - } - - existing is QueryCriteria.Exclude<*> && criterion is QueryCriteria.Exclude<*> -> { - uniqueCriteria[key] = - QueryCriteria.Exclude(criterion.field, existing.values union criterion.values) - } - - else -> throw IllegalArgumentException( - "Match and Range have only one criterion per type, but found duplicates for: ${criterion.field} in ${criterion::class.simpleName}", - ) - } - } - - return uniqueCriteria.values.toSet() - } - } - - public companion object { - - public val EMPTY: MangaSearchQuery = MangaSearchQuery(emptySet(), null, 0, false) - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt deleted file mode 100644 index d2ba414f7..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt +++ /dev/null @@ -1,50 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.model.search - -import androidx.collection.ArraySet -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* -import org.koitharu.kotatsu.parsers.util.mapToSet - -@Deprecated("Too complex. Use MangaListFilterCapabilities instead") -@ExposedCopyVisibility -public data class MangaSearchQueryCapabilities internal constructor( - public val capabilities: Set, -) { - - public constructor(vararg capabilities: SearchCapability) : this(ArraySet(capabilities)) - - internal fun validate(query: MangaSearchQuery) { - val strictFields = capabilities.filter { it.isExclusive }.mapToSet { it.field } - val usedStrictFields = query.criteria.mapToSet { it.field }.intersect(strictFields) - - require(usedStrictFields.isEmpty() || query.criteria.size <= 1) { - "Query contains multiple criteria, but at least one field (${usedStrictFields.joinToString()}) does not support multiple criteria." - } - for (criterion in query.criteria) { - val capability = requireNotNull(capabilities.find { it.field == criterion.field }) { - "Unsupported search field: ${criterion.field}" - } - - require(criterion::class in capability.criteriaTypes) { - "Unsupported search criterion: ${criterion::class.simpleName} for field ${criterion.field}" - } - - // Ensure single value per criterion if supportMultiValue is false - if (!capability.isMultiple) { - when (criterion) { - is Include<*> -> require(criterion.values.size <= 1) { - "Multiple values are not allowed for field ${criterion.field}" - } - - is Exclude<*> -> require(criterion.values.size <= 1) { - "Multiple values are not allowed for field ${criterion.field}" - } - - is Range<*> -> Unit // Range is always valid (from, to) - is Match<*> -> Unit // Match always has a single value - } - } - } - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt deleted file mode 100644 index 342d93b73..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt +++ /dev/null @@ -1,108 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.model.search - -/** - * Represents a generic search criterion used for filtering manga search results. - * Each criterion applies a specific condition to a [SearchableField] and operates on values of type [T]. - * - * @param T The type of value associated with the search criterion. - * @property field The field to which this search criterion applies. - */ -@Deprecated("Too complex") -public sealed interface QueryCriteria { - - public val field: SearchableField - - override fun equals(other: Any?): Boolean - - override fun hashCode(): Int - - /** - * Represents an inclusion criterion that allows search results based on a set of allowed values. - * - * @param T The type of value being included in the search. - * @property values The set of values that should be included in the search results. - * - * ### Example Usage: - * ```kotlin - * val genreFilter = QueryCriteria.Include(SearchableField.STATE, setOf(MangaState.ONGOING, MangaState.FINISHED)) - * ``` - */ - public data class Include( - public override val field: SearchableField, - @JvmField public val values: Set, - ) : QueryCriteria { - - init { - check(values.all { x -> field.type.isInstance(x) }) - } - } - - /** - * Represents an exclusion criterion that exclude results containing certain values. - * - * @param T The type of value being excluded from the search. - * @property values The set of values that should be excluded from the search results. - * - * ### Example Usage: - * ```kotlin - * val excludeTag = QueryCriteria.Exclude(SearchableField.TAG, setOf(MangaTag(key, title, source))) - * ``` - */ - public data class Exclude( - public override val field: SearchableField, - @JvmField public val values: Set, - ) : QueryCriteria { - - init { - check(values.all { x -> field.type.isInstance(x) }) - } - } - - /** - * Represents a range criterion that allows search based on a range of values. - * - * @param T The type of value used in the range (must be comparable). - * @property from The starting value of the range (inclusive). - * @property to The ending value of the range (inclusive). - * - * ### Example Usage: - * ```kotlin - * val yearRange = QueryCriteria.Range(SearchableField.PUBLICATION_YEAR, 2000, 2020) - * ``` - */ - public data class Range>( - public override val field: SearchableField, - @JvmField public val from: T, - @JvmField public val to: T, - ) : QueryCriteria { - - init { - check(field.type.isInstance(from)) - check(field.type.isInstance(to)) - } - } - - - /** - * Represents a match criterion that search results based on an exact match of a value. - * - * @param T The type of value being matched. - * @property value The exact value that must be matched. - * - * ### Example Usage: - * ```kotlin - * val titleMatch = QueryCriteria.Match(SearchableField.TITLE, "manga title") - * ``` - */ - public data class Match( - public override val field: SearchableField, - @JvmField public val value: T, - ) : QueryCriteria { - - init { - check(field.type.isInstance(value)) - } - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt deleted file mode 100644 index df697ab81..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt +++ /dev/null @@ -1,36 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.model.search - -import kotlin.reflect.KClass - -/** - * Defines the search capabilities of a given field in the manga search query. - * - * @property field The searchable field that this capability applies to. - * Example values: - * - `SearchableField.TITLE_NAME` for searching by title. - * - `SearchableField.AUTHOR` for searching by author names. - * - `SearchableField.TAG` for filtering by tags. - * @property criteriaTypes The set of supported criteria types for the field. - * Example values: - * - `setOf(Include::class, Exclude::class)` selected field supports inclusion/exclusion criteria. - * - `setOf(Range::class)` selected field support numerical range criteria. - * @property isMultiValue Indicates whether the field supports multiple values. - * - `true` if multiple values can be provided (e.g., multiple tags or authors). - * - `false` if only a single value is allowed (e.g., only one tag or author). - * @property isExclusive Specifies whether the field can be used alongside other criteria. - * - `true` if this field can be used with other search criteria. - * - `false` if using this field requires it to be the only criterion in query. - */ -@Deprecated("Too complex") -public data class SearchCapability( - /** The searchable field that this capability applies to. */ - @JvmField public val field: SearchableField, - /** The set of supported criteria types for this field. */ - @JvmField public val criteriaTypes: Set>>, - /** Indicates whether the field supports multiple values. */ - @JvmField public val isMultiple: Boolean, - /** Specifies whether the field can be used alongside other criteria. */ - @JvmField public val isExclusive: Boolean = false, -) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt deleted file mode 100644 index f29df1d4d..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.koitharu.kotatsu.parsers.model.search - -import org.koitharu.kotatsu.parsers.model.* -import java.util.* - -/** - * Represents the various fields that can be used for searching manga. - * Each field is associated with a specific data type that defines its expected values. - * - * @property type The Java class representing the expected type of values for this field. - */ -@Deprecated("Too complex") -public enum class SearchableField(public val type: Class<*>) { - TITLE_NAME(String::class.java), - TAG(MangaTag::class.java), - AUTHOR(MangaTag::class.java), - LANGUAGE(Locale::class.java), - ORIGINAL_LANGUAGE(Locale::class.java), - STATE(MangaState::class.java), - CONTENT_TYPE(ContentType::class.java), - CONTENT_RATING(ContentRating::class.java), - DEMOGRAPHIC(Demographic::class.java), - PUBLICATION_YEAR(Int::class.javaObjectType); -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt deleted file mode 100644 index 123735977..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.koitharu.kotatsu.parsers.network - -import okhttp3.CookieJar -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Response -import org.jsoup.Jsoup -import java.net.HttpURLConnection.HTTP_FORBIDDEN -import java.net.HttpURLConnection.HTTP_UNAVAILABLE - -public object CloudFlareHelper { - - public const val PROTECTION_NOT_DETECTED: Int = 0 - public const val PROTECTION_CAPTCHA: Int = 1 - public const val PROTECTION_BLOCKED: Int = 2 - - private const val CF_CLEARANCE = "cf_clearance" - - public fun checkResponseForProtection(response: Response): Int { - if (response.code != HTTP_FORBIDDEN && response.code != HTTP_UNAVAILABLE) { - return PROTECTION_NOT_DETECTED - } - val content = try { - response.peekBody(Long.MAX_VALUE).use { - Jsoup.parse(it.byteStream(), Charsets.UTF_8.name(), response.request.url.toString()) - } - } catch (_: IllegalStateException) { - return PROTECTION_NOT_DETECTED - } - return when { - content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null -> PROTECTION_BLOCKED - content.getElementById("challenge-error-title") != null || - content.getElementById("challenge-error-text") != null || - content.selectFirst("form#challenge-form") != null || - content.selectFirst("iframe[src*='challenges.cloudflare.com']") != null || - content.selectFirst("div#turnstile-wrapper") != null || - content.selectFirst("div.cf-turnstile") != null || - content.getElementById("cf-wrapper") != null || - content.title() == "Just a moment..." || - content.title() == "Attention Required! | Cloudflare" || - content.body().text().let { text -> - text.contains("Verify you are human") || - text.contains("needs to review the security of your connection") - } -> PROTECTION_CAPTCHA - else -> PROTECTION_NOT_DETECTED - } - } - - public fun getClearanceCookie(cookieJar: CookieJar, url: String): String? { - return cookieJar.loadForRequest(url.toHttpUrl()).find { it.name == CF_CLEARANCE }?.value - } - - public fun isCloudFlareCookie(name: String): Boolean { - return name.startsWith("cf_") - || name.startsWith("_cf") - || name.startsWith("__cf") - || name == "csrftoken" - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt deleted file mode 100644 index 66d17a2ce..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.koitharu.kotatsu.parsers.network - -internal object CommonHeaders { - - const val REFERER: String = "Referer" - const val USER_AGENT: String = "User-Agent" - const val ACCEPT: String = "Accept" - const val ACCEPT_LANGUAGE: String = "Accept-Language" - const val ACCEPT_CHARSET: String = "Accept-Charset" - const val ACCEPT_ENCODING: String = "Accept-Encoding" - - const val CONTENT_TYPE: String = "Content-Type" - const val CONTENT_LENGTH: String = "Content-Length" - const val CONTENT_DISPOSITION: String = "Content-Disposition" - const val CONTENT_ENCODING: String = "Content-Encoding" - const val CONTENT_LANGUAGE: String = "Content-Language" - - const val AUTHORIZATION: String = "Authorization" - const val PROXY_AUTHORIZATION: String = "Proxy-Authorization" - - const val COOKIE: String = "Cookie" - const val SET_COOKIE: String = "Set-Cookie" - - const val CACHE_CONTROL: String = "Cache-Control" - const val PRAGMA: String = "Pragma" - const val EXPIRES: String = "Expires" - - const val LAST_MODIFIED: String = "Last-Modified" - const val IF_MODIFIED_SINCE: String = "If-Modified-Since" - const val ETAG: String = "ETag" - const val IF_NONE_MATCH: String = "If-None-Match" - - const val LOCATION: String = "Location" - const val HOST: String = "Host" - const val ORIGIN: String = "Origin" - const val CONNECTION: String = "Connection" - - const val RANGE: String = "Range" - const val CONTENT_RANGE: String = "Content-Range" - - const val RETRY_AFTER: String = "Retry-After" - const val SERVER: String = "Server" - const val DATE: String = "Date" - - const val X_FORWARDED_FOR: String = "X-Forwarded-For" - const val X_REAL_IP: String = "X-Real-IP" - const val X_REQUESTED_WITH: String = "X-Requested-With" - - /* Fetch metadata headers */ - const val SEC_FETCH_DEST: String = "Sec-Fetch-Dest" - const val SEC_FETCH_MODE: String = "Sec-Fetch-Mode" - const val SEC_FETCH_SITE: String = "Sec-Fetch-Site" - const val SEC_FETCH_USER: String = "Sec-Fetch-User" - - /* Client hints headers */ - const val SEC_CH_UA: String = "Sec-Ch-Ua" - const val SEC_CH_UA_MOBILE: String = "Sec-Ch-Ua-Mobile" - const val SEC_CH_UA_PLATFORM: String = "Sec-Ch-Ua-Platform" - - /* Client API headers */ - const val X_CLIENT_TS: String = "x-client-ts" - const val X_CLIENT_SIG: String = "x-client-sig" - - /* API specific headers */ - const val X_APP_KEY: String = "x-app-key" - const val X_APP_ORIGIN: String = "x-app-origin" - const val X_WM_REQUEST_TIME: String = "x-wm-request-time" - const val X_WM_ACCESS_KEY: String = "x-wm-accses-key" - const val X_WM_REQUEST_SIGNATURE: String = "x-wm-request-signature" - - /* Other browser headers */ - const val UPGRADE_INSECURE_REQUESTS: String = "Upgrade-Insecure-Requests" - - /* Some custom headers */ - const val MANGA_SOURCE: String = "X-Manga-Source" - const val TOKEN: String = "Token" -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt deleted file mode 100644 index c641b2477..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt +++ /dev/null @@ -1,133 +0,0 @@ -package org.koitharu.kotatsu.parsers.network - -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.RequestBody.Companion.toRequestBody -import org.json.JSONObject -import org.jsoup.HttpStatusException -import org.koitharu.kotatsu.parsers.exception.AuthRequiredException -import org.koitharu.kotatsu.parsers.exception.GraphQLException -import org.koitharu.kotatsu.parsers.exception.NotFoundException -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.await -import org.koitharu.kotatsu.parsers.util.parseJson -import java.net.HttpURLConnection - -public class OkHttpWebClient( - private val httpClient: OkHttpClient, - private val mangaSource: MangaSource, -) : WebClient { - - override suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response { - val request = Request.Builder() - .get() - .url(url) - .addTags() - .addExtraHeaders(extraHeaders) - return httpClient.newCall(request.build()).await().ensureSuccess() - } - - override suspend fun httpHead(url: HttpUrl): Response { - val request = Request.Builder() - .head() - .url(url) - .addTags() - return httpClient.newCall(request.build()).await().ensureSuccess() - } - - override suspend fun httpPost(url: HttpUrl, form: Map, extraHeaders: Headers?): Response { - val body = FormBody.Builder() - form.forEach { (k, v) -> - body.addEncoded(k, v) - } - val request = Request.Builder() - .post(body.build()) - .url(url) - .addTags() - .addExtraHeaders(extraHeaders) - return httpClient.newCall(request.build()).await().ensureSuccess() - } - - override suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response { - val body = FormBody.Builder() - payload.split('&').forEach { - val pos = it.indexOf('=') - if (pos != -1) { - val k = it.substring(0, pos) - val v = it.substring(pos + 1) - body.addEncoded(k, v) - } - } - val request = Request.Builder() - .post(body.build()) - .url(url) - .addTags() - .addExtraHeaders(extraHeaders) - return httpClient.newCall(request.build()).await().ensureSuccess() - } - - override suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response { - val mediaType = "application/json; charset=utf-8".toMediaType() - val requestBody = body.toString().toRequestBody(mediaType) - val request = Request.Builder() - .post(requestBody) - .url(url) - .addTags() - .addExtraHeaders(extraHeaders) - return httpClient.newCall(request.build()).await().ensureSuccess() - } - - override suspend fun graphQLQuery(endpoint: String, query: String): JSONObject { - val body = JSONObject() - body.put("operationName", null as Any?) - body.put("variables", JSONObject()) - body.put("query", "{$query}") - - val mediaType = "application/json; charset=utf-8".toMediaType() - val requestBody = body.toString().toRequestBody(mediaType) - val request = Request.Builder() - .post(requestBody) - .url(endpoint) - .addTags() - val json = httpClient.newCall(request.build()).await().parseJson() - json.optJSONArray("errors")?.let { - if (it.length() != 0) { - throw GraphQLException(it) - } - } - return json - } - - private fun Request.Builder.addTags(): Request.Builder { - tag(MangaSource::class.java, mangaSource) - return this - } - - private fun Request.Builder.addExtraHeaders(headers: Headers?): Request.Builder { - if (headers != null) { - headers(headers) - } - return this - } - - private fun Response.ensureSuccess(): Response { - val exception: Exception? = when (code) { // Catch some error codes, not all - HttpURLConnection.HTTP_NOT_FOUND -> NotFoundException(message, request.url.toString()) - HttpURLConnection.HTTP_UNAUTHORIZED -> request.tag(MangaSource::class.java)?.let { - AuthRequiredException(it) - } ?: HttpStatusException(message, code, request.url.toString()) - - in 400..599 -> HttpStatusException(message, code, request.url.toString()) - else -> null - } - if (exception != null) { - runCatching { - close() - }.onFailure { - exception.addSuppressed(it) - } - throw exception - } - return this - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt deleted file mode 100644 index a5c2ed54e..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.koitharu.kotatsu.parsers.network - -import okhttp3.HttpUrl -import okhttp3.Interceptor -import okhttp3.Response -import org.koitharu.kotatsu.parsers.network.utils.RateLimiter -import java.util.concurrent.ConcurrentHashMap -import kotlin.time.Duration - -public class RateLimitHelper( - private val permits: Int, - private val period: Duration, - private val shouldLimit: (HttpUrl) -> Boolean -) : Interceptor { - - private val limiters = ConcurrentHashMap() - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - - if (!shouldLimit(request.url)) { - return chain.proceed(request) - } - - val host = request.url.host - val limiter = limiters.computeIfAbsent(host) { - RateLimiter(permits, period.inWholeMilliseconds) - } - - val timestamp = limiter.acquire( - call = chain.call(), - url = request.url.toString(), - ) - val response = chain.proceed(request) - - if (response.networkResponse == null) { - limiter.release(timestamp) - } - - return response - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt deleted file mode 100644 index 58cfa1da2..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.koitharu.kotatsu.parsers.network - -public object UserAgents { - - public const val CHROME_MOBILE: String = - "Mozilla/5.0 (Linux; Android 16) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36" - - public const val FIREFOX_MOBILE: String = - "Mozilla/5.0 (Android 16; Mobile; LG-M255; rv:146.0) Gecko/146.0 Firefox/146.0" - - /* Fallback: Set Linux as default for "DESKTOP" User-Agent */ - - public const val CHROME_DESKTOP: String = - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.59 Safari/537.36" - - public const val FIREFOX_DESKTOP: String = "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0" - - public const val CHROME_WINDOWS: String = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" - - public const val FIREFOX_WINDOWS: String = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0" - - public const val KOTATSU: String = "Kotatsu/9.0 (Android 16;;; en)" - - public const val LEGACY_KOTATSU: String = "Kotatsu/6.8 (Android 13;;; en)" -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt deleted file mode 100644 index 8b243714e..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt +++ /dev/null @@ -1,117 +0,0 @@ -package org.koitharu.kotatsu.parsers.network - -import okhttp3.Headers -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Response -import org.json.JSONObject - -public interface WebClient { - - /** - * Do a GET http request to specific url - * @param url - */ - public suspend fun httpGet(url: String): Response = httpGet(url.toHttpUrl()) - - public suspend fun httpGet(url: String, extraHeaders: Headers?): Response = httpGet(url.toHttpUrl(), extraHeaders) - - /** - * Do a GET http request to specific url - * @param url - */ - public suspend fun httpGet(url: HttpUrl): Response = httpGet(url, null) - - /** - * Do a GET http request to specific url - * @param url - * @param extraHeaders additional HTTP headers for request - */ - public suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response - - /** - * Do a HEAD http request to specific url - * @param url - */ - public suspend fun httpHead(url: String): Response = httpHead(url.toHttpUrl()) - - /** - * Do a HEAD http request to specific url - * @param url - */ - public suspend fun httpHead(url: HttpUrl): Response - - /** - * Do a POST http request to specific url with `multipart/form-data` payload - * @param url - * @param form payload as key=>value map - */ - public suspend fun httpPost(url: String, form: Map): Response = - httpPost(url.toHttpUrl(), form, null) - - /** - * Do a POST http request to specific url with `multipart/form-data` payload - * @param url - * @param form payload as key=>value map - */ - public suspend fun httpPost(url: HttpUrl, form: Map): Response = httpPost(url, form, null) - - /** - * Do a POST http request to specific url with `multipart/form-data` payload - * @param url - * @param form payload as key=>value map - * @param extraHeaders additional HTTP headers for request - */ - public suspend fun httpPost(url: HttpUrl, form: Map, extraHeaders: Headers?): Response - - /** - * Do a POST http request to specific url with `multipart/form-data` payload - * @param url - * @param payload payload as `key=value` string with `&` separator - */ - public suspend fun httpPost(url: String, payload: String): Response = httpPost(url.toHttpUrl(), payload, null) - - /** - * Do a POST http request to specific url with `multipart/form-data` payload - * @param url - * @param payload payload as `key=value` string with `&` separator - */ - public suspend fun httpPost(url: HttpUrl, payload: String): Response = httpPost(url, payload, null) - - /** - * Do a POST http request to specific url with `multipart/form-data` payload - * @param url - * @param payload payload as `key=value` string with `&` separator - * @param extraHeaders additional HTTP headers for request - */ - public suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response - - /** - * Do a POST http request to specific url with json payload - * @param url - * @param body - */ - public suspend fun httpPost(url: String, body: JSONObject): Response = httpPost(url.toHttpUrl(), body, null) - - /** - * Do a POST http request to specific url with json payload - * @param url - * @param body - */ - public suspend fun httpPost(url: HttpUrl, body: JSONObject): Response = httpPost(url, body, null) - - /** - * Do a POST http request to specific url with json payload - * @param url - * @param body - * @param extraHeaders additional HTTP headers for request - */ - public suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response - - /** - * Do a GraphQL request to specific url - * @param endpoint an url - * @param query GraphQL request payload - */ - public suspend fun graphQLQuery(endpoint: String, query: String): JSONObject -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt deleted file mode 100644 index 0399df703..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.koitharu.kotatsu.parsers.network.utils - -import okhttp3.Call -import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions -import java.io.IOException -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock - -public class RateLimiter( - private val permits: Int, - private val periodMs: Long -) { - private val timestamps = ArrayDeque(permits) - private val lock = ReentrantLock(true) // fair lock - - public fun acquire(call: Call, url: String = ""): Long = lock.withLock { - val now = System.currentTimeMillis() - - while (timestamps.isNotEmpty() && now - timestamps.first() >= periodMs) { - timestamps.removeFirst() - } - - if (timestamps.size >= permits) { - if (call.isCanceled()) throw IOException("Canceled") - val oldestRequest = timestamps.first() - val waitTime = periodMs - (now - oldestRequest) - - if (waitTime > 0) { - throw TooManyRequestExceptions(url, waitTime) - } - - val current = System.currentTimeMillis() - while (timestamps.isNotEmpty() && current - timestamps.first() >= periodMs) { - timestamps.removeFirst() - } - } - - val timestamp = System.currentTimeMillis() - timestamps.addLast(timestamp) - return timestamp - } - - public fun release(timestamp: Long): Boolean = lock.withLock { - timestamps.remove(timestamp) - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt index 92b07651d..923057ac7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt @@ -76,14 +76,15 @@ internal class ComicKLive(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder() .host(domain) .addPathSegment("api") .addPathSegment("search") .addQueryParameter("type", "comic") - filter.query?.let { - url.addQueryParameter("q", filter.query) + query?.let { query -> + url.addQueryParameter("q", query) } filter.tags.forEach { @@ -154,7 +155,7 @@ internal class ComicKLive(context: MangaLoaderContext) : } // no "page" parameter, use "cursor" to prevent empty list when searching with keyword - if (filter.query.isNullOrEmpty() && page > 1) { + if (query.isNullOrEmpty() && page > 1) { url.addQueryParameter("cursor", pageCursor) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt index b8a143681..a91e9f84e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt @@ -301,7 +301,8 @@ internal class ExHentaiParser( override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) - if (response.headersContentLength(BANNED_RESPONSE_LENGTH) <= BANNED_RESPONSE_LENGTH) { + val contentLength = response.headersContentLength() + if (contentLength in 0L..BANNED_RESPONSE_LENGTH) { val text = response.peekBody(BANNED_RESPONSE_LENGTH).use { it.string() } if (text.contains("IP address has been temporarily banned", ignoreCase = true)) { val hours = Regex("([0-9]+) hours?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0 @@ -432,6 +433,7 @@ internal class ExHentaiParser( return null } val joiner = StringUtil.StringJoiner(" ") + val query = query if (!query.isNullOrEmpty()) { joiner.add(query) } @@ -456,6 +458,7 @@ internal class ExHentaiParser( joiner.append(lc.toLanguagePath()) joiner.append("\"$") } + val author = author if (!author.isNullOrEmpty()) { joiner.add("artist:\"") joiner.append(author) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt index 5807f9508..7f336f6ef 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt @@ -125,8 +125,10 @@ internal class HitomiLaParser(context: MangaLoaderContext) : AbstractMangaParser private var cachedSearchIds: List = emptyList() - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List = when { - filter.query.isNullOrEmpty() -> { + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + return when { + query.isNullOrEmpty() -> { if (filter.tags.isEmpty()) { when (order) { @@ -185,11 +187,12 @@ internal class HitomiLaParser(context: MangaLoaderContext) : AbstractMangaParser else -> { if (offset == 0) { - cachedSearchIds = hitomiSearch(filter.query, order).toList() + cachedSearchIds = hitomiSearch(query, order).toList() } cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size)) } }.toMangaList() + } private fun Int.nextOffsetRange(): LongRange { val bytes = this * 4L diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt index df890bdb8..ffe658eb3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt @@ -59,15 +59,16 @@ internal class ImHentai(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/search/?page=") append(page.toString()) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("&key=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt index 8c8ad3368..73e50fdcd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt @@ -80,6 +80,8 @@ internal class Koharu(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val baseUrl = "https://$apiSuffix/books" val url = buildString { append(baseUrl) @@ -88,8 +90,8 @@ internal class Koharu(context: MangaLoaderContext) : val includedTags: MutableList = mutableListOf() val excludedTags: MutableList = mutableListOf() - if (!filter.query.isNullOrEmpty() && filter.query.startsWith("id:")) { - val ipk = filter.query.removePrefix("id:") + if (!query.isNullOrEmpty() && query.startsWith("id:")) { + val ipk = query.removePrefix("id:") val response = webClient.httpGet("$baseUrl/detail/$ipk").parseJson() return listOf(parseMangaDetail(response)) } @@ -105,18 +107,18 @@ internal class Koharu(context: MangaLoaderContext) : } append("?sort=").append(sortValue) - if (!filter.query.isNullOrEmpty()) { - terms.add("title:\"${filter.query.urlEncoded()}\"") + if (!query.isNullOrEmpty()) { + terms.add("title:\"${query.urlEncoded()}\"") } - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { val authors = authorsIds.getOrDefault(emptyMap()) - val authorId = authors[filter.author.lowercase()] + val authorId = authors[author.lowercase()] if (authorId != null) { includedTags.add(authorId) } else { - terms.add("artist:\"${filter.author.urlEncoded()}\"") + terms.add("artist:\"${author.urlEncoded()}\"") } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt index 2922d7594..10a2944d9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt @@ -145,9 +145,10 @@ internal abstract class LineWebtoonsParser( } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val manga = when { - !filter.query.isNullOrEmpty() -> { - makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20") + !query.isNullOrEmpty() -> { + makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20") .getJSONObject("challengeSearch") .getJSONArray("titleList") .mapJSON { jo -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt index 03e93b6ab..f23706bd8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt @@ -176,6 +176,7 @@ internal abstract class MangaFireParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("/filter") @@ -188,12 +189,12 @@ internal abstract class MangaFireParser( append(siteLang) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { // Keyword + vrf append("&keyword=") - append(encodeKeyword(filter.query)) + append(encodeKeyword(query)) append("&vrf=") - append(VrfGenerator.generate(filter.query)) + append(VrfGenerator.generate(query)) append("&sort=") append(when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt index d4abb2175..1d85350b0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt @@ -110,14 +110,15 @@ internal class MangaPark(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/search?page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&word=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } append("&genres=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt index 407440a2b..60b9aa26b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt @@ -62,8 +62,9 @@ internal abstract class MangaPlusParser( private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString()) override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - filter.query.isNullOrEmpty() -> { + query.isNullOrEmpty() -> { when (order) { SortOrder.POPULARITY -> getPopularList() SortOrder.UPDATED -> getLatestList() @@ -71,7 +72,7 @@ internal abstract class MangaPlusParser( } } - else -> getAllTitleList(filter.query) + else -> getAllTitleList(query) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt index c906953bd..da0900a81 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt @@ -88,11 +88,12 @@ internal class MangaReaderToParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = "https://$domain".toHttpUrl().newBuilder().apply { when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { addPathSegment("search") - addQueryParameter("keyword", filter.query) + addQueryParameter("keyword", query) addQueryParameter("page", page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt index 780077e04..df7f9c1d5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt @@ -37,16 +37,17 @@ internal class Manhwa210(context: MangaLoaderContext) : PagedMangaParser(context ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search") append("?filter[name]=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (page > 1) { append("&page=") @@ -93,7 +94,7 @@ internal class Manhwa210(context: MangaLoaderContext) : PagedMangaParser(context } } - if (filter.query.isNullOrEmpty()) { + if (query.isNullOrEmpty()) { append("&sort=") when (order) { SortOrder.POPULARITY -> append("-views") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt index 3ff234ba0..5175b7a7f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt @@ -29,14 +29,15 @@ internal class Misskon(context: MangaLoaderContext) : PagedMangaParser(context, override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/page/$page/") append("?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } order == SortOrder.POPULARITY -> { append("/top3/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt index a252137fb..11f708a2a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt @@ -52,13 +52,14 @@ internal class Multporn(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?search_api_views_fulltext=") - val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part -> + val encodedQuery = query.splitByWhitespace().joinToString(separator = "+") { part -> part.urlEncoded() } append(encodedQuery) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt index 2ea1bd94f..163f5aee6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt @@ -151,6 +151,7 @@ internal class MyReadingManga(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -171,8 +172,8 @@ internal class MyReadingManga(context: MangaLoaderContext) : // fix order append("&s=") - if (!filter.query.isNullOrEmpty()) { - append(filter.query.splitByWhitespace().joinToString(separator = "+")) + if (!query.isNullOrEmpty()) { + append(query.splitByWhitespace().joinToString(separator = "+")) } val langSlug = getLanguageSlug(filter.locale) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt index 98016c77c..9804ce7b7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt @@ -68,18 +68,19 @@ internal abstract class NineMangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) - if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty() || !filter.query.isNullOrEmpty()) { + if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty() || !query.isNullOrEmpty()) { append("/search/") append("?page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&name_sel=contain&wd=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } append("&category_id=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt index 4a846af76..2f92feeb2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt @@ -113,14 +113,15 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - return if (filter.query.isNullOrEmpty()) { + val query = filter.query + return if (query.isNullOrEmpty()) { if (filter.tags.isEmpty() && order == SortOrder.POPULARITY) { getPopularList(page, filter.locale) } else { getSearchList(page, null, filter.locale, filter.tags, order) } } else { - getSearchList(page, filter.query, null, null, order) + getSearchList(page, query, null, null, order) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt index a8cee699a..cc4b7e8cd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt @@ -181,9 +181,10 @@ internal abstract class WebtoonsParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val document = when { - !filter.query.isNullOrEmpty() -> { - val searchUrl = "https://$domain/$languageCode/search?keyword=${filter.query.urlEncoded()}" + !query.isNullOrEmpty() -> { + val searchUrl = "https://$domain/$languageCode/search?keyword=${query.urlEncoded()}" webClient.httpGet(searchUrl).parseHtml() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt index f6f19736e..108ecbc46 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt @@ -157,6 +157,8 @@ internal class WeebDex(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = urlBuilder() .host("api.$domain") .addPathSegment("manga") @@ -195,8 +197,8 @@ internal class WeebDex(context: MangaLoaderContext) : } // Keyword - if (!filter.query.isNullOrEmpty()) { - url.addQueryParameter("title", filter.query.urlEncoded()) + if (!query.isNullOrEmpty()) { + url.addQueryParameter("title", query.urlEncoded()) } // Content rating @@ -284,8 +286,8 @@ internal class WeebDex(context: MangaLoaderContext) : } // Author + Artist search - if (!filter.author.isNullOrBlank()) { - url.addQueryParameter("authorOrArtist", filter.author.substringAfter("-").trim()) + if (!author.isNullOrBlank()) { + url.addQueryParameter("authorOrArtist", author.substringAfter("-").trim()) } val js = webClient.httpGet(url.build()).parseJson() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt index 7a3bed07d..390bddaf9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt @@ -56,6 +56,7 @@ internal abstract class AnimeBootstrapParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -64,9 +65,9 @@ internal abstract class AnimeBootstrapParser( append(page.toString()) append("&type=all") - filter.query?.let { + query?.let { query -> append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt index 9496dab80..5eccd6a81 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt @@ -29,6 +29,7 @@ internal class PapScan(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -36,9 +37,9 @@ internal class PapScan(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("&alpha=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt index 491443b6c..13603b03c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt @@ -55,11 +55,12 @@ internal class AzoraMoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val all = allPostsCache.get() var filtered: List = all - if (!filter.query.isNullOrEmpty()) { - val q = filter.query.lowercase(sourceLocale) + if (!query.isNullOrEmpty()) { + val q = query.lowercase(sourceLocale) filtered = filtered.filter { p -> p.getStringOrNull("postTitle")?.lowercase(sourceLocale)?.contains(q) == true || p.getStringOrNull("alternativeTitles")?.lowercase(sourceLocale)?.contains(q) == true || diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt index 68b0a16f1..612752790 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt @@ -48,14 +48,15 @@ internal class FlixScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val json = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } val url = "https://api.$domain/api/v1/search/serie" val body = JSONObject() - body.put("title", filter.query.urlEncoded()) + body.put("title", query.urlEncoded()) webClient.httpPost(url, body).parseJson().getJSONArray("data") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt index df6711751..80d82a598 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt @@ -77,11 +77,12 @@ internal class HentaMan(context: MangaLoaderContext) : PagedMangaParser( private val baseUrl get() = "https://$domain" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val searchUrl = "$baseUrl/manga/search".toHttpUrl().newBuilder() - .addQueryParameter("query", filter.query) + .addQueryParameter("query", query) .build() val response = webClient.httpGet(searchUrl).parseJson() val results = response.optJSONArray("results") ?: return emptyList() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt index 9548e36d9..9167055fb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt @@ -33,15 +33,16 @@ internal class MangaStorm(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/mangas?page=") append(page) append("&query=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt index ff672dedd..9eb2b7585 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt @@ -48,13 +48,14 @@ internal class TeamXNovel(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt index dc214df01..b3bc9d354 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt @@ -43,10 +43,11 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val filters = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { return if (offset == 0) { - search(filter.query) + search(query) } else { emptyList() } @@ -89,7 +90,7 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c id = generateUid(mediaId), title = title.getString("be"), coverUrl = jo.getString("poster").removePrefix("/cdn") - .toAbsoluteUrl(getDomain("cdn")) + "?width=200&height=280", + .toAbsoluteUrl(getDomain(this, "cdn")) + "?width=200&height=280", altTitles = setOfNotNull(title.optJSONArray("alt")?.optString(0)?.nullIfEmpty()), authors = emptySet(), contentRating = null, @@ -128,7 +129,7 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c """.trimIndent(), ).getJSONObject("media") val title = details.getJSONObject("title") - val poster = details.getString("poster").removePrefix("/cdn").toAbsoluteUrl(getDomain("cdn")) + val poster = details.getString("poster").removePrefix("/cdn").toAbsoluteUrl(getDomain(this, "cdn")) val chapters = apiCall( """ chapters(mediaId: "${details.getString("mediaId")}", offset: 0, limit: 99999) { @@ -236,7 +237,7 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c id = generateUid(mediaId), title = title.getString("be"), coverUrl = jo.getString("poster").removePrefix("/cdn") - .toAbsoluteUrl(getDomain("cdn")) + "?width=200&height=280", + .toAbsoluteUrl(getDomain(this, "cdn")) + "?width=200&height=280", altTitles = setOfNotNull(title.getString("en").nullIfEmpty()), authors = emptySet(), contentRating = null, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt index 6e6040877..0d7c1a7b0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt @@ -38,13 +38,14 @@ internal abstract class CupFoxParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append('/') append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt index 14541e235..973cae657 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt @@ -64,15 +64,17 @@ internal class AsuraScansParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://") append(domain) append("/browse?page=") append(page) - filter.query?.let { + query?.let { query -> append("&q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (filter.tags.isNotEmpty()) { @@ -105,9 +107,9 @@ internal class AsuraScansParser(context: MangaLoaderContext) : ) } - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { append("&author=") - append(filter.author.urlEncoded()) + append(author.urlEncoded()) } append( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt index 888a11f3e..bed1e5094 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt @@ -30,9 +30,10 @@ internal class Atsumaru(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query // Case 1: Search (Uses a different API endpoint) - if (!filter.query.isNullOrEmpty()) { - return getSearchPage(page, filter.query) + if (!query.isNullOrEmpty()) { + return getSearchPage(page, query) } // Case 2: Popular / Latest (Uses the infinite API) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt index 72bc1dc38..00eb0231b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt @@ -47,10 +47,11 @@ internal class BatCave(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val urlBuilder = StringBuilder() when { - !filter.query.isNullOrEmpty() -> { - val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "%20") { part -> + !query.isNullOrEmpty() -> { + val encodedQuery = query.splitByWhitespace().joinToString(separator = "%20") { part -> part.urlEncoded() } urlBuilder.append("/search/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt index 79e2b07d8..81727b6ee 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt @@ -32,16 +32,17 @@ internal class BeeToon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt index 8efda3622..b824d454f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt @@ -39,14 +39,15 @@ internal class ComicExtra(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/") when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("search?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (page > 1) { append("&page=") append(page.toString()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt index 8a9ca468c..060082bc5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt @@ -35,12 +35,13 @@ internal class DemonicScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val hasQuery = !filter.query.isNullOrEmpty() + val query = filter.query + val hasQuery = !query.isNullOrEmpty() val isNewest = order == SortOrder.NEWEST && !hasQuery && filter.tags.isEmpty() val isAlpha = order == SortOrder.ALPHABETICAL && !hasQuery && filter.tags.isEmpty() val isAlphaDesc = order == SortOrder.ALPHABETICAL_DESC && !hasQuery && filter.tags.isEmpty() val url = when { - hasQuery -> "https://$domain/search.php?manga=" + (filter.query ?: "") + hasQuery -> "https://$domain/search.php?manga=" + (query ?: "") isNewest -> "https://$domain/lastupdates.php?list=$page" isAlpha -> buildString { append("https://$domain/advanced.php") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt index d9cad6813..ff800e901 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt @@ -42,13 +42,14 @@ internal class DynastyScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&") append("classes[]=Series&page=") append(page.toString()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt index 2aaa3ed42..44f5a0ff8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt @@ -44,13 +44,14 @@ internal class FanFoxParser(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?name=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt index b59c7e495..5907bdacd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt @@ -37,6 +37,7 @@ internal class FlameComics(context: MangaLoaderContext) : override val configKeyDomain = ConfigKey.Domain("flamecomics.xyz") override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder() .addPathSegment("_next") .addPathSegment("data") @@ -51,8 +52,8 @@ internal class FlameComics(context: MangaLoaderContext) : } // Filter by search if provided - val filteredManga = if (!filter.query.isNullOrEmpty()) { - val normalizedQuery = removeSpecialCharsRegex.replace(filter.query.lowercase(), "") + val filteredManga = if (!query.isNullOrEmpty()) { + val normalizedQuery = removeSpecialCharsRegex.replace(query.lowercase(), "") allManga.filter { manga -> val titles = mutableListOf(manga.title) titles.addAll(manga.altTitles) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt index ebaa729d8..06f8598e0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt @@ -48,8 +48,9 @@ internal class FlixScansOrg(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val json = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt index 752232913..5b2c2bb74 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt @@ -60,17 +60,19 @@ internal class Hentalk(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() // not found any URLs for it override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://") append(domain) append("/__data.json?x-sveltekit-trailing-slash=1&x-sveltekit-invalidated=001") when { - !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || !filter.author.isNullOrEmpty() -> { + !query.isNullOrEmpty() || filter.tags.isNotEmpty() || !author.isNullOrEmpty() -> { append("&q=") - if (!filter.author.isNullOrEmpty()) { - append("artist:\"${space2plus(filter.author)}\"") + if (!author.isNullOrEmpty()) { + append("artist:\"${space2plus(author)}\"") append('+') } @@ -81,8 +83,8 @@ internal class Hentalk(context: MangaLoaderContext) : } } - if (!filter.query.isNullOrEmpty()) { - append(space2plus(filter.query)) + if (!query.isNullOrEmpty()) { + append(space2plus(query)) } else { append('+') } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt index 0f85c2a30..0edfad20c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt @@ -65,11 +65,12 @@ internal class HeyToonParser(context: MangaLoaderContext) : private val headers = Headers.headersOf("X-Requested-With", "XMLHttpRequest") override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) return emptyList() - val url = "https://$domain/api/complete-search?keyword=${filter.query.urlEncoded()}" + val url = "https://$domain/api/complete-search?keyword=${query.urlEncoded()}" val response = webClient.httpGet(url, headers).parseJsonArray() response.mapJSON { comic -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt index 21896c626..242cd82cf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt @@ -76,11 +76,12 @@ internal class KdtScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://$domain/manga/?page=${page}") - filter.query?.let { - append("&s=${it.urlEncoded()}") + query?.let { q -> + append("&s=${q.urlEncoded()}") } val sortValue = when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt index 7c48e05a2..fff414093 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt @@ -37,7 +37,8 @@ internal class MangaGeko(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val isSearch = !filter.query.isNullOrEmpty() + val query = filter.query + val isSearch = !query.isNullOrEmpty() val url = buildString { append("https://") append(domain) @@ -47,7 +48,7 @@ internal class MangaGeko(context: MangaLoaderContext) : return emptyList() } append("/search/?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt index dd2f28693..16eff95aa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt @@ -41,13 +41,14 @@ internal class MangaHomeParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?name=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt index ef517b99b..e0a66404a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt @@ -41,13 +41,14 @@ internal class MangaKatana(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = when { - !filter.query.isNullOrEmpty() -> buildString { + !query.isNullOrEmpty() -> buildString { append("https://") append(domain) if (page > 1) append("/page/").append(page) append("/?search=1&q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> buildString { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt index 38b694f9c..0d7c01858 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt @@ -39,13 +39,14 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?query=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&search_type=manga&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt index 74f978881..301dffd5c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt @@ -41,8 +41,9 @@ internal class MangaPill(context: MangaLoaderContext) : PagedMangaParser(context ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query // bruh - val url = if (filter.types.isNotEmpty() || filter.states.isNotEmpty() || !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty()) { + val url = if (filter.types.isNotEmpty() || filter.states.isNotEmpty() || !query.isNullOrEmpty() || filter.tags.isNotEmpty()) { buildString { append("/search") append("?type=") @@ -66,9 +67,9 @@ internal class MangaPill(context: MangaLoaderContext) : PagedMangaParser(context } } ?: "") - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt index 4e82318dd..2b0aa9edd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt @@ -42,13 +42,14 @@ internal class MangaTownParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?name=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } @@ -260,7 +261,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : } private suspend fun bypassLicensedChapters(manga: Manga): List { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain("m"))).parseHtml() + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain(this, "m"))).parseHtml() val list = doc.body().selectFirst("ul.detail-ch-list") ?: return emptyList() val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US) return list.select("li").asReversed().mapIndexedNotNull { i, li -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt index 9d9ed76a4..2b95f5fe6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt @@ -41,6 +41,7 @@ internal class MangaGo(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val sortParam = when (order) { SortOrder.UPDATED -> "s=1" SortOrder.POPULARITY -> "s=9" @@ -49,8 +50,8 @@ internal class MangaGo(context: MangaLoaderContext) : else -> "s=1" } - val url = if (!filter.query.isNullOrEmpty()) { - "https://$domain/r/l_search/?name=${filter.query.urlEncoded()}&page=$page" + val url = if (!query.isNullOrEmpty()) { + "https://$domain/r/l_search/?name=${query.urlEncoded()}&page=$page" } else { val genre = filter.tags.firstOrNull()?.key ?: "All" "https://$domain/genre/$genre/$page/?$sortParam" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt index 38c56a34a..14cb37510 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt @@ -51,13 +51,14 @@ internal class Mangaowl(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/10-search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt index 41a3356f5..91efcb146 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt @@ -57,15 +57,16 @@ internal class Manhwa18Com(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/tim-kiem?page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } append("&accept_genres=") @@ -108,10 +109,10 @@ internal class Manhwa18Com(context: MangaLoaderContext) : } // Support author - // filter.author.let{ + // author.let{ // the // append("&artist=") - // append(filter.author) + // append(author) // } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt index 0e5c89f0c..b32004d07 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt @@ -87,15 +87,16 @@ internal class Manhwa18Parser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/tim-kiem?page=") append(page.toString()) - filter.query?.let { + query?.let { q -> append("&q=") - append(it.urlEncoded()) + append(q.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt index 944f67120..1442c8aa1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt @@ -39,6 +39,7 @@ internal class ManhwasMen(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -46,9 +47,9 @@ internal class ManhwasMen(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt index cdd6c3ba9..a0829fc9c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt @@ -37,13 +37,14 @@ internal class MyComicList(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/comic-search?key=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } filter.tags.isNotEmpty() -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt index d39740dbb..727c4e081 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt @@ -31,13 +31,14 @@ internal class Po2Scans(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/series") - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt index 6136d3c47..ebccdfa08 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt @@ -42,15 +42,16 @@ internal class Pururin(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/search?tag_condition=contains&page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt index 5a481b9c0..b2a2a971c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt @@ -52,9 +52,10 @@ internal class RoliaScan(context: MangaLoaderContext) : PagedMangaParser(context ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val body = JSONObject().apply { put("page", page) - put("search", filter.query.orEmpty()) + put("search", query.orEmpty()) put("years", "[]") put("genres", buildJsonArrayString(filter.tags.map { it.key })) put( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt index 08922dd62..6895055b9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt @@ -36,8 +36,9 @@ internal class VioletScans(context: MangaLoaderContext) : override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - return if (!filter.query.isNullOrEmpty()) { - scrapeSearchList(filter.query, page) + val query = filter.query + return if (!query.isNullOrEmpty()) { + scrapeSearchList(query, page) } else { scrapeNonSearchList(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt index 7307997dd..6e80645f5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt @@ -46,19 +46,20 @@ internal class VyManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/search?search_po=0&q=") - filter.query?.let { - append(filter.query.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } append("&author_po=0&author=") - // filter.author?.let { - // append(filter.author.urlEncoded()) + // author?.let { author -> + // append(author.urlEncoded()) // } append("&completed=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt index e7b14db89..a18a2e8bd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt @@ -131,15 +131,16 @@ internal class WeebCentral(context: MangaLoaderContext) : AbstractMangaParser(co } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = "https://$domain/search/data".toHttpUrl().newBuilder().apply { addQueryParameter("limit", "32") addQueryParameter("offset", offset.toString()) - filter.query?.let { - val query = it + query?.let { q -> + val normalized = q .replace(Regex("""[^a-zA-Z0-9\s]"""), " ") .replace(Regex("""\s+"""), " ") .trim() - addQueryParameter("text", query) + addQueryParameter("text", normalized) } addQueryParameter( name = "sort", diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt index 351d11171..641fbecc5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt @@ -58,9 +58,10 @@ internal class Comix(context: MangaLoaderContext) : // List / Search // ------------------------- override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val builder = "$apiBaseUrl/manga".toHttpUrl().newBuilder().apply { - if (!filter.query.isNullOrBlank()) { - addQueryParameter("keyword", filter.query) + if (!query.isNullOrBlank()) { + addQueryParameter("keyword", query) } val (param, dir) = when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt index 595ed3812..c25397e8d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt @@ -50,6 +50,7 @@ internal class HouseMangasParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt index 967e8697e..ee32dde90 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt @@ -105,8 +105,9 @@ internal class InMangaParser(context: MangaLoaderContext) : PagedMangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val genres = filter.tags.map { it.key }.toSet() - val formData = buildFormData(page, order, filter.query ?: "", genres) + val formData = buildFormData(page, order, query ?: "", genres) val response = webClient.httpPost( "https://$domain/manga/getMangasConsultResult".toHttpUrl(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt index a26c3f7fc..dea224550 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt @@ -69,15 +69,16 @@ internal class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaPars ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/library?_pg=1&page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&title=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } append("&order_item=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt index 05bd57148..ffd5e5aae 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt @@ -82,6 +82,7 @@ internal class ZonaTmoParser(context: MangaLoaderContext) : PagedMangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append(apiUrl) append("/listing/manga?page=").append(page) @@ -98,7 +99,7 @@ internal class ZonaTmoParser(context: MangaLoaderContext) : PagedMangaParser( ) append("&order=").append(if (order == SortOrder.ALPHABETICAL) "asc" else "desc") - filter.query?.takeIf { it.isNotEmpty() }?.let { + query?.takeIf { it.isNotEmpty() }?.let { append("&search=").append(it.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt index 4fcad1433..3165ff82f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt @@ -82,6 +82,8 @@ internal abstract class FmreaderParser( protected open val tagPrefix = "manga-list-genre-" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://") append(domain) @@ -89,14 +91,14 @@ internal abstract class FmreaderParser( append("?page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&name=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } - // filter.author?.let { + // author?.let { author -> // append("&author=") - // append(filter.author.urlEncoded()) + // append(author.urlEncoded()) // } append("&genre=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt index 54827a864..91ccbc05d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt @@ -45,8 +45,9 @@ internal abstract class FoolSlideParser( override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val doc = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } @@ -58,7 +59,7 @@ internal abstract class FoolSlideParser( append(searchUrl) } - webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml() + webClient.httpPost(url, "search=${query.urlEncoded()}").parseHtml() } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt index e4bf51430..bdee74582 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt @@ -65,10 +65,11 @@ internal class AnimeSama(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = "$baseUrl/catalogue".toHttpUrl().newBuilder() // keyword - val encodedQuery = filter.query?.splitByWhitespace()?.joinToString(separator = "+") { part -> + val encodedQuery = query?.splitByWhitespace()?.joinToString(separator = "+") { part -> part.urlEncoded() }.orEmpty() url.addQueryParameter("search", encodedQuery) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt index b6324bf07..ac4165e9b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt @@ -138,6 +138,8 @@ internal class AstralManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val all = getFilteredAndSortedList(order, filter) val from = ((page - 1) * pageSize).coerceAtLeast(0) if (from >= all.size) return emptyList() @@ -222,9 +224,10 @@ internal class AstralManga(context: MangaLoaderContext) : } continue } - if (isCoverUrlFresh(manga.coverUrl, now)) { - if (slug.isNotBlank() && !manga.coverUrl.isNullOrBlank()) { - putCoverCache(slug, manga.coverUrl, now) + val coverUrl = manga.coverUrl + if (isCoverUrlFresh(coverUrl, now)) { + if (slug.isNotBlank() && !coverUrl.isNullOrBlank()) { + putCoverCache(slug, coverUrl, now) } continue } @@ -630,11 +633,15 @@ internal class AstralManga(context: MangaLoaderContext) : tags = (first.manga.tags + second.manga.tags).toSet(), state = first.manga.state ?: second.manga.state, authors = (first.manga.authors + second.manga.authors).toSet(), - description = when { - first.manga.description.isNullOrBlank() -> second.manga.description - second.manga.description.isNullOrBlank() -> first.manga.description - first.manga.description.length >= second.manga.description.length -> first.manga.description - else -> second.manga.description + description = run { + val d1 = first.manga.description + val d2 = second.manga.description + when { + d1.isNullOrBlank() -> d2 + d2.isNullOrBlank() -> d1 + d1.length >= d2.length -> d1 + else -> d2 + } }, ) return MangaCache( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt index 5d99b2bf2..6bcfd2cd0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt @@ -56,13 +56,14 @@ internal class BentomangaParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder() .host(domain) .addPathSegment("manga_list") .addQueryParameter("limit", page.toString()) - filter.query?.let { - url.addQueryParameter("search", filter.query) + query?.let { query -> + url.addQueryParameter("search", query) } when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt index 89082290b..f949bf861 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt @@ -77,16 +77,18 @@ internal class BigSolo(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val catalog = getCatalog() - val query = normalizeSearchText(filter.query) + val normalizedQuery = normalizeSearchText(query) val includeTags = filter.tags.mapTo(hashSetOf()) { normalizeTagKey(it.key) } val excludeTags = filter.tagsExclude.mapTo(hashSetOf()) { normalizeTagKey(it.key) } val stateFilter = filter.states val typesFilter = filter.types val demographicFilter = filter.demographics - val authorFilter = normalizeSearchText(filter.author) + val authorFilter = normalizeSearchText(author) val filtered = catalog.entries.filter { entry -> - if (query.isNotEmpty() && query !in entry.searchText) { + if (normalizedQuery.isNotEmpty() && normalizedQuery !in entry.searchText) { return@filter false } if (authorFilter.isNotEmpty() && authorFilter !in entry.authorsSearchText) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt index 9296d7cc2..d5a456faa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt @@ -44,11 +44,12 @@ internal class FuryoSociety(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt index 6df551b18..408739f8c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt @@ -80,9 +80,10 @@ internal class InovaScanManga(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildApiUrl( - search = filter.query.orEmpty(), + search = query.orEmpty(), sort = when (order) { SortOrder.POPULARITY -> "popular" SortOrder.NEWEST -> "new" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt index d79b47609..c7e4a637c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt @@ -45,17 +45,18 @@ internal class LegacyScansParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val end = page * pageSize val start = end - (pageSize - 1) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } val url = buildString { append("https://api.$domain/misc/home/search?title=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } return parseMangaListQuery(webClient.httpGet(url).parseJson()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt index 68dda205a..d5b21d574 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt @@ -33,12 +33,13 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context, } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val doc = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } - val q = filter.query.urlEncoded().replace("%20", "+") + val q = query.urlEncoded().replace("%20", "+") val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q" webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt index 42f41a23d..89420199b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt @@ -61,8 +61,9 @@ internal class LugnicaScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt index 0db768097..13398e676 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt @@ -39,13 +39,14 @@ internal class MangaKawaii(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?query=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&search_type=manga&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt index b79a3638f..209457bb4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt @@ -55,16 +55,17 @@ internal class MangaMana(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val postData = buildString { append("page=") append(page) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } val domainCdn = "cdn" + domain.removePrefix("www") - val url = "https://$domain/search-live?q=${filter.query.urlEncoded()}" + val url = "https://$domain/search-live?q=${query.urlEncoded()}" val json = webClient.httpGet(url).parseJsonArray() return json.mapJSON { jo -> val slug = jo.getString("slug") ?: throw ParseException("Missing Slug", url) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt index 01f1eb02c..320679044 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt @@ -63,6 +63,7 @@ internal class MangaMoins(context: MangaLoaderContext) : override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = HttpUrl.Builder() .scheme("https") .host(domain) @@ -70,7 +71,7 @@ internal class MangaMoins(context: MangaLoaderContext) : .addQueryParameter("page", page.toString()) .addQueryParameter("limit", API_LIMIT.toString()) .apply { - filter.query?.trim()?.takeIf { it.isNotEmpty() }?.let { q -> + query?.trim()?.takeIf { it.isNotEmpty() }?.let { q -> addQueryParameter("q", q) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt index b5862cfaa..d8111c2c7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt @@ -149,6 +149,7 @@ internal class OrtegaScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val all = getFilteredAndSortedList(order, filter) val from = ((page - 1) * pageSize).coerceAtLeast(0) if (from >= all.size) return emptyList() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt index c76bf76e4..a869bdbd3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt @@ -74,18 +74,19 @@ internal class PhenixscansParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/api/front/manga") when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } append("/search?query=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt index 7b3c0828e..94b8e7b78 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt @@ -143,6 +143,8 @@ internal class PoseidonScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val all = getFilteredAndSortedList(order, filter) val fromIndex = ((page - 1) * pageSize).coerceAtLeast(0) if (fromIndex >= all.size) { @@ -536,9 +538,10 @@ internal class PoseidonScans(context: MangaLoaderContext) : } override suspend fun getDetails(manga: Manga): Manga { - if (!manga.chapters.isNullOrEmpty()) { + val chapters = manga.chapters + if (!chapters.isNullOrEmpty()) { return manga.copy( - chapters = normalizeChapterOrder(manga.chapters), + chapters = normalizeChapterOrder(chapters), ) } @@ -896,7 +899,7 @@ internal class PoseidonScans(context: MangaLoaderContext) : if (dateString.isNullOrBlank()) return 0L val cleanedDateString = - dateString.removePrefix("\"").removeSuffix("\"").removePrefix($$"$D").removePrefix("D").trim() + dateString.removePrefix("\"").removeSuffix("\"").removePrefix("D").trim() return isoDateFormat.parseSafe(cleanedDateString) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt index f2df36b3c..b19d777e8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt @@ -103,8 +103,10 @@ internal class PunkRecordz(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val filtered = filterManga(getCatalogManga(), filter) - val sorted = sortManga(filtered, order, filter.query) + val sorted = sortManga(filtered, order, query) val fromIndex = ((page - 1) * pageSize).coerceAtLeast(0) if (fromIndex >= sorted.size) { return emptyList() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt index 148146060..c1d376e50 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt @@ -44,13 +44,14 @@ internal class ScansMangasMe(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&post_type=manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt index d7a46d2ed..7ddda9aee 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt @@ -39,15 +39,16 @@ internal class ScantradUnion(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt index 5d0853367..7cb1d6ce5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt @@ -91,6 +91,7 @@ internal abstract class FuzzyDoodleParser( protected open val comicsValue = "bande-dessinee" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -110,9 +111,9 @@ internal abstract class FuzzyDoodleParser( ) } - filter.query?.let { + query?.let { query -> append("&title=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } append("&status=") filter.states.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt index a55fb9ce7..82244f167 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt @@ -33,9 +33,10 @@ internal abstract class GalleryParser( override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder().apply { when { - !filter.query.isNullOrEmpty() -> addQueryParameter("search", filter.query) + !query.isNullOrEmpty() -> addQueryParameter("search", query) filter.tags.isNotEmpty() -> addPathSegments(filter.tags.first().key) order == SortOrder.POPULARITY -> addPathSegment("hot") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt index e83d997a7..53f80ad69 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt @@ -60,13 +60,14 @@ internal abstract class GalleryAdultsParser( order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&") } @@ -80,9 +81,9 @@ internal abstract class GalleryAdultsParser( append("/tag/") append(tag.key) append("/?") - } else if (filter.locale != null) { + } else if (lang != null) { append("/language/") - append(filter.locale.toLanguagePath()) + append(lang.toLanguagePath()) append("/?") } else { append("/?") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt index 4a5254cef..a2bcdb3d4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt @@ -44,37 +44,40 @@ internal class Hentai3(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } else -> { - if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { + val tags = filter.tags + val lang = filter.locale + if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { append("/search?q=") - append(buildQuery(filter.tags, filter.locale)) + append(buildQuery(tags, lang)) if (order == SortOrder.POPULARITY) { append("&sort=popular") } append("&page=") append(page.toString()) - } else if (filter.locale != null) { + } else if (lang != null) { append("/language/") - append(filter.locale.toLanguagePath()) + append(lang.toLanguagePath()) append("/") append(page.toString()) if (order == SortOrder.POPULARITY) { append("?sort=popular") } - } else if (filter.tags.isNotEmpty()) { - filter.tags.oneOrThrowIfMany()?.let { + } else if (tags.isNotEmpty()) { + tags.oneOrThrowIfMany()?.let { append("/tags/") append(it.key) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt index c45bb4a0e..16d5940e1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt @@ -38,22 +38,25 @@ internal class HentaiEnvy(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/?s_key=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&") } else -> { - if (filter.tags.isNotEmpty()) { - if (filter.locale != null) { + val tags = filter.tags + val lang = filter.locale + if (tags.isNotEmpty()) { + if (lang != null) { throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED) } - filter.tags.oneOrThrowIfMany()?.let { + tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) if (order == SortOrder.POPULARITY) { @@ -61,9 +64,9 @@ internal class HentaiEnvy(context: MangaLoaderContext) : } append("/?") } - } else if (filter.locale != null) { + } else if (lang != null) { append("/language/") - append(filter.locale.toLanguagePath()) + append(lang.toLanguagePath()) append("/?") } else { append("/?") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt index 1f503034c..52c28c81f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt @@ -46,31 +46,34 @@ internal class HentaiEra(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/?key=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&") } else -> { - if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { + val tags = filter.tags + val lang = filter.locale + if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { append("/search/?key=") if (order == SortOrder.POPULARITY) { append( - buildQuery(filter.tags, filter.locale) + buildQuery(tags, lang) .replace("<=1&dl=0&pp=0&tr=0", "<=0&dl=0&pp=1&tr=0"), ) } else { - append(buildQuery(filter.tags, filter.locale)) + append(buildQuery(tags, lang)) } append("&") - } else if (filter.tags.isNotEmpty()) { - filter.tags.oneOrThrowIfMany()?.let { + } else if (tags.isNotEmpty()) { + tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) } @@ -80,9 +83,9 @@ internal class HentaiEra(context: MangaLoaderContext) : append("popular/") } append("?") - } else if (filter.locale != null) { + } else if (lang != null) { append("/language/") - append(filter.locale.toLanguagePath()) + append(lang.toLanguagePath()) append("/") if (order == SortOrder.POPULARITY) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt index c48a65848..ad65267b1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt @@ -54,26 +54,29 @@ internal class HentaiForce(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") } else -> { - if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { + val tags = filter.tags + val lang = filter.locale + if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { append("/search?q=") - append(buildQuery(filter.tags, filter.locale)) + append(buildQuery(tags, lang)) if (order == SortOrder.POPULARITY) { append("&sort=popular") } append("&page=") - } else if (filter.tags.isNotEmpty()) { - filter.tags.oneOrThrowIfMany()?.let { + } else if (tags.isNotEmpty()) { + tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) } @@ -83,9 +86,9 @@ internal class HentaiForce(context: MangaLoaderContext) : append("popular/") } append("?") - } else if (filter.locale != null) { + } else if (lang != null) { append("/language/") - append(filter.locale.toLanguagePath()) + append(lang.toLanguagePath()) append("/") if (order == SortOrder.POPULARITY) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt index 60d5a62ac..5b47d5fa1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt @@ -26,13 +26,14 @@ internal class HentaiFox(context: MangaLoaderContext) : override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (page > 1) { append("&page=") append(page.toString()) @@ -40,9 +41,11 @@ internal class HentaiFox(context: MangaLoaderContext) : } else -> { - if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { + val tags = filter.tags + val lang = filter.locale + if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { append("/search/?q=") - append(buildQuery(filter.tags, filter.locale)) + append(buildQuery(tags, lang)) if (page > 1) { append("&page=") append(page.toString()) @@ -51,8 +54,8 @@ internal class HentaiFox(context: MangaLoaderContext) : if (order == SortOrder.POPULARITY) { append("&sort=popular") } - } else if (filter.tags.isNotEmpty()) { - filter.tags.oneOrThrowIfMany()?.let { + } else if (tags.isNotEmpty()) { + tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) } @@ -66,9 +69,9 @@ internal class HentaiFox(context: MangaLoaderContext) : append(page.toString()) append("/") } - } else if (filter.locale != null) { + } else if (lang != null) { append("/language/") - append(filter.locale.toLanguagePath()) + append(lang.toLanguagePath()) append("/") if (order == SortOrder.POPULARITY) { append("popular/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt index 387b472f1..de06e2c08 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt @@ -44,9 +44,10 @@ internal class NHentaiParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder().addPathSegments(apiSuffix) val isDefaultHome = order == SortOrder.UPDATED - && filter.query.isNullOrEmpty() + && query.isNullOrEmpty() && filter.tags.isEmpty() && filter.locale == null @@ -55,7 +56,7 @@ internal class NHentaiParser(context: MangaLoaderContext) : url.addQueryParameter("page", page.toString()) } else { val query = buildString { - filter.query?.trim()?.takeIf { it.isNotEmpty() }?.let { + query?.trim()?.takeIf { it.isNotEmpty() }?.let { append(it) append(" ") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt index 4514a4392..9dd58364f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt @@ -47,6 +47,7 @@ internal class NHentaiToParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -55,9 +56,9 @@ internal class NHentaiToParser(context: MangaLoaderContext) : append("/go/?") } - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&") } @@ -71,7 +72,7 @@ internal class NHentaiToParser(context: MangaLoaderContext) : append("/tag/") append(tag.key) append("/?") - } else if (filter.locale != null) { + } else if (lang != null) { append("/language/") append(lang.toLanguagePath()) append("/?") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt index f63fddf55..6cd2c9fb2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt @@ -44,6 +44,7 @@ internal class NHentaiXxxParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -69,8 +70,8 @@ internal class NHentaiXxxParser(context: MangaLoaderContext) : joiner.add(tag.title) } - if (!filter.query.isNullOrEmpty()) { - joiner.add(filter.query.urlEncoded()) + if (!query.isNullOrEmpty()) { + joiner.add(query.urlEncoded()) } append(joiner.complete()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt index 9329471bf..45364a8b1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt @@ -85,19 +85,21 @@ internal class HentaiNexus(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://$domain/page/$page") when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { val queries = mutableListOf() - if (!filter.author.isNullOrEmpty()) { - queries.add("artist:${filter.author}") + if (!author.isNullOrEmpty()) { + queries.add("artist:${author}") } filter.tags.map { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt index c992273ef..4c1ca4d69 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt @@ -159,6 +159,8 @@ internal class HentaiRead(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query + val author = filter.author // Query structure: // ?s=& // title-type=contains& /* ?s= query types: contain, start-with, end-with */ @@ -178,12 +180,12 @@ internal class HentaiRead(context: MangaLoaderContext) : append("https://$domain") // ANY filter (including single tag) uses search format - val isSearch = filter.query != null || + val isSearch = query != null || filter.tags.isNotEmpty() || filter.types.isNotEmpty() || filter.year != YEAR_UNKNOWN || filter.tagsExclude.isNotEmpty() || - !filter.author.isNullOrEmpty() + !author.isNullOrEmpty() when { // Search with any filters @@ -196,7 +198,7 @@ internal class HentaiRead(context: MangaLoaderContext) : val queries = mutableListOf() // Search query (can be empty) - queries.add("s=${filter.query?.trim()?.urlEncoded() ?: ""}") + queries.add("s=${query?.trim()?.urlEncoded() ?: ""}") queries.add("title-type=contains") queries.add("search-mode=AND") queries.add("release-type=in") @@ -222,8 +224,8 @@ internal class HentaiRead(context: MangaLoaderContext) : } // Author (using artist ID for search) - if (!filter.author.isNullOrEmpty()) { - val authorId = getAuthorId(filter.author) + if (!author.isNullOrEmpty()) { + val authorId = getAuthorId(author) if (authorId != null) { queries.add("artists[]=$authorId") } @@ -399,14 +401,15 @@ internal class HentaiRead(context: MangaLoaderContext) : } override suspend fun getPageUrl(page: MangaPage): String { - if (page.preview.isNullOrEmpty()) { + val preview = page.preview + if (preview.isNullOrEmpty()) { throw Exception("It should be not null. Something wrong!") } // preview page url: https://hencover.xyz/preview/${mangaId}/${chapterId}/hr_${index.padLeft(4)}.jpg // page url: https://henread.xyz/${mangaId}/${chapterId}/hr_${index.padLeft(4)}.jpg val index = page.url.split("/").last() - val t = page.preview.split("/") + val t = preview.split("/") val mangaId = t[4] val chapterId = t[5] diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt index 9f5cedefa..69b27fbce 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt @@ -37,16 +37,17 @@ internal abstract class GattsuParser( protected open val tagPrefix = "tag" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt index 57b7b736f..ae26659ee 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt @@ -34,13 +34,14 @@ internal abstract class GuyaParser( override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/api/get_all_series/") } - return if (!filter.query.isNullOrEmpty()) { - parseMangaList(webClient.httpGet(url).parseJson(), filter.query) + return if (!query.isNullOrEmpty()) { + parseMangaList(webClient.httpGet(url).parseJson(), query) } else { parseMangaList(webClient.httpGet(url).parseJson(), "") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt index 47b848830..cd811ecb0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt @@ -50,18 +50,19 @@ internal abstract class HeanCms( protected open val pathManga = "series" protected open val apiPath - get() = getDomain("api") + get() = getDomain(this, "api") protected open val paramsUpdated = "latest" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(apiPath) append("/query?query_string=") - filter.query?.let { - append(filter.query.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } append("&series_type=Comic&perPage=$pageSize") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt index 172d9d576..e244513b6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt @@ -56,8 +56,9 @@ internal class TempleScan(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - return if (!filter.query.isNullOrEmpty()) { - getSearchList(filter.query, page) + val query = filter.query + return if (!query.isNullOrEmpty()) { + getSearchList(query, page) } else { getBrowseList(page, order) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt index 76a04ca0b..35af4cc45 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt @@ -61,10 +61,11 @@ internal class PerfScan(context: MangaLoaderContext) : override val cdn = "https://$apiPath/cdn/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://$apiPath/series?page=$page&take=$pageSize&type=COMIC") - filter.query?.let { append("&title=").append(it.urlEncoded()) } + query?.let { query -> append("&title=").append(query.urlEncoded()) } if (filter.states.isNotEmpty()) { val statusMapping = fetchStatusMap() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt index 3394796e1..c012bbdd0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt @@ -45,11 +45,12 @@ internal abstract class HeanCmsAlt( override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append(listUrl) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt index 4dc8c588d..4b05e04b8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt @@ -52,6 +52,7 @@ internal abstract class HotComicsParser( protected open val onePage = false override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query if (onePage && page > 1) { return emptyList() } @@ -61,9 +62,9 @@ internal abstract class HotComicsParser( append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt index 0e994f814..56c88fb7b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt @@ -51,15 +51,16 @@ internal class DoujinDesuParser(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val author = filter.author val url = urlBuilder().apply { if (filter.tags.isNotEmpty()) { addPathSegment("genre") filter.tags.oneOrThrowIfMany()?.key?.let { it -> addPathSegment(it.lowercase().splitByWhitespace().joinToString("-") { it }) } - } else if (!filter.author.isNullOrEmpty()) { + } else if (!author.isNullOrEmpty()) { addPathSegment("author") - addPathSegment(filter.author.splitByWhitespace().joinToString("-") { it.lowercase() }) + addPathSegment(author.splitByWhitespace().joinToString("-") { it.lowercase() }) } else { addPathSegment("manga") } @@ -70,7 +71,7 @@ internal class DoujinDesuParser(context: MangaLoaderContext) : } if (!filter.states.isEmpty() && - filter.author.isNullOrEmpty() && + author.isNullOrEmpty() && filter.tags.isEmpty() ) { filter.states.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt index 2ec8f5634..4bb297fa3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt @@ -33,15 +33,16 @@ internal class HentaiCrot(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/page/") append(page) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt index 099262e9d..ef400ba9d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt @@ -86,12 +86,13 @@ internal class Komikcast(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder().host(apiUrl).apply { addPathSegment("series") // Query with keyword, testing... - if (!filter.query.isNullOrEmpty()) { - val keyword = filter.query.encodeKeyword() + if (!query.isNullOrEmpty()) { + val keyword = query.encodeKeyword() val filterValue = "title=like=\"$keyword\",nativeTitle=like=\"$keyword\"" addEncodedQueryParameter("filter", filterValue) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt index cb4429076..0f0f25456 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt @@ -42,14 +42,15 @@ internal class Kumapage(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val genre = if (filter.tags.isNotEmpty()) filter.tags.first().key else "all" val url = "https://$domain/search-process/" val headers = Headers.Builder() .add(CommonHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") .build() - val response = webClient.httpPost(url.toHttpUrl(), payload = "view=1&keyword=${filter.query}&genre=$genre", headers).parseHtml() + val response = webClient.httpPost(url.toHttpUrl(), payload = "view=1&keyword=${query}&genre=$genre", headers).parseHtml() parseSearchList(response) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt index 8d3bb33b9..01b91c23f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt @@ -33,15 +33,16 @@ internal class PixHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/page/") append(page) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt index dea872bd9..62e86edaa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt @@ -64,6 +64,8 @@ internal class Shinigami(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://") append(apiSuffix) @@ -128,14 +130,14 @@ internal class Shinigami(context: MangaLoaderContext) : append("&genre_exclude_mode=and") } - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { append("&author=") - append(filter.author.lowercase().replace(' ', '-').urlEncoded()) + append(author.lowercase().replace(' ', '-').urlEncoded()) } - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&q=") - val encodedQuery = filter.query.splitByWhitespace() + val encodedQuery = query.splitByWhitespace() .joinToString(separator = "+") { part -> part.urlEncoded() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt index 68e81a2c1..7f849651f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt @@ -82,8 +82,9 @@ internal class TheManga(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> searchManga(filter.query) + !query.isNullOrEmpty() -> searchManga(query) filter.tags.isNotEmpty() -> getMangaListByGenre(filter.tags.first().key, page, filter.tags) else -> getAllManga(page, order) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt index 306fe2319..bb6ec69a4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt @@ -44,18 +44,19 @@ internal class WestmangaParser(context: MangaLoaderContext) : ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val page = (offset / PAGE_SIZE) + 1 val urlBuilder = "$apiUrl/api/contents".toHttpUrl().newBuilder() - if (!filter.query.isNullOrBlank()) { - urlBuilder.addQueryParameter("q", filter.query) + if (!query.isNullOrBlank()) { + urlBuilder.addQueryParameter("q", query) } urlBuilder.addQueryParameter("page", page.toString()) urlBuilder.addQueryParameter("per_page", PAGE_SIZE.toString()) - if (filter.query.isNullOrBlank()) { + if (query.isNullOrBlank()) { val orderBy = when (order) { SortOrder.POPULARITY -> "Popular" SortOrder.UPDATED -> "Update" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt index a68e9103a..f8c154afd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt @@ -56,6 +56,7 @@ internal abstract class IkenParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(defaultDomain) @@ -63,8 +64,8 @@ internal abstract class IkenParser( append(page) append("&perPage=18&searchTerm=") - filter.query?.let { - append(filter.query.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt index 95e098bd1..a5c2041f2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt @@ -66,6 +66,7 @@ internal class Klz9(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val builder = urlBuilder() .addPathSegment("api").addPathSegment("manga") .addPathSegment("list") @@ -73,8 +74,8 @@ internal class Klz9(context: MangaLoaderContext) : builder.addQueryParameter("page", page.toString()) builder.addQueryParameter("limit", pageSize.toString()) - if (!filter.query.isNullOrEmpty()) { - builder.addQueryParameter("search", filter.query) + if (!query.isNullOrEmpty()) { + builder.addQueryParameter("search", query) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt index aebb7dede..d0c537386 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt @@ -43,23 +43,24 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : ) override val authUrl: String - get() = "https://${getDomain("account")}/login?site=seiga" + get() = "https://${getDomain(this, "account")}/login?site=seiga" - override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(getDomain("seiga")).any { + override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(getDomain(this, "seiga")).any { it.name == "user_session" } override suspend fun getUsername(): String { - val body = webClient.httpGet("https://${getDomain("app")}/my/apps").parseHtml().body() + val body = webClient.httpGet("https://${getDomain(this, "app")}/my/apps").parseHtml().body() return body.selectFirst("#userinfo > div > div > strong")?.text() ?: throw AuthRequiredException(source) } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val page = (offset / 20f).toIntUp().inc() - val domain = getDomain("seiga") + val domain = getDomain(this, "seiga") val url = when { - !filter.query.isNullOrEmpty() -> { - return if (offset == 0) getSearchList(filter.query, page) else emptyList() + !query.isNullOrEmpty() -> { + return if (offset == 0) getSearchList(query, page) else emptyList() } else -> { @@ -105,14 +106,14 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : STATUS_FINISHED -> MangaState.FINISHED else -> null }, - publicUrl = href.toAbsoluteUrl(item.host ?: getDomain("seiga")), + publicUrl = href.toAbsoluteUrl(item.host ?: getDomain(this, "seiga")), source = source, ) } } override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain("seiga"))).parseHtml() + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain(this, "seiga"))).parseHtml() val contents = doc.body().requireElementById("contents") val statusText = contents .select("div.mg_work_detail > div > div:nth-child(2) > div.tip.content_status.status_series > span") @@ -150,7 +151,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : } override suspend fun getPages(chapter: MangaChapter): List { - val fullUrl = chapter.url.toAbsoluteUrl(getDomain("seiga")) + val fullUrl = chapter.url.toAbsoluteUrl(getDomain(this, "seiga")) val doc = webClient.httpGet(fullUrl).parseHtml() if (!doc.select("#login_manga").isEmpty()) throw AuthRequiredException(source) @@ -167,7 +168,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : } private suspend fun fetchAvailableTags(): Set { - val doc = webClient.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml() + val doc = webClient.httpGet("https://${getDomain(this, "seiga")}/manga/list").parseHtml() val root = doc.body().selectOrThrow("#mg_category_list > ul > li").drop(1) return root.mapToSet { li -> val a = li.selectFirstOrThrow("a") @@ -180,7 +181,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : } private suspend fun getSearchList(query: String, page: Int): List { - val domain = getDomain("seiga") + val domain = getDomain(this, "seiga") val doc = webClient.httpGet("https://$domain/manga/search/?q=$query&page=$page&sort=score").parseHtml() val root = doc.body().select(".search_result__item") return root.mapNotNull { item -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt index e2f1d681e..1b08e0f19 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt @@ -66,15 +66,10 @@ internal abstract class KeyoappParser( ) override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - var query = "" + val query = filter.query.orEmpty() var tag = "" val url = urlBuilder().apply { - - filter.query?.let { - query = filter.query - } - filter.tags.oneOrThrowIfMany()?.let { tag = it.title } @@ -84,7 +79,6 @@ internal abstract class KeyoappParser( SortOrder.NEWEST -> addPathSegment("series") else -> addPathSegment("series") } - }.build() return parseMangaList(webClient.httpGet(url).parseHtml(), tag, query) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt index 672c9b561..143bb9536 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt @@ -52,16 +52,17 @@ internal abstract class LikeMangaParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/?act=search") - filter.query?.let { + query?.let { query -> append("&f") append("[keyword]".urlEncoded()) append("=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } append("&f") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt index 73795abb8..8bb7afb0e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt @@ -57,15 +57,16 @@ internal abstract class LilianaParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/") append(page) append("/?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt index 290a2a43f..4466ed9b5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt @@ -250,6 +250,8 @@ internal abstract class MadaraParser( protected open val listUrl = "manga/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author if (withoutAjax) { val pages = page + 1 @@ -263,8 +265,8 @@ internal abstract class MadaraParser( } append("/?s=") - filter.query?.let { - append(filter.query.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } append("&post_type=wp-manga") @@ -305,8 +307,8 @@ internal abstract class MadaraParser( append(filter.year.toString()) } - if (!filter.author.isNullOrEmpty()) { - filter.author.let { + if (!author.isNullOrEmpty()) { + author.let { append("&author=") // should be like "minamida-usuke" append(it.lowercase().replace(" ", "-")) @@ -347,8 +349,8 @@ internal abstract class MadaraParser( payload["page"] = page.toString() - filter.query?.let { - payload["vars[s]"] = filter.query.urlEncoded() + query?.let { query -> + payload["vars[s]"] = query.urlEncoded() } if (filter.tags.isNotEmpty()) { @@ -375,11 +377,11 @@ internal abstract class MadaraParser( payload["vars[tax_query][2][terms][]"] = filter.year.toString() } - if (!filter.author.isNullOrEmpty()) { - filter.author.let { + if (!author.isNullOrEmpty()) { + author.let { payload["vars[tax_query][3][taxonomy]"] = "wp-manga-author" payload["vars[tax_query][3][field]"] = "name" - payload["vars[tax_query][3][terms][0]"] = filter.author + payload["vars[tax_query][3][terms][0]"] = author payload["vars[tax_query][3][operator]"] = "IN" } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt index 9bca29160..d2dd96a92 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt @@ -31,13 +31,14 @@ internal class Manga18Fx(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt index a1a084563..99e385178 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt @@ -31,14 +31,15 @@ internal class Manhwa18Cc(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt index e1293c93a..49f920c7a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt @@ -84,10 +84,11 @@ internal class ArabsHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val pages = page + 1 val url = when { - !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() -> { + !query.isNullOrEmpty() || filter.tags.isNotEmpty() -> { buildString { append("https://") append(domain) @@ -96,7 +97,7 @@ internal class ArabsHentai(context: MangaLoaderContext) : append(pages) } append("/?s=") - append(filter.query?.urlEncoded() ?: "") + append(query?.urlEncoded() ?: "") filter.tags.forEach { tag -> append("&genre%5B%5D=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt index 3cfb44328..0ccfe6878 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt @@ -72,6 +72,7 @@ internal class RocksManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -84,8 +85,8 @@ internal class RocksManga(context: MangaLoaderContext) : append("/?post_type=wp-manga") append("&s=") - filter.query?.let { - append(it.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } filter.types.forEach { contentType -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt index 396393901..7c70f6c05 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt @@ -31,19 +31,20 @@ internal class AdultWebtoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt index 76d4ee69e..8cfc7a2da 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt @@ -23,15 +23,16 @@ internal class Hentai4Free(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt index 9621e348a..845648f35 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt @@ -25,19 +25,20 @@ internal class HentaiManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt index 2c68a9cc3..413f6d882 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt @@ -25,19 +25,20 @@ internal class HentaiWebtoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt index f6c368064..d0be0d11a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt @@ -32,13 +32,14 @@ internal class IsekaiScan(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt index 40d36c18d..5ec2b780d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt @@ -21,19 +21,20 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { append("/page/") append(page.toString()) } append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt index e8a4e46a3..a1eff32ce 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt @@ -30,13 +30,14 @@ internal class MangaDass(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt index cc9b80205..4ae595ec1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt @@ -25,13 +25,14 @@ internal class MangaDna(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt index c1393bb10..3000302d6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt @@ -36,13 +36,14 @@ internal class MangaPure(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt index df31a917e..cc4a8f879 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt @@ -26,13 +26,14 @@ internal class Manhwaz(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt index 223786876..e002b2066 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt @@ -28,19 +28,20 @@ internal class ManyToon(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt index abff4c3ae..37bb7d35b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt @@ -42,14 +42,15 @@ internal class DoujinHentaiNet(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val pageNum = page + 1 val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?query=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (pageNum > 1) { append("&page=") append(pageNum) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt index c91758121..f9098ca24 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt @@ -29,14 +29,15 @@ internal class TmoManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/$listUrl") append("?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (page > 1) { append("&page=") append(page) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt index c07178896..1ee5956cc 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt @@ -85,12 +85,13 @@ internal class RaijinScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://$domain") if (page > 0) append("/page/${page + 1}") append("?post_type=wp-manga&s=") - filter.query?.let { append(it.urlEncoded()) } + query?.let { query -> append(query.urlEncoded()) } if (filter.year != YEAR_UNKNOWN) append("&release[]=${filter.year}") if (!filter.tags.isEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt index 5a222ed0a..e7fb109f4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt @@ -30,13 +30,14 @@ internal class ManhwaHub(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt index 3b520f721..6a62ca5ff 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt @@ -31,15 +31,17 @@ internal class HentaiCube(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val pages = page + 1 val url = buildString { - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tacgia/") - append(filter.author.lowercase().replace(" ", "-")) + append(author.lowercase().replace(" ", "-")) if (pages > 1) { append("/page/") @@ -69,8 +71,8 @@ internal class HentaiCube(context: MangaLoaderContext) : append("/?s=") - filter.query?.let { - append(filter.query.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt index abdae5403..41932bdc5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt @@ -23,15 +23,17 @@ internal class HentaiVnPlus(context: MangaLoaderContext) : override val authorSearchSupported = true override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val pages = page + 1 val url = buildString { - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tac-gia/") - append(filter.author.lowercase().replace(" ", "-")) + append(author.lowercase().replace(" ", "-")) if (pages > 1) { append("/page/") @@ -61,8 +63,8 @@ internal class HentaiVnPlus(context: MangaLoaderContext) : append("/?s=") - filter.query?.let { - append(filter.query.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt index b91703139..7d0fc9d9a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt @@ -29,13 +29,14 @@ internal class Saytruyenhay(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt index e3a0aef2a..5bf2efedc 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt @@ -68,6 +68,7 @@ internal abstract class MadthemeParser( protected open val datePattern = "MMM dd, yyyy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -77,9 +78,9 @@ internal abstract class MadthemeParser( append("?page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } append("&sort=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt index c640a9737..5685b7c4c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt @@ -55,6 +55,8 @@ internal class ManhuaScan(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://") append(domain) @@ -63,9 +65,9 @@ internal class ManhuaScan(context: MangaLoaderContext) : append("?page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&q=") - append(it.urlEncoded()) + append(query.urlEncoded()) } append("&sort=") @@ -106,7 +108,7 @@ internal class ManhuaScan(context: MangaLoaderContext) : ) } ?: append("&status=all") - filter.author?.takeIf { it.isNotBlank() }?.let { author -> + author?.takeIf { it.isNotBlank() }?.let { author -> append("&author=") append(author.urlEncoded()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt index c96f920c6..de29c3300 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt @@ -62,12 +62,13 @@ internal abstract class Manga18Parser( protected open val datePattern = "dd-MM-yyyy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append('/') - if (filter.tags.isNotEmpty() && filter.query != null) { + if (filter.tags.isNotEmpty() && query != null) { throw IllegalArgumentException("Search is not supported with tags") } @@ -80,11 +81,11 @@ internal abstract class Manga18Parser( } } - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append(listUrl) append(page.toString()) append("?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&order_by=latest") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt index 1c87f670d..12c1a22e5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt @@ -89,13 +89,14 @@ internal abstract class MangaboxParser( protected open val itemSelector = "div.search-story-item, a.list-story-item, .story_item" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder() - if (!filter.query.isNullOrEmpty()) { - val query = filter.query.filterQuery() + if (!query.isNullOrEmpty()) { + val filteredQuery = query.filterQuery() url.addPathSegment("search") url.addPathSegment("story") - url.addPathSegment(query) + url.addPathSegment(filteredQuery) } else { url.addPathSegment("genre") if (filter.tags.isEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt index f02d37abf..1f188a9b7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt @@ -61,12 +61,13 @@ internal abstract class MangAdventureParser( order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query val url = apiUrl.addEncodedPathSegment("series") .addEncodedQueryParameter("limit", pageSize.toString()) .addEncodedQueryParameter("page", page.toString()) - filter.query?.let { - url.addQueryParameter("title", filter.query) + query?.let { query -> + url.addQueryParameter("title", query) } url.addQueryParameter( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt index 3e9ccf4c6..2cba9fcf6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt @@ -70,17 +70,18 @@ internal abstract class MangaReaderParser( protected val mutex = Mutex() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt index cbef2d8a7..e3013a104 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt @@ -23,14 +23,15 @@ internal class Normoyun(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt index 02395442d..5b0085fbf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt @@ -58,16 +58,17 @@ internal class RizzComic(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query if (page > 1) { return emptyList() } var url = "https://$domain$filterUrl" val payload = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { url = "https://$domain$searchUrl" - if (filter.query != "") { - mapOf("search_value" to filter.query.trim()) + if (query != "") { + mapOf("search_value" to query.trim()) } else { null } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt index dc794832e..a08104b32 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt @@ -31,6 +31,7 @@ internal class Zahard(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -38,9 +39,9 @@ internal class Zahard(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt index 451cdfa97..0e70add01 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt @@ -29,16 +29,17 @@ internal class CerberuSeries(context: MangaLoaderContext) : // Site is served at /wp/ subpath, so search URLs need that prefix too. override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/wp/page/") append(page.toString()) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt index f4eda7727..e319bd09e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt @@ -18,16 +18,17 @@ internal class HentaiReader(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append(listUrl) append("?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt index 0356b6fc7..ee909c610 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt @@ -17,16 +17,17 @@ internal class LectorHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append(listUrl) append("?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt index 3077b5668..900cd2724 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt @@ -18,15 +18,16 @@ internal class MangaTv(context: MangaLoaderContext) : override val datePattern = "yyyy-MM-dd" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/lista?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt index 4c14dd5d5..273912d5a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt @@ -46,9 +46,10 @@ internal class TenkaiScan(context: MangaLoaderContext) : private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.ROOT) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query if (page > 1) return emptyList() // site does not paginate; all comics are returned on page 1 val url = "https://$domain/comics".toHttpUrl().newBuilder().apply { - if (!filter.query.isNullOrBlank()) addQueryParameter("search", filter.query) + if (!query.isNullOrBlank()) addQueryParameter("search", query) }.build() val doc = webClient.httpGet(url).parseHtml() return doc.select(".card.blade-project-index").mapNotNull { card -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt index f31eaaf85..806f4ba9d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt @@ -30,6 +30,7 @@ internal class TuManhwas(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -37,9 +38,9 @@ internal class TuManhwas(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt index 3d0a02e5a..456cc8428 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt @@ -30,10 +30,11 @@ internal class RevolutionScantrad(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query if (page > 1) { return emptyList() } - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } val url = buildString { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt index 0841c0ba3..7b0052b9f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt @@ -46,6 +46,7 @@ internal class KomikIndo(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -111,9 +112,9 @@ internal class KomikIndo(context: MangaLoaderContext) : }, ) - filter.query?.let { + query?.let { query -> append("&title=") - append(it.urlEncoded()) + append(query.urlEncoded()) } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt index ea024c7d1..b5743c23b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt @@ -23,14 +23,15 @@ internal class KomikSan(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt index f9e4745f1..21546b489 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt @@ -57,8 +57,10 @@ internal abstract class MangaWorldParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author // Handle UPDATED sort without filters - use homepage - val noQuery = filter.query.isNullOrBlank() + val noQuery = query.isNullOrBlank() && filter.tags.isEmpty() && filter.states.isEmpty() && filter.types.isEmpty() @@ -75,9 +77,9 @@ internal abstract class MangaWorldParser( append("/archive?&page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } filter.tags.forEach { @@ -125,7 +127,7 @@ internal abstract class MangaWorldParser( } // author ( not query but same to tags ) - // filter.author.forEach { + // author.forEach { // append("&author=") // append(it.key) // } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt index 02bc867f7..d882a44b0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt @@ -73,12 +73,13 @@ internal abstract class ManhwaZ( protected open val searchPath = "/search" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("$searchPath?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } else { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt index 72424b3f5..4b3da68fd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt @@ -74,9 +74,10 @@ internal abstract class MmrcmsParser( protected open val datePattern = "dd MMM. yyyy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query if (order == SortOrder.UPDATED) { - if (filter.query != null || filter.tags.isNotEmpty()) { + if (query != null || filter.tags.isNotEmpty()) { throw IllegalArgumentException("Sorting by update with filters is not supported by this source.") } @@ -98,8 +99,8 @@ internal abstract class MmrcmsParser( append(page.toString()) append("&author=&tag=&alpha=") - filter.query?.let { - append(filter.query.urlEncoded()) + query?.let { query -> + append(query.urlEncoded()) } append("&cat=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt index 498478157..055d145af 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt @@ -111,6 +111,8 @@ internal abstract class NatsuParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = "https://${domain}/wp-admin/admin-ajax.php?action=advanced_search" val formParts = mutableMapOf() @@ -130,8 +132,8 @@ internal abstract class NatsuParser( formParts["page"] = page.toString() - if (!filter.author.isNullOrEmpty()) { - val authorArray = JSONArray(filter.author) + if (!author.isNullOrEmpty()) { + val authorArray = JSONArray(author) formParts["author"] = authorArray.toString() } else formParts["author"] = "[]" @@ -179,8 +181,8 @@ internal abstract class NatsuParser( else -> "popular" } - if (!filter.query.isNullOrEmpty()) { - formParts["query"] = filter.query + if (!query.isNullOrEmpty()) { + formParts["query"] = query } val html = httpPost(url, formParts) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt index 48aac05a1..f5f92cb95 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt @@ -63,6 +63,7 @@ internal abstract class NepnepParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val doc = searchDoc.get() val json = JSONArray( doc.selectFirstOrThrow("script:containsData(MainFunction)").data() @@ -83,10 +84,10 @@ internal abstract class NepnepParser( //val viewMonth = m.getString("vm") when { - !filter.query.isNullOrEmpty() -> { - if (m.getString("s").contains(filter.query, ignoreCase = true) || (m.getJSONArray("al") + !query.isNullOrEmpty() -> { + if (m.getString("s").contains(query, ignoreCase = true) || (m.getJSONArray("al") .length() > 0 && m.getJSONArray("al").getString(0) - .contains(filter.query, ignoreCase = true)) + .contains(query, ignoreCase = true)) ) { mangaWithLastUpdateList.add( MangaWithLastUpdate(addManga(href, imgUrl, m), lastUpdate, views), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt index 319ea41c8..f4877713e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt @@ -67,8 +67,9 @@ internal abstract class OtakuSanctuaryParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val doc = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } @@ -76,7 +77,7 @@ internal abstract class OtakuSanctuaryParser( append("https://") append(domain) append("/Home/Search?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } webClient.httpGet(url).parseHtml().requireElementById("collection-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt index 279a7869c..ee2f0901e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt @@ -105,6 +105,7 @@ internal abstract class PizzaReaderParser( protected open val abandonedFilter = "droppato" override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val manga = ArrayList() val selectedState = filter.states.oneOrThrowIfMany() val selectedRating = filter.contentRating.oneOrThrowIfMany() @@ -112,8 +113,8 @@ internal abstract class PizzaReaderParser( val excludeTags = filter.tagsExclude.map { it.key.lowercase(Locale.ROOT) } when { - !filter.query.isNullOrEmpty() -> { - val jsonManga = webClient.httpGet("https://$domain/api/search/${filter.query.urlEncoded()}").parseJson() + !query.isNullOrEmpty() -> { + val jsonManga = webClient.httpGet("https://$domain/api/search/${query.urlEncoded()}").parseJson() .getJSONArray("comics") for (i in 0 until jsonManga.length()) { val j = jsonManga.getJSONObject(i) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt index 87b157b63..671d5a34b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt @@ -55,7 +55,8 @@ internal class AnimeXNovel(context: MangaLoaderContext) : private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val all = fetchAllSeries(filter.query) + val query = filter.query + val all = fetchAllSeries(query) val sorted = when (order) { SortOrder.POPULARITY -> all.sortedByDescending { it.postCount } SortOrder.ALPHABETICAL -> all.sortedBy { it.name.lowercase(Locale.ROOT) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt index a3a281cbb..969abf520 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt @@ -35,17 +35,18 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { append("/page/$page/") } append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { @@ -77,7 +78,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, val item = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { doc.select("div.listagem div.item") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt index 98b4f1607..0ee53c872 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt @@ -47,14 +47,15 @@ internal class InstaHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("/page/") append(page) append("/?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else { val tagSlug = filter.tags.oneOrThrowIfMany()?.key if (tagSlug != null) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt index 552d51de2..b6e1b1fc2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt @@ -42,6 +42,7 @@ internal class LerManga(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -54,7 +55,7 @@ internal class LerManga(context: MangaLoaderContext) : PagedMangaParser(context, when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt index 573557b7a..94c8222b7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt @@ -35,19 +35,20 @@ internal class LerMangaOnline(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { append("page/") append(page.toString()) append("/") } append("?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt index 7063378bf..41bfde54a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt @@ -37,7 +37,8 @@ internal class LuratoonScansParser(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - require(filter.query.isNullOrEmpty()) { ErrorMessages.SEARCH_NOT_SUPPORTED } + val query = filter.query + require(query.isNullOrEmpty()) { ErrorMessages.SEARCH_NOT_SUPPORTED } val url = urlBuilder() val tag = filter.tags.oneOrThrowIfMany() if (tag == null) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt index 64abdba39..d7e9a6d05 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt @@ -33,14 +33,15 @@ internal class MangaOnline(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search/") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt index 5bd97465c..ea5fd2eef 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt @@ -49,10 +49,11 @@ internal class Manhastro(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val all = allMangasCache.get() var filtered: List = all - if (!filter.query.isNullOrEmpty()) { - val q = filter.query.lowercase(sourceLocale) + if (!query.isNullOrEmpty()) { + val q = query.lowercase(sourceLocale) filtered = filtered.filter { m -> (m.getStringOrNull("titulo")?.lowercase(sourceLocale)?.contains(q) == true) || (m.getStringOrNull("titulo_brasil")?.lowercase(sourceLocale)?.contains(q) == true) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt index 802875c28..52fb35547 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt @@ -75,8 +75,9 @@ internal class MediocreToons(context: MangaLoaderContext) : PagedMangaParser( private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", sourceLocale) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = when { - !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.types.isNotEmpty() -> buildSearchUrl( + !query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.types.isNotEmpty() -> buildSearchUrl( page, filter, ) @@ -96,12 +97,13 @@ internal class MediocreToons(context: MangaLoaderContext) : PagedMangaParser( } private fun buildSearchUrl(page: Int, filter: MangaListFilter): HttpUrl { + val query = filter.query val builder = "$apiUrl/obras".toHttpUrl().newBuilder().addQueryParameter("limite", pageSize.toString()) .addQueryParameter("pagina", page.toString()) // Add search query - if (!filter.query.isNullOrEmpty()) { - builder.addQueryParameter("string", filter.query) + if (!query.isNullOrEmpty()) { + builder.addQueryParameter("string", query) } // Add tags diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt index a846a14fd..bb04a6ca4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt @@ -31,15 +31,16 @@ internal class MuitoHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) return emptyList() append("/buscar-manga/?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt index 70ec164af..a628ba99e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt @@ -51,6 +51,7 @@ internal class SssScanlator(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -67,9 +68,9 @@ internal class SssScanlator(context: MangaLoaderContext) : else -> "recent" }, ) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } filter.tags.firstOrNull()?.let { tag -> append("&genre=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt index 3e45ad084..a84fe81d8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt @@ -86,13 +86,14 @@ internal class SussyScan(context: MangaLoaderContext) : PagedMangaParser( private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", sourceLocale) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val genId = when { filter.types.oneOrThrowIfMany() == ContentType.HENTAI -> "5" else -> "1" } val url = when { - !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || + !query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty() -> buildSearchUrl(page, filter) // Popularity rankings @@ -131,8 +132,9 @@ internal class SussyScan(context: MangaLoaderContext) : PagedMangaParser( } private fun buildSearchUrl(page: Int, filter: MangaListFilter): HttpUrl { + val query = filter.query val builder = "$apiUrl/obras".toHttpUrl().newBuilder() - .addQueryParameter("obr_nome", filter.query ?: "") + .addQueryParameter("obr_nome", query ?: "") .addQueryParameter("limite", "15") .addQueryParameter("pagina", page.toString()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt index e9b1cdf94..a9561c457 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt @@ -111,7 +111,7 @@ internal class TaiyoParser(context: MangaLoaderContext) : .addHeader(CommonHeaders.ORIGIN, "https://$domain") .addHeader(CommonHeaders.REFERER, "https://$domain/") .addHeader(CommonHeaders.ACCEPT, "application/json") - .tag(MangaParserSource::class.java, source) + .tag(source) .build() val client = context.httpClient.newBuilder() .apply { interceptors().clear() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt index 429269e8d..8b14132d0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt @@ -34,15 +34,16 @@ internal class YugenMangas(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val json = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://api.") append(domain) append("/api/series/?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } webClient.httpGet(url).parseJsonArray() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt index 1e2a3bc9b..8bda4474c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt @@ -48,16 +48,17 @@ internal class AComics(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 0) { return emptyList() } append("/search?keyword=") - append(filter.query) + append(query) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt index fc82d67f1..17202d936 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt @@ -48,10 +48,11 @@ internal class ComXParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val urlBuilder = StringBuilder() when { - !filter.query.isNullOrEmpty() -> { - val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "%20") { part -> + !query.isNullOrEmpty() -> { + val encodedQuery = query.splitByWhitespace().joinToString(separator = "%20") { part -> part.urlEncoded() } urlBuilder.append("/search/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt index 4636ffea6..c6cf24f35 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt @@ -54,7 +54,8 @@ internal class DesuMeParser(context: MangaLoaderContext) : private val tagsCache = suspendLazy(initializer = ::fetchTags) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - if (!filter.query.isNullOrEmpty() && page != searchPaginator.firstPage) { + val query = filter.query + if (!query.isNullOrEmpty() && page != searchPaginator.firstPage) { return emptyList() } val domain = domain @@ -69,9 +70,9 @@ internal class DesuMeParser(context: MangaLoaderContext) : append("&genres=") filter.tags.joinTo(this, ",") { it.key } } - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&search=") - append(filter.query) + append(query) } } val json = webClient.httpGet(url).parseJson().getJSONArray("response") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt index f2c524843..4dd4253ae 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt @@ -55,6 +55,7 @@ internal class MangaWtfParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder("api") .addPathSegment("v2") @@ -63,7 +64,7 @@ internal class MangaWtfParser( .addQueryParameter("size", pageSize.toString()) .addQueryParameter("type", "COMIC") when { - filter.query.isNullOrEmpty() -> { + query.isNullOrEmpty() -> { url.addQueryParameter( "sort", when (order) { @@ -110,7 +111,7 @@ internal class MangaWtfParser( } else -> { - url.addQueryParameter("search", filter.query) + url.addQueryParameter("search", query) } } val ja = webClient.httpGet(url.build()).parseJsonArray() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt index 571ba6573..1b52dd204 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt @@ -55,14 +55,15 @@ internal class NudeMoonParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val domain = domain val url = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (!isAuthorized()) { throw AuthRequiredException(source) } - "https://$domain/search?stext=${filter.query.urlEncoded()}&rowstart=$offset" + "https://$domain/search?stext=${query.urlEncoded()}&rowstart=$offset" } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt index 9605d3ad9..da6995c6a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt @@ -82,6 +82,7 @@ internal class RemangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query copyCookies() val urlBuilder = urlBuilder(subdomain = "api") .addPathSegment("api") @@ -90,8 +91,8 @@ internal class RemangaParser( .addQueryParameter("page", page.toString()) .addQueryParameter("count", PAGE_SIZE.toString()) .addQueryParameter("ordering", getSortKey(order)) - if (!filter.query.isNullOrEmpty()) { - urlBuilder.addQueryParameter("query", filter.query) + if (!query.isNullOrEmpty()) { + urlBuilder.addQueryParameter("query", query) } else { urlBuilder.addPathSegment("catalog") } @@ -168,7 +169,7 @@ internal class RemangaParser( } override suspend fun getPages(chapter: MangaChapter): List { - val content = webClient.httpGet(chapter.url.toAbsoluteUrl(getDomain("api"))) + val content = webClient.httpGet(chapter.url.toAbsoluteUrl(org.koitharu.kotatsu.parsers.util.getDomain(this, "api"))) .parseJson() val pages = content.optJSONArray("pages") if (pages == null) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt index 2707a7a17..f9c1e4b2a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt @@ -51,8 +51,10 @@ internal class ZenMangaParser(context: MangaLoaderContext) : private val apiDomain = if (domain.startsWith("v1.")) domain.replace("v1.", "api.") else "api.$domain" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - if (!filter.author.isNullOrBlank()) { - return getListPageByAuthor(filter.author, page) + val query = filter.query + val author = filter.author + if (!author.isNullOrBlank()) { + return getListPageByAuthor(author, page) } val urlBuilder = HttpUrl.Builder() @@ -66,8 +68,8 @@ internal class ZenMangaParser(context: MangaLoaderContext) : urlBuilder.addQueryParameter("sort", getSortParameter(order)) - if (!filter.query.isNullOrBlank()) { - urlBuilder.addQueryParameter("search", filter.query) + if (!query.isNullOrBlank()) { + urlBuilder.addQueryParameter("search", query) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt index f7f002552..e3c591a08 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt @@ -91,6 +91,7 @@ internal abstract class GroupleParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val domain = domain val root = if (filter.isEmpty()) { webClient.httpGet( @@ -356,11 +357,12 @@ internal abstract class GroupleParser( } private suspend fun advancedSearch(offset: Int, order: SortOrder, filter: MangaListFilter): Response { + val query = filter.query val tagsMap = tagsIndex.get() val url = urlBuilder() .addPathSegment("search") .addPathSegment("advancedResults") - url.addQueryParameter("q", filter.query) + url.addQueryParameter("q", query) url.addQueryParameter("offset", offset.toString()) filter.tags.forEach { tag -> val tagId = requireNotNull(tagsMap[tag.title.lowercase()]) { "Tag ${tag.title} not found" } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt index dbe16c35b..85e31dcb0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt @@ -42,6 +42,7 @@ internal abstract class ChanParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val domain = domain val doc = webClient.httpGet(buildUrl(offset, order, filter)).parseHtml() val root = doc.body().selectFirst("div.main_fon")?.getElementById("content") @@ -193,17 +194,18 @@ internal abstract class ChanParser( order: SortOrder, filter: MangaListFilter, ): HttpUrl { + val query = filter.query val builder = urlBuilder() builder.addQueryParameter("offset", offset.toString()) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { builder.addQueryParameter("do", "search") builder.addQueryParameter("subaction", "search") builder.addQueryParameter("search_start", ((offset / 40) + 1).toString()) builder.addQueryParameter("full_search", "0") builder.addQueryParameter("result_from", (offset + 1).toString()) builder.addQueryParameter("result_num", "40") - builder.addQueryParameter("story", filter.query) + builder.addQueryParameter("story", query) builder.addQueryParameter("need_sort_date", "false") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt index 87fb31478..a01688c13 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt @@ -59,30 +59,37 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context, ) } - override fun buildUrl(offset: Int, order: SortOrder, filter: MangaListFilter): HttpUrl = when { - filter.query.isNullOrEmpty() && filter.tags.isEmpty() && filter.tagsExclude.isEmpty() -> { - val builder = urlBuilder().addQueryParameter("offset", offset.toString()) - when (order) { - SortOrder.POPULARITY -> { - builder.addPathSegment("mostviews") - builder.addQueryParameter("sort", "manga") - } + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + return super.getList(offset, order, filter) + } - SortOrder.RATING -> { - builder.addPathSegment("mostfavorites") - builder.addQueryParameter("sort", "manga") - } + override fun buildUrl(offset: Int, order: SortOrder, filter: MangaListFilter): HttpUrl { + val query = filter.query + return when { + query.isNullOrEmpty() && filter.tags.isEmpty() && filter.tagsExclude.isEmpty() -> { + val builder = urlBuilder().addQueryParameter("offset", offset.toString()) + when (order) { + SortOrder.POPULARITY -> { + builder.addPathSegment("mostviews") + builder.addQueryParameter("sort", "manga") + } - else -> { // SortOrder.NEWEST - builder.addPathSegment("manga") - builder.addPathSegment("newest") + SortOrder.RATING -> { + builder.addPathSegment("mostfavorites") + builder.addQueryParameter("sort", "manga") + } + + else -> { // SortOrder.NEWEST + builder.addPathSegment("manga") + builder.addPathSegment("newest") + } } + builder.build() } - builder.build() - } - else -> { - super.buildUrl(offset, order, filter) + else -> { + super.buildUrl(offset, order, filter) + } } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt index 2acddbf36..a2cebe3b6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt @@ -108,6 +108,7 @@ internal abstract class LibSocialParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val urlBuilder = HttpUrl.Builder() .scheme(SCHEME_HTTPS) .host(apiHost) @@ -126,8 +127,8 @@ internal abstract class LibSocialParser( for (tag in filter.tagsExclude) { urlBuilder.addQueryParameter("${tag.typeKey()}_exclude[]", tag.key.drop(1)) } - if (!filter.query.isNullOrEmpty()) { - urlBuilder.addQueryParameter("q", filter.query) + if (!query.isNullOrEmpty()) { + urlBuilder.addQueryParameter("q", query) } urlBuilder.addQueryParameter( "sort_by", diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt index a35821686..496e814ae 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt @@ -43,16 +43,17 @@ internal abstract class ScanParser( protected open val listUrl = "/manga" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - var query = false + val q = filter.query + var isSearch = false val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !q.isNullOrEmpty() -> { append("/search?q=") - append(filter.query.urlEncoded()) - query = true + append(q.urlEncoded()) + isSearch = true } else -> { @@ -80,7 +81,7 @@ internal abstract class ScanParser( } } - val doc = if (query) { + val doc = if (isSearch) { val raw = webClient.httpGet(url).parseRaw() Jsoup.parseBodyFragment( raw.unescapeJson(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt index 6a514676d..2e033b9ea 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt @@ -57,15 +57,16 @@ internal abstract class SinmhParser( protected open val listUrl = "list/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append(searchUrl) append("?keywords=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt index 417dded6b..c78d71ffb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt @@ -50,6 +50,7 @@ internal class ElderManga(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -57,9 +58,9 @@ internal class ElderManga(context: MangaLoaderContext): append("?page=") append(page.toString()) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt index 80c083db0..0b189a58e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt @@ -35,15 +35,16 @@ internal class MangaAy(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } return parseMangaListQueryOrTags( webClient.httpPost( "https://$domain/arama", - mapOf("title" to filter.query.urlEncoded(), "genres" to ""), + mapOf("title" to query.urlEncoded(), "genres" to ""), ).parseHtml(), ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt index 98d4d3d0b..80947aa4a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt @@ -31,13 +31,14 @@ internal class SadScans(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/series") - filter.query?.let { + query?.let { query -> append("?search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt index c5224d8a4..f382c175a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt @@ -50,6 +50,7 @@ internal class TenshiManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -57,9 +58,9 @@ internal class TenshiManga(context: MangaLoaderContext) : append("?page=") append(page.toString()) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt index bb05c6823..9424bea73 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt @@ -43,8 +43,9 @@ internal class TrWebtoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query if (order == SortOrder.UPDATED) { - if (filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.query != null) { + if (filter.tags.isNotEmpty() || filter.states.isNotEmpty() || query != null) { throw IllegalArgumentException("Sorting by update with filters is not supported by this source.") } val url = buildString { @@ -61,9 +62,9 @@ internal class TrWebtoon(context: MangaLoaderContext) : append("/webtoon-listesi?page=") append(page.toString()) - filter.query?.let { + query?.let { query -> append("&q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt index 32bbca473..a66143f20 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt @@ -49,6 +49,7 @@ internal class UzayManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -56,9 +57,9 @@ internal class UzayManga(context: MangaLoaderContext) : append("?page=") append(page.toString()) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt index 1af345784..b6ead2d89 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt @@ -87,16 +87,17 @@ internal class HentaiUkrParser(context: MangaLoaderContext) : AbstractMangaParse } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query // Get all manga val json = allManga.get().toMutableList() - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { json.retainAll { item -> - item.getString("name").contains(filter.query, ignoreCase = true) || - item.getStringOrNull("eng_name")?.contains(filter.query, ignoreCase = true) == true || - item.getStringOrNull("orig_name")?.contains(filter.query, ignoreCase = true) == true || - item.getStringOrNull("author")?.contains(filter.query, ignoreCase = true) == true || - item.getStringOrNull("team")?.contains(filter.query, ignoreCase = true) == true + item.getString("name").contains(query, ignoreCase = true) || + item.getStringOrNull("eng_name")?.contains(query, ignoreCase = true) == true || + item.getStringOrNull("orig_name")?.contains(query, ignoreCase = true) == true || + item.getStringOrNull("author")?.contains(query, ignoreCase = true) == true || + item.getStringOrNull("team")?.contains(query, ignoreCase = true) == true } } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt index 3c8459ce2..2f8e1b943 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt @@ -89,6 +89,7 @@ internal class HoneyMangaParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val body = JSONObject() body.put("page", page) body.put("pageSize", PAGE_SIZE) @@ -115,15 +116,15 @@ internal class HoneyMangaParser(context: MangaLoaderContext) : } - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { // Search when { - filter.query.length < 3 -> throw IllegalArgumentException( + query.length < 3 -> throw IllegalArgumentException( "The query must contain at least 3 characters (Запит має містити щонайменше 3 символи)", ) page == searchPaginator.firstPage -> webClient - .httpGet(searchApi + filter.query.urlEncoded()) + .httpGet(searchApi + query.urlEncoded()) .parseJsonArray() else -> JSONArray() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt index 591b55991..2d385e68f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt @@ -42,13 +42,14 @@ internal class MangaInUaParser(context: MangaLoaderContext) : PagedMangaParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { - append("/index.php?do=search&subaction=search&search_start=$page&full_search=1&story=${filter.query}&titleonly=3") + !query.isNullOrEmpty() -> { + append("/index.php?do=search&subaction=search&search_start=$page&full_search=1&story=${query}&titleonly=3") } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt index 3204e4596..f626aa3a6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt @@ -55,10 +55,12 @@ internal class BFANGTeam (context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = urlBuilder().addPathSegment("manga") - if (!filter.query.isNullOrEmpty()) { - url.addEncodedQueryParameter("q", filter.query) + if (!query.isNullOrEmpty()) { + url.addEncodedQueryParameter("q", query) } // todo: need to use data-genre (num) instead of genre name @@ -75,9 +77,9 @@ internal class BFANGTeam (context: MangaLoaderContext) : } } - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { url.addQueryParameter("q", - filter.author.splitByWhitespace().joinToString(separator = "+") { it } + author.splitByWhitespace().joinToString(separator = "+") { it } ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt index 7fbbb3337..3069bcf1c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt @@ -56,9 +56,10 @@ internal class BlogTruyenParser(context: MangaLoaderContext) : private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> { - val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${filter.query.urlEncoded()}&p=$page" + !query.isNullOrEmpty() -> { + val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page" val searchContent = webClient.httpGet(searchUrl).parseHtml() .selectFirst("section.list-manga-bycate > div.list") parseMangaList(searchContent) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt index 794cedbef..4bc44cf20 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt @@ -60,9 +60,10 @@ internal class BlogTruyenVN(context: MangaLoaderContext) : private var cacheTags = suspendLazy(initializer = ::fetchTags) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> { - val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${filter.query.urlEncoded()}&p=$page" + !query.isNullOrEmpty() -> { + val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page" val searchContent = webClient.httpGet(searchUrl).parseHtml() .selectFirst("section.list-manga-bycate > div.list") parseMangaList(searchContent) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt index 9d194182a..c25f1c036 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt @@ -107,8 +107,9 @@ internal class CMangaParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder().apply { - if (filter.query.isNullOrEmpty() && (order == SortOrder.RELEVANCE || + if (query.isNullOrEmpty() && (order == SortOrder.RELEVANCE || order == SortOrder.POPULARITY_TODAY || order == SortOrder.POPULARITY_WEEK || order == SortOrder.POPULARITY_MONTH @@ -121,7 +122,7 @@ internal class CMangaParser(context: MangaLoaderContext) : addQueryParameter("team", "0") addQueryParameter("sort", "update") addQueryParameter("tag", filter.tags.joinToString(separator = ",") { it.key }) - addQueryParameter("string", filter.query.orEmpty()) + addQueryParameter("string", query.orEmpty()) addQueryParameter( "status", when (filter.states.oneOrThrowIfMany()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt index 5e8d4eb86..be2a11839 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt @@ -72,12 +72,13 @@ internal class CuuTruyenParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query // Move to urlBuilder val url = buildString { - if (!filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty()) { + if (!query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty()) { append("/mangas/search?q=") - if (!filter.query.isNullOrEmpty()) { - append(filter.query.urlEncoded()) + if (!query.isNullOrEmpty()) { + append(query.urlEncoded()) } // Bug from API: Select both ONGOING + FINISHED will return empty list for all case diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt index e705951ac..5c677e239 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt @@ -55,6 +55,7 @@ internal class DamCoNuong(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -90,9 +91,9 @@ internal class DamCoNuong(context: MangaLoaderContext) : append(filter.tags.joinTo(this, ",") { it.key }) } - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&filter[name]=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (filter.tagsExclude.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt index 4308edb7f..f27663525 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt @@ -40,14 +40,15 @@ internal class DuaLeoTruyen(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/tim-kiem.html") append("?key=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } filter.tags.isNotEmpty() -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt index fd00d70d8..2a4bc711c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt @@ -53,12 +53,13 @@ internal class GocTruyenTranh(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append("/baseapi/comics/filterComic") append("?keyword=") - append(filter.query?.urlEncoded() ?: "") + append(query?.urlEncoded() ?: "") if (filter.tags.isNotEmpty()) { append("&categories=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt index c83b4065c..809bd9c87 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt @@ -110,11 +110,12 @@ internal class GocTruyenTranhVui(context: MangaLoaderContext): } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append(apiUrl) append("/search?p=${page - 1}") - if (!filter.query.isNullOrBlank()) { - append("&searchValue=${filter.query.urlEncoded()}") + if (!query.isNullOrBlank()) { + append("&searchValue=${query.urlEncoded()}") } val sortValue = when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt index 2db5cd81c..8f6daea4e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt @@ -45,6 +45,7 @@ internal class HangTruyen(context: MangaLoaderContext) : PagedMangaParser(contex ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("/tim-kiem?page=") append(page) @@ -79,9 +80,9 @@ internal class HangTruyen(context: MangaLoaderContext) : PagedMangaParser(contex else -> "view_desc" }) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&keyword=") - val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part -> + val encodedQuery = query.splitByWhitespace().joinToString(separator = "+") { part -> part.urlEncoded() } append(encodedQuery) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt index 677ddc225..a3a2428e2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt @@ -37,13 +37,14 @@ internal class Hentai18VN(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } - val keyword = filter.query + val keyword = query val url = "http://$domain/search/html/1" val headers = Headers.Builder().add("X-Requested-With", "XMLHttpRequest").build() val response = webClient.httpPost(url.toHttpUrl(), payload = "keyword=$keyword", headers).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt index 5cabdcb92..a5b28623b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt @@ -63,12 +63,13 @@ internal class HentaiVNParser(context: MangaLoaderContext) : AbstractMangaParser ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1 urlBuilder() val searchUrl = - "/tim-kiem-truyen.html?key=${filter.query.urlEncoded()}&page=$page".toAbsoluteUrl(domain) + "/tim-kiem-truyen.html?key=${query.urlEncoded()}&page=$page".toAbsoluteUrl(domain) val docs = webClient.httpGet(searchUrl).parseHtml() parseMainList(docs, page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt index 6d50d37e8..599d64562 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt @@ -35,6 +35,7 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -73,13 +74,13 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : append(filter.tagsExclude.joinToString(",") { it.key }) } - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tim-kiem?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) return@buildString // end of buildString } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt index 9a0653fe3..b840b3857 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt @@ -71,12 +71,13 @@ internal class HentaiVnSU(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val apiUrl = urlBuilder().run { addPathSegments("api/library") when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { addPathSegments("search") - addQueryParameter("q", filter.query) + addQueryParameter("q", query) } filter.tags.isNotEmpty() -> { addPathSegments("advanced-search") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt index 28c297699..9714db35c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt @@ -55,14 +55,16 @@ internal class KuroNeko(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tac-gia/") - append(filter.author.lowercase().replace(" ", "-")) + append(author.lowercase().replace(" ", "-")) append("?sort=") append( @@ -109,9 +111,9 @@ internal class KuroNeko(context: MangaLoaderContext): }, ) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt index 9535a3439..686a2dc1a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt @@ -58,13 +58,14 @@ internal class LangGeekParser(context: MangaLoaderContext): override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query return when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } - val keyword = filter.query.urlEncoded() + val keyword = query.urlEncoded() val url = "https://$domain/wp-admin/admin-ajax.php?action=flatsome_ajax_search_products&query=${keyword}" val response = webClient.httpGet(url.toHttpUrl()).parseJson() parseMangaSearch(response) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt index ecd085494..b66862ae7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt @@ -50,15 +50,16 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/tim-kiem") append("?filter[name]=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (page > 1) { append("&page=") @@ -105,7 +106,7 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context, } } - if (filter.query.isNullOrEmpty()) { + if (query.isNullOrEmpty()) { append("&sort=") when (order) { SortOrder.POPULARITY -> append("-views") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt index 1f753bc54..a2d5458a3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt @@ -68,26 +68,28 @@ internal class MimiHentai(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions(availableTags = fetchTags()) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://") append("$domain/$apiSuffix") - if (!filter.query.isNullOrEmpty() || - !filter.author.isNullOrEmpty() || + if (!query.isNullOrEmpty() || + !author.isNullOrEmpty() || filter.tags.isNotEmpty() ) { append("/advance-search?page=") append(page) append("&max=18") // page size, avoid rate limit - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&name=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { append("&author=") - append(filter.author.urlEncoded()) + append(author.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt index 0c8faee3a..11589d28b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt @@ -56,6 +56,8 @@ internal class NhentaiWorld(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val urlBuilder = urlBuilder() urlBuilder.host(apiDomain) urlBuilder.addPathSegment("comic") @@ -81,10 +83,10 @@ internal class NhentaiWorld(context: MangaLoaderContext) : } // Search - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { urlBuilder.addQueryParameter( "search", - filter.query.splitByWhitespace().joinToString("+") { it } + query.splitByWhitespace().joinToString("+") { it } ) } @@ -102,10 +104,10 @@ internal class NhentaiWorld(context: MangaLoaderContext) : } // Author - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { urlBuilder.addQueryParameter( "author", - filter.author.splitByWhitespace().joinToString("+") { it } + author.splitByWhitespace().joinToString("+") { it } ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt index 3d3e7346e..ea67857dc 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt @@ -74,13 +74,14 @@ internal class OTruyenParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/v1/api/tim-kiem?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt index d900d7727..9980289cf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt @@ -43,14 +43,15 @@ internal class TruyenGG(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { buildString { append("https://") append(domain) append("/tim-kiem/trang-$page.html") append("?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt index e7a15234a..c3a7835c5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt @@ -42,6 +42,7 @@ internal class TruyenHentai18(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = when { filter.tags.isNotEmpty() -> { buildString { @@ -72,8 +73,8 @@ internal class TruyenHentai18(context: MangaLoaderContext): append("&page=") append(page) - if (!filter.query.isNullOrEmpty()) { - append("&query=${filter.query}") + if (!query.isNullOrEmpty()) { + append("&query=${query}") } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt index 55aea6d39..ccb70b877 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt @@ -34,6 +34,7 @@ internal class TruyenHentaiVN(context: MangaLoaderContext) : override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -48,9 +49,9 @@ internal class TruyenHentaiVN(context: MangaLoaderContext) : } } - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/tim-kiem-truyen/?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (page > 1) { append("&page=") append(page) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt index ce52c8dc2..6c9c986c8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt @@ -48,14 +48,15 @@ internal class TruyenQQ(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { buildString { append("https://") append(domain) append("/tim-kiem/trang-$page.html") append("?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt index fea97c295..164de66ee 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt @@ -45,6 +45,7 @@ internal class TruyenTranh3Q(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -58,9 +59,9 @@ internal class TruyenTranh3Q(context: MangaLoaderContext) : append("&") } - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt index beb652c94..3e7fd19d9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt @@ -40,12 +40,13 @@ internal class TuSachXinhXinh(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = urlBuilder() when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) return emptyList() // no paging url.addPathSegments("wp-admin/admin-ajax.php") - val payload = "action=searchtax&keyword=${filter.query.urlEncoded()}" + val payload = "action=searchtax&keyword=${query.urlEncoded()}" return webClient.httpPost(url.build(), payload) .parseJson().getJSONArray("data") .mapJSONNotNull(::parseSearchItem) @@ -58,7 +59,7 @@ internal class TuSachXinhXinh(context: MangaLoaderContext) : else -> url.addPathSegment("danh-sach-truyen") } - if (page > 1 && filter.query.isNullOrEmpty()) { + if (page > 1 && query.isNullOrEmpty()) { url.addPathSegments("page/$page") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt index 8220b2623..4a44392d8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt @@ -53,10 +53,11 @@ internal class VcomycsParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - if (!filter.query.isNullOrEmpty()) { + val query = filter.query + if (!query.isNullOrEmpty()) { if (page > 1) return emptyList() - val payload = "action=searchtax&keyword=${filter.query.urlEncoded()}" + val payload = "action=searchtax&keyword=${query.urlEncoded()}" return webClient.httpPost("/wp-admin/admin-ajax.php".toAbsoluteUrl(domain), payload) .parseJson().getJSONArray("data") .mapJSONNotNull { jo -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt index fb6978f72..7c25a8444 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt @@ -86,6 +86,8 @@ internal abstract class YuriGardenParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query + val author = filter.author val url = buildString { append("https://") append(apiSuffix) @@ -104,9 +106,9 @@ internal abstract class YuriGardenParser( else -> "newest" // default }) - if (!filter.query.isNullOrEmpty()) { + if (!query.isNullOrEmpty()) { append("&search=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } filter.states.oneOrThrowIfMany()?.let { state -> @@ -128,14 +130,14 @@ internal abstract class YuriGardenParser( append(filter.tags.joinToString(separator = ",") { it.key }) } - if (!filter.author.isNullOrEmpty()) { + if (!author.isNullOrEmpty()) { clear() append("https://") append(apiSuffix) append("/creators/authors/") append( - filter.author.substringAfter("(").substringBefore(")") + author.substringAfter("(").substringBefore(")") ) return@buildString // end of buildString diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt index 72aead355..21bdc8874 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt @@ -59,9 +59,10 @@ internal class YurinekoParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val listUrl = when { - !filter.query.isNullOrEmpty() -> { - "/search?query=${filter.query.urlEncoded()}&page=$page" + !query.isNullOrEmpty() -> { + "/search?query=${query.urlEncoded()}&page=$page" } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt index 20a97317a..26e67d7c8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt @@ -42,18 +42,19 @@ internal abstract class VmpParser( protected open val geneUrl = "genero/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append(listUrl) append("/page/") append(page.toString()) append("?s=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt index 63dfdadd4..178518096 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt @@ -81,15 +81,16 @@ internal abstract class WpComicsParser( protected open val datePattern = "dd/MM/yy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append(listUrl) append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt index 70cd7406e..045f7ac00 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt @@ -50,13 +50,14 @@ internal class XoxoComics(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append("/search-comic?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt index 30320350c..498f31840 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt @@ -15,14 +15,15 @@ internal class MangaRaw(context: MangaLoaderContext) : override val listUrl = "/search/manga" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append(listUrl) append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt index 3fd8e2701..867ec568a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt @@ -35,6 +35,7 @@ internal class DocTruyen3Q(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -61,8 +62,8 @@ internal class DocTruyen3Q(context: MangaLoaderContext) : } } - if (filter.query?.isNotEmpty() == true) { - params.add("keyword=${filter.query.urlEncoded()}") + if (query?.isNotEmpty() == true) { + params.add("keyword=${query.urlEncoded()}") } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt index 9e891b48b..15895d1f7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt @@ -35,16 +35,17 @@ internal class MeHentaiVN(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { // url template: https://www.mehentaivn.xyz/tim-truyen?keyword=${query} - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append(listUrl) append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) if (page > 1) { append("&page=$page") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt index 4f912a30f..a47405e44 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt @@ -40,9 +40,10 @@ internal class NetTruyen1975(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -51,7 +52,7 @@ internal class NetTruyen1975(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt index c590fa9f2..fffe42926 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt @@ -40,9 +40,10 @@ internal class NetTruyenFE(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -51,7 +52,7 @@ internal class NetTruyenFE(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt index bbdf27370..9c09b6234 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt @@ -36,9 +36,10 @@ internal class NetTruyenHE(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -47,7 +48,7 @@ internal class NetTruyenHE(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt index f34569aff..41835faa7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt @@ -40,9 +40,10 @@ internal class NetTruyenLL(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -51,7 +52,7 @@ internal class NetTruyenLL(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt index 35dce2270..2841126c6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt @@ -40,9 +40,10 @@ internal class NetTruyenSSR(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -51,7 +52,7 @@ internal class NetTruyenSSR(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt index 15f46d64c..305c8558f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt @@ -40,9 +40,10 @@ internal class NetTruyenUU(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val response = when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -51,7 +52,7 @@ internal class NetTruyenUU(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt index 509b4a3e9..5d41066b0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt @@ -35,6 +35,7 @@ internal class TopTruyen(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -61,8 +62,8 @@ internal class TopTruyen(context: MangaLoaderContext) : } } - if (filter.query?.isNotEmpty() == true) { - params.add("keyword=${filter.query.urlEncoded()}") + if (query?.isNotEmpty() == true) { + params.add("keyword=${query.urlEncoded()}") } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt index 168cece52..f4381a5ba 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt @@ -90,6 +90,7 @@ internal abstract class ZeistMangaParser( protected open val datePattern = "yyyy-MM-dd" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val startIndex = maxMangaResults * (page - 1) + 1 val url = buildString { @@ -98,7 +99,7 @@ internal abstract class ZeistMangaParser( append("/feeds/posts/default/-/") when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { append(mangaCategory) append("?alt=json&orderby=published&max-results=") append((maxMangaResults + 1).toString()) @@ -107,7 +108,7 @@ internal abstract class ZeistMangaParser( append("&q=label:") append(mangaCategory) append("+") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt index 3be5286d6..956ff117a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt @@ -46,14 +46,15 @@ internal class Baozimh(context: MangaLoaderContext) : private val tagsMap = suspendLazy(initializer = ::parseTags) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query when { - !filter.query.isNullOrEmpty() -> { + !query.isNullOrEmpty() -> { if (page > 1) return emptyList() val url = buildString { append("https://") append(domain) append("/search?q=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } return parseMangaListSearch(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt index e6e127723..fb0276c23 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt @@ -141,6 +141,7 @@ internal class KomiicParser(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val offset = (page - paginator.firstPage) * pageSize val sexyLevel = parseSexyLevel(filter) // 严格使用原始查询:始终将 sexyLevel 传递给远端(若未选择则为 null) @@ -157,10 +158,10 @@ internal class KomiicParser(context: MangaLoaderContext) : else -> "DATE_UPDATED" } - val base = if (!filter.query.isNullOrEmpty()) { - val list = search(filter.query) + val base = if (!query.isNullOrEmpty()) { + val list = search(query) if (page == paginator.firstPage) { - searchFirstPageCacheQuery = filter.query + searchFirstPageCacheQuery = query searchFirstPageCache = list } list diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt index 4a6d65f6c..d04941994 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt @@ -162,6 +162,7 @@ internal class ManhuaguiParser(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { + val query = filter.query // Flag of whether there is title query param var flagHasTitleQuery = false @@ -170,7 +171,7 @@ internal class ManhuaguiParser(context: MangaLoaderContext) : var queryFull: String? var orderStr: String? var pageStr: String? - if (filter.query == null) { + if (query == null) { append(listUrl.toAbsoluteUrl(domain)) queryFull = listOfNotNull( filter.locale, @@ -189,7 +190,7 @@ internal class ManhuaguiParser(context: MangaLoaderContext) : } else { flagHasTitleQuery = true append(searchUrl.toAbsoluteUrl(domain)) - queryFull = filter.query + queryFull = query orderStr = when (order) { SortOrder.POPULARITY -> "_o1" SortOrder.NEWEST -> "_o2" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt index f6bb36b20..a97e62071 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt @@ -78,6 +78,7 @@ internal abstract class ZMangaParser( // https://komikindo.info/advanced-search/?title=the&author=the&artist=the&yearx=2020&status=ongoing&type=Manga&order=update override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val query = filter.query val url = buildString { append("https://") append(domain) @@ -100,15 +101,15 @@ internal abstract class ZMangaParser( else -> append("update") } - filter.query?.let { + query?.let { query -> append("&title=") - append(filter.query.urlEncoded()) + append(query.urlEncoded()) } // author - // filter.author?.let { + // author?.let { author -> // append("&author=") - // append(filter.author.urlEncoded()) + // append(author.urlEncoded()) // } // artist diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt deleted file mode 100644 index 35212e304..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -internal fun T?.assertNotNull(name: String): T? { - assert(this != null) { - "Value $name is null" - } - return this -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt deleted file mode 100644 index ecbf8cbf6..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.koitharu.kotatsu.core.parser - -import org.jsoup.nodes.Element -import org.koitharu.kotatsu.parsers.util.attrOrNull -import org.koitharu.kotatsu.parsers.util.nullIfEmpty -import org.koitharu.kotatsu.parsers.util.splitByWhitespace - -/** - * Utility class for parsing the `background` property of css - */ -public class CSSBackground private constructor( - public val url: String, - public val left: Int, - public val top: Int, - public val width: Int, - public val height: Int, -) { - - public val right: Int - get() = left + width - - public val bottom: Int - get() = top + height - - internal companion object { - - fun parse(element: Element): CSSBackground? { - val style = element.attrOrNull("style") ?: return null - val attrs = style.split(';').associate { - val trimmed = it.trim() - trimmed.substringBefore(':') to trimmed.substringAfter(':', "") - } - val width = attrs["width"]?.toPx() ?: return null - val height = attrs["height"]?.toPx() ?: return null - val bg = attrs["background"]?.substringAfter("url")?.splitByWhitespace() ?: return null - val url = bg.firstOrNull()?.removeSurrounding("(", ")")?.nullIfEmpty() ?: return null - val x = bg.getOrNull(1)?.toPx() ?: 0 - val y = bg.getOrNull(2)?.toPx() ?: 0 - return CSSBackground( - url = url, - left = -x, - top = y, - width = width, - height = height, - ) - } - - private fun String.toPx(): Int? { - return removeSuffix("px").toIntOrNull() - } - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt deleted file mode 100644 index afad7166d..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import org.json.JSONArray -import org.json.JSONObject -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.util.json.asTypedList - -@InternalParsersApi -public inline fun List.mapChapters( - reversed: Boolean = false, - transform: (index: Int, T) -> MangaChapter?, -): List { - val builder = ChaptersListBuilder(collectionSize()) - var index = 0 - val elements = if (reversed) this.asReversed() else this - for (item in elements) { - if (builder.add(transform(index, item))) { - index++ - } - } - return builder.toList() -} - -@InternalParsersApi -public inline fun JSONArray.mapChapters( - reversed: Boolean = false, - transform: (index: Int, JSONObject) -> MangaChapter?, -): List = asTypedList().mapChapters(reversed, transform) - -@InternalParsersApi -public inline fun List.flatMapChapters( - reversed: Boolean = false, - transform: (T) -> Iterable, -): List { - val builder = ChaptersListBuilder(collectionSize()) - val elements = if (reversed) this.asReversed() else this - for (item in elements) { - builder.addAll(transform(item)) - } - return builder.toList() -} - -@PublishedApi -internal fun Iterable.collectionSize(): Int { - return if (this is Collection<*>) this.size else 10 -} - -@PublishedApi -internal class ChaptersListBuilder(initialSize: Int) { - - private val ids = HashSet(initialSize) - private val list = ArrayList(initialSize) - - fun add(chapter: MangaChapter?): Boolean { - return chapter != null && ids.add(chapter.id) && list.add(chapter) - } - - fun addAll(chapters: Iterable) { - if (chapters is Collection<*>) { - list.ensureCapacity(list.size + chapters.size) - } - chapters.forEach { add(it) } - } - - operator fun plusAssign(chapter: MangaChapter?) { - add(chapter) - } - - fun reverse() { - list.reverse() - } - - fun toList(): List = list -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt deleted file mode 100644 index 38d0dfa7b..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt +++ /dev/null @@ -1,101 +0,0 @@ -@file:JvmName("CollectionUtils") - -package org.koitharu.kotatsu.parsers.util - -import androidx.collection.ArrayMap -import androidx.collection.ArraySet -import java.util.* - -public fun Collection<*>?.sizeOrZero(): Int = this?.size ?: 0 - -public fun MutableCollection.replaceWith(subject: Iterable) { - clear() - addAll(subject) -} - -public fun > Iterable>.flattenTo(destination: C): C { - for (element in this) { - destination.addAll(element) - } - return destination -} - -public fun List.medianOrNull(): T? = when { - isEmpty() -> null - else -> get((size / 2).coerceIn(indices)) -} - -public inline fun Collection.mapToSet(transform: (T) -> R): Set { - return mapTo(ArraySet(size), transform) -} - -public inline fun Collection.mapNotNullToSet(transform: (T) -> R?): Set { - val destination = ArraySet(size) - for (item in this) { - destination.add(transform(item) ?: continue) - } - return destination -} - -public inline fun Array.mapToArray(transform: (T) -> R): Array = Array(size) { i -> - transform(get(i)) -} - -public fun List>.toMutableMap(): MutableMap = toMap(ArrayMap(size)) - -public fun MutableList.move(sourceIndex: Int, targetIndex: Int) { - if (sourceIndex <= targetIndex) { - Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) - } else { - Collections.rotate(subList(targetIndex, sourceIndex + 1), 1) - } -} - -public fun Iterator.nextOrNull(): T? = if (hasNext()) next() else null - -public inline fun Collection.associateGrouping(transform: (T) -> Pair): Map> { - val result = LinkedHashMap>(size) - for (item in this) { - val (k, v) = transform(item) - result.getOrPut(k) { LinkedHashSet() }.add(v) - } - return result -} - -public fun MutableMap.incrementAndGet(key: K): Int { - var value = get(key) ?: 0 - value++ - put(key, value) - return value -} - -public inline fun MutableSet(size: Int, init: (index: Int) -> T): MutableSet { - val set = ArraySet(size) - repeat(size) { index -> set.add(init(index)) } - return set -} - -public inline fun Set(size: Int, init: (index: Int) -> T): Set = when (size) { - 0 -> emptySet() - 1 -> Collections.singleton(init(0)) - else -> MutableSet(size, init) -} - -@Suppress("UNCHECKED_CAST") -public inline fun Collection.mapToArray(transform: (T) -> R): Array { - val result = arrayOfNulls(size) - forEachIndexed { index, t -> result[index] = transform(t) } - return result as Array -} - -public fun Array.toArraySet(): Set = when (size) { - 0 -> emptySet() - 1 -> setOf(first()) - else -> ArraySet(this) -} - -public fun Collection.toArraySet(): Set = when (size) { - 0 -> emptySet() - 1 -> setOf(first()) - else -> ArraySet(this) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt deleted file mode 100644 index c24521449..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.CompletionHandler -import okhttp3.Call -import okhttp3.Callback -import okhttp3.Response -import okhttp3.internal.closeQuietly -import java.io.IOException -import kotlin.coroutines.resumeWithException - -internal class ContinuationCallCallback( - private val call: Call, - private val continuation: CancellableContinuation, -) : Callback, CompletionHandler { - - override fun onResponse(call: Call, response: Response) { - continuation.resume(response) { _, value, _ -> - value.closeQuietly() - } - } - - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } - - override fun invoke(cause: Throwable?) { - runCatching { - call.cancel() - }.onFailure { e -> - cause?.addSuppressed(e) - } - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt deleted file mode 100644 index b3534c880..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt +++ /dev/null @@ -1,48 +0,0 @@ -@file:JvmName("CookieJarUtils") - -package org.koitharu.kotatsu.parsers.util - -import okhttp3.Cookie -import okhttp3.CookieJar -import okhttp3.HttpUrl - -public fun CookieJar.insertCookies(domain: String, vararg cookies: String) { - val url = safeUrlOf(domain) ?: return - saveFromResponse( - url, - cookies.mapNotNull { - Cookie.parse(url, it) - }, - ) -} - -public fun CookieJar.insertCookie(domain: String, cookie: Cookie) { - val url = safeUrlOf(domain) ?: return - saveFromResponse(url, listOf(cookie)) -} - -public fun CookieJar.getCookies(domain: String): List { - val url = safeUrlOf(domain) ?: return emptyList() - return loadForRequest(url) -} - -public fun CookieJar.copyCookies(oldDomain: String, newDomain: String, names: Array? = null) { - val url = HttpUrl.Builder() - .scheme(SCHEME_HTTPS) - .host(oldDomain) - var cookies = loadForRequest(url.build()) - if (names != null) { - cookies = cookies.filter { c -> c.name in names } - } - url.host(newDomain) - saveFromResponse(url.build(), cookies) -} - -private fun safeUrlOf(domain: String): HttpUrl? = try { - HttpUrl.Builder() - .scheme(SCHEME_HTTPS) - .host(domain) - .build() -} catch (_: IllegalArgumentException) { - null -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt deleted file mode 100644 index 0a902c1aa..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException - -public fun Iterable.cancelAll(cause: CancellationException? = null) { - forEach { it.cancel(cause) } -} - -public suspend fun Iterable>.awaitFirst(): T { - return channelFlow { - for (deferred in this@awaitFirst) { - launch { - send(deferred.await()) - } - } - }.first().also { this@awaitFirst.cancelAll() } -} - -public suspend fun Collection>.awaitFirst(condition: (T) -> Boolean): T { - return channelFlow { - for (deferred in this@awaitFirst) { - launch { - val result = deferred.await() - if (condition(result)) { - send(result) - } - } - } - }.first().also { this@awaitFirst.cancelAll() } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt deleted file mode 100644 index d615d14a8..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt +++ /dev/null @@ -1,127 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import java.security.MessageDigest -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING" -private const val AES = "AES" -private const val KDF_DIGEST = "MD5" - -/** - * Conforming with CryptoJS AES method - */ -@InternalParsersApi -public class CryptoAES( - private val context: MangaLoaderContext, -) { - - /** - * Decrypt using CryptoJS defaults compatible method. - * Uses KDF equivalent to OpenSSL's EVP_BytesToKey function - * - * http://stackoverflow.com/a/29152379/4405051 - * @param cipherText base64 encoded ciphertext - * @param password passphrase - */ - @Throws(Exception::class) - public fun decrypt(cipherText: String, password: String): String { - val ctBytes = context.decodeBase64(cipherText) - val saltBytes = ctBytes.copyOfRange(8, 16) - val cipherTextBytes = ctBytes.copyOfRange(16, ctBytes.size) - val md5: MessageDigest = MessageDigest.getInstance(KDF_DIGEST) - val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5) - return decryptAES( - cipherTextBytes, - keyAndIV.getOrNull(0) ?: ByteArray(32), - keyAndIV.getOrNull(1) ?: ByteArray(16), - ) - } - - /** - * Decrypt using CryptoJS defaults compatible method. - * - * @param cipherText base64 encoded ciphertext - * @param keyBytes key as a bytearray - * @param ivBytes iv as a bytearray - */ - @Throws(Exception::class) - public fun decrypt(cipherText: String, keyBytes: ByteArray, ivBytes: ByteArray): String { - val cipherTextBytes = context.decodeBase64(cipherText) - return decryptAES(cipherTextBytes, keyBytes, ivBytes) - } - - /** - * Decrypt using CryptoJS defaults compatible method. - * - * @param cipherTextBytes encrypted text as a bytearray - * @param keyBytes key as a bytearray - * @param ivBytes iv as a bytearray - */ - @Throws(Exception::class) - private fun decryptAES(cipherTextBytes: ByteArray, keyBytes: ByteArray, ivBytes: ByteArray): String { - val cipher = Cipher.getInstance(HASH_CIPHER) - val keyS = SecretKeySpec(keyBytes, AES) - cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(ivBytes)) - return cipher.doFinal(cipherTextBytes).toString(Charsets.UTF_8) - } - - /** - * Generates a key and an initialization vector (IV) with the given salt and password. - * - * https://stackoverflow.com/a/41434590 - * This method is equivalent to OpenSSL's EVP_BytesToKey function - * (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c). - * By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data. - * - * @param keyLength the length of the generated key (in bytes) - * @param ivLength the length of the generated IV (in bytes) - * @param iterations the number of digestion rounds - * @param salt the salt data (8 bytes of data or `null`) - * @param password the password data (optional) - * @param md the message digest algorithm to use - * @return an two-element array with the generated key and IV - */ - @Suppress("SameParameterValue") - @Throws(Exception::class) - private fun generateKeyAndIV( - keyLength: Int, - ivLength: Int, - iterations: Int, - salt: ByteArray?, - password: ByteArray, - md: MessageDigest, - ): Array { - val digestLength = md.digestLength - val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength - val generatedData = ByteArray(requiredLength) - var generatedLength = 0 - md.reset() - - // Repeat process until sufficient data has been generated - while (generatedLength < keyLength + ivLength) { - - // Digest data (last digest if available, password data, salt if available) - if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength) - md.update(password) - if (salt != null) md.update(salt, 0, 8) - md.digest(generatedData, generatedLength, digestLength) - - // additional rounds - for (i in 1 until iterations) { - md.update(generatedData, generatedLength, digestLength) - md.digest(generatedData, generatedLength, digestLength) - } - generatedLength += digestLength - } - - // Copy key and IV into separate byte arrays - val result = arrayOfNulls(2) - result[0] = generatedData.copyOfRange(0, keyLength) - if (ivLength > 0) result[1] = generatedData.copyOfRange(keyLength, keyLength + ivLength) - return result - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt deleted file mode 100644 index 2cb7cfb23..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt +++ /dev/null @@ -1,13 +0,0 @@ -@file:JvmName("EnumUtils") - -package org.koitharu.kotatsu.parsers.util - -import kotlin.enums.EnumEntries - -public fun > EnumEntries.names(): Array = Array(size) { i -> - get(i).name -} - -public fun > EnumEntries.find(name: String): E? { - return find { x -> x.name == name } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt deleted file mode 100644 index b01fc794d..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.jsoup.nodes.Element -import org.koitharu.kotatsu.parsers.model.Favicon -import org.koitharu.kotatsu.parsers.model.Favicons -import org.koitharu.kotatsu.parsers.network.WebClient -import org.koitharu.kotatsu.parsers.util.json.mapJSON - -public class FaviconParser( - private val webClient: WebClient, - private val domain: String, -) { - - public suspend fun parseFavicons(): Favicons = withContext(Dispatchers.Default) { - val url = "https://$domain" - val doc = webClient.httpGet(url).parseHtml() - val result = HashSet() - val manifestLink = doc.getElementsByAttributeValue("rel", "manifest").firstOrNull() - ?.attrAsAbsoluteUrlOrNull("href") - if (manifestLink != null) { - runCatchingCancellable { - parseManifest(manifestLink) - }.onSuccess { manifest -> - result += manifest - } - } - val links = doc.getElementsByAttributeValueContaining("rel", "icon") - links.mapNotNullTo(result) { link -> - parseLink(link) - } - val touchIcons = doc.getElementsByAttributeValue("rel", "apple-touch-icon") - touchIcons.mapNotNullTo(result) { link -> - parseLink(link) - } - if (result.isEmpty()) { - result.add(createFallback()) - } - Favicons(result, url) - } - - private fun parseLink(link: Element): Favicon? { - val href = link.attrAsAbsoluteUrlOrNull("href") - if (href == null || href.endsWith('/')) { - return null - } - val sizes = link.attr("sizes") - return Favicon( - url = href, - size = parseSize(sizes), - rel = link.attrOrNull("rel"), - ) - } - - private fun parseSize(sizes: String): Int { - if (sizes.isEmpty() || sizes == "any") { - return 0 - } - return sizes.substringBefore(' ') - .split('x', 'X', '*') - .firstNotNullOfOrNull { it.toIntOrNull() } - ?: 0 - } - - private suspend fun parseManifest(url: String): List { - val json = webClient.httpGet(url).parseJson() - val icons = json.optJSONArray("icons") ?: return emptyList() - return icons.mapJSON { jo -> - Favicon( - url = jo.getString("src").resolveLink(), - size = parseSize(jo.getString("sizes")), - rel = null, - ) - } - } - - private fun createFallback(): Favicon { - val href = "https://$domain/favicon.ico" - return Favicon( - url = href, - size = 0, - rel = null, - ) - } - - private fun String.resolveLink(): String = when { - startsWith("http:") || startsWith("https:") -> this - startsWith('/') -> "https://$domain$this" - else -> "https://$domain/$this" - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt deleted file mode 100644 index b9bdb0286..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt +++ /dev/null @@ -1,225 +0,0 @@ -@file:JvmName("JsoupUtils") - -package org.koitharu.kotatsu.parsers.util - -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.jsoup.nodes.Element -import org.jsoup.select.Elements -import org.jsoup.select.QueryParser -import org.jsoup.select.Selector -import org.koitharu.kotatsu.core.parser.CSSBackground -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.exception.ParseException -import kotlin.contracts.contract - -public val Element.host: String? - get() { - val uri = baseUri() - return if (uri.isEmpty()) { - null - } else { - uri.toHttpUrlOrNull()?.host - } - } - -/** - * Return an attribute value or null if it is missing or empty - * @see [Element.attr] which returns empty string instead of null - */ -public fun Element.attrOrNull(attributeKey: String): String? = attr(attributeKey).takeUnless { it.isBlank() }?.trim() - -/** - * Return an attribute value or throw an exception if it is missing - * @see [Element.attr] which returns empty string instead - */ -public fun Element.attrOrThrow(attributeKey: String): String = if (hasAttr(attributeKey)) { - attr(attributeKey) -} else { - throw ParseException("Attribute \"$attributeKey\" is missing at element \"$this\"", baseUri()) -} - -/** - * Return an attribute value as relative url or null if it is missing or empty - * @see attrAsRelativeUrl - * @see attrAsAbsoluteUrlOrNull - * @see attrAsAbsoluteUrl - */ -public fun Element.attrAsRelativeUrlOrNull(attributeKey: String): String? { - val attr = attrOrNull(attributeKey) ?: return null - if (attr.isEmpty() || attr.startsWith("data:")) { - return null - } - if (attr.startsWith('/')) { - return attr - } - val host = baseUri().toHttpUrlOrNull()?.host ?: return null - return attr.substringAfter(host) -} - -/** - * Return an attribute value as relative url or throw an exception if it is missing or empty - * @throws IllegalArgumentException if attribute value is missing or empty - * @see attrAsRelativeUrlOrNull - * @see attrAsAbsoluteUrlOrNull - * @see attrAsAbsoluteUrl - */ -public fun Element.attrAsRelativeUrl(attributeKey: String): String { - return requireNotNull(attrAsRelativeUrlOrNull(attributeKey)) { - "Cannot get relative url for $attributeKey: \"${attr(attributeKey)}\"" - } -} - -/** - * Return an attribute value as absolute url or null if it is missing or empty - * @see attrAsAbsoluteUrl - * @see attrAsRelativeUrl - * @see attrAsRelativeUrlOrNull - */ -public fun Element.attrAsAbsoluteUrlOrNull(attributeKey: String): String? { - val attr = attrOrNull(attributeKey) ?: return null - if (attr.isEmpty() || attr.startsWith("data:")) { - return null - } - return (baseUri().toHttpUrlOrNull()?.resolve(attr) ?: return null).toString() -} - -/** - * Return an attribute value as absolute url or throw an exception if it is missing or empty - * @throws IllegalArgumentException if attribute value is missing or empty - * @see attrAsAbsoluteUrlOrNull - * @see attrAsRelativeUrl - * @see attrAsRelativeUrlOrNull - */ -public fun Element.attrAsAbsoluteUrl(attributeKey: String): String { - return parseNotNull(attrAsAbsoluteUrlOrNull(attributeKey)) { - "Cannot get absolute url for $attributeKey: \"${attr(attributeKey)}\"" - } -} - -/** - * Return css value from `style` attribute or null if it is missing - */ -public fun Element.styleValueOrNull(property: String): String? { - val regex = Regex("${Regex.escape(property)}\\s*:\\s*[^;]+") - val css = attr("style").find(regex) ?: return null - return css.substringAfter(':').removeSuffix(';').trim() -} - -/** - * Like a `expectFirst` but with detailed error message - */ -public fun Element.selectFirstOrThrow(cssQuery: String): Element = parseNotNull(Selector.selectFirst(cssQuery, this)) { - "Cannot find \"$cssQuery\"" -} - -public fun Element.selectOrThrow(cssQuery: String): Elements { - return Selector.select(cssQuery, this).ifEmpty { - throw ParseException("Empty result for \"$cssQuery\"", baseUri()) - } -} - -public fun Element.requireElementById(id: String): Element = parseNotNull(getElementById(id)) { - "Cannot find \"#$id\"" -} - -public fun Element.selectLast(cssQuery: String): Element? = select(cssQuery).lastOrNull() - -public fun Element.selectLastOrThrow(cssQuery: String): Element = parseNotNull(selectLast(cssQuery)) { - "Cannot find \"$cssQuery\"" -} - -public fun Element.textOrNull(): String? = text().nullIfEmpty() - -public fun Elements.textOrNull(): String? = text().nullIfEmpty() - -public fun Element.ownTextOrNull(): String? = ownText().nullIfEmpty() - -public fun Element.selectFirstParent(query: String): Element? { - val selector = QueryParser.parse(query) - val parents = parents() - val root = parents.lastOrNull() ?: return null - return parents.firstOrNull { - selector.matches(root, it) - } -} - -public fun Element.selectFirstParentOrThrow(query: String): Element = parseNotNull(selectFirstParent(query)) { - "Cannot find parent \"$query\"" -} - -/** - * Return a first non-empty attribute value of [names] or null if it is missing or empty - */ -public fun Element.attrOrNull(vararg names: String): String? { - for (name in names) { - val value = attr(name) - if (value.isNotEmpty()) { - return value.trim() - } - } - return null -} - -@InternalParsersApi -@JvmOverloads -public fun Element.src( - names: Array = arrayOf( - "data-src", - "data-cfsrc", - "data-original", - "data-original-src", - "data-cdn", - "data-sizes", - "data-lazy-src", - "data-srcset", - "original-src", - "data-wpfc-original-src", - "src", - ), -): String? { - for (name in names) { - val value = attrAsAbsoluteUrlOrNull(name) - if (value != null) { - return value - } - } - return null -} - -@InternalParsersApi -public fun Element.requireSrc(): String = parseNotNull(src()) { - "Image src not found" -} - -public fun Element.backgroundOrNull(): CSSBackground? = CSSBackground.parse(this) - -public fun Element.metaValue(itemprop: String): String? = getElementsByAttributeValue("itemprop", itemprop) - .firstNotNullOfOrNull { element -> - element.attrOrNull("content") - } - -public fun String.cssUrl(): String? { - val fromIndex = indexOf("url(") - if (fromIndex == -1) { - return null - } - val toIndex = indexOf(')', startIndex = fromIndex) - return if (toIndex == -1) { - null - } else { - substring(fromIndex + 4, toIndex).trim() - } -} - -internal inline fun Element.parseNotNull(value: T?, lazyMessage: () -> String): T { - contract { - returns() implies (value != null) - } - - if (value == null) { - val message = lazyMessage() - throw ParseException(message, baseUri()) - } else { - return value - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt index cb542f7d3..f5c3a85ae 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt @@ -20,8 +20,9 @@ public class LinkResolver internal constructor( public suspend fun getSource(): MangaParserSource? = source.get() public suspend fun getManga(): Manga? { - val parser = context.newParserInstance(source.get() ?: return null) - return parser.resolveLink(this, link) ?: resolveManga(parser) + val parserSource = source.get() ?: return null + val parser = context.newParserInstance(parserSource) + return parser.resolveLink(link) } private suspend fun resolveSource(): MangaParserSource? = runInterruptible(Dispatchers.Default) { @@ -37,7 +38,7 @@ public class LinkResolver internal constructor( null } - internal suspend fun resolveManga( + public suspend fun resolveManga( parser: MangaParser, url: String = link.toString().toRelativeUrl(link.host), id: Long = parser.generateUid(url), @@ -74,14 +75,14 @@ public class LinkResolver internal constructor( seed.authors.isNotEmpty() -> seed.authors.first() else -> return seed // unfortunately we do not know a real manga title so unable to find it } - val resolved = runCatchingCancellable { + val resolved = runCatching { val list = parser.getList(0, parser.bestSortOrder(), MangaListFilter(query = query)) list.singleOrNull { manga -> isSameUrl(manga.publicUrl) } }.getOrNull() if (resolved == null) { return seed } - return runCatchingCancellable { + return runCatching { parser.getDetails(resolved) }.getOrElse { resolved.copy( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt deleted file mode 100644 index 7f34cd8de..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import okhttp3.HttpUrl -import org.jsoup.nodes.Element -import org.koitharu.kotatsu.parsers.ErrorMessages -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.MangaParser -import org.koitharu.kotatsu.parsers.core.AbstractMangaParser -import org.koitharu.kotatsu.parsers.exception.ParseException -import org.koitharu.kotatsu.parsers.model.* - - -/** - * Create a unique id for [Manga]/[MangaChapter]/[MangaPage]. - * @param url must be relative url, without a domain - * @see [Manga.id] - * @see [MangaChapter.id] - * @see [MangaPage.id] - */ -@InternalParsersApi -public fun MangaParser.generateUid(url: String): Long { - var h = LONG_HASH_SEED - source.name.forEach { c -> - h = 31 * h + c.code - } - url.forEach { c -> - h = 31 * h + c.code - } - return h -} - -/** - * Create a unique id for [Manga]/[MangaChapter]/[MangaPage]. - * @param id an internal identifier - * @see [Manga.id] - * @see [MangaChapter.id] - * @see [MangaPage.id] - */ -@InternalParsersApi -public fun MangaParser.generateUid(id: Long): Long { - var h = LONG_HASH_SEED - source.name.forEach { c -> - h = 31 * h + c.code - } - h = 31 * h + id - return h -} - -@InternalParsersApi -public fun Element.parseFailed(message: String? = null): Nothing { - throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null) -} - -@InternalParsersApi -public fun Set?.oneOrThrowIfMany(): MangaTag? = oneOrThrowIfMany( - ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED, -) - -@InternalParsersApi -public fun Set?.oneOrThrowIfMany(): MangaState? = oneOrThrowIfMany( - ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED, -) - -@InternalParsersApi -public fun Set?.oneOrThrowIfMany(): ContentType? = oneOrThrowIfMany( - ErrorMessages.FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED, -) - -@InternalParsersApi -public fun Set?.oneOrThrowIfMany(): Demographic? = oneOrThrowIfMany( - ErrorMessages.FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED, -) - -@InternalParsersApi -public fun Set?.oneOrThrowIfMany(): ContentRating? = oneOrThrowIfMany( - ErrorMessages.FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED, -) - -private fun Set?.oneOrThrowIfMany(msg: String): T? = when { - isNullOrEmpty() -> null - size == 1 -> first() - else -> throw IllegalArgumentException(msg) -} - -@InternalParsersApi -public fun AbstractMangaParser.getDomain(subdomain: String): String { - val domain = domain - return subdomain + "." + domain.removePrefix("www.") -} - -@InternalParsersApi -public fun MangaParser.urlBuilder(subdomain: String? = null): HttpUrl.Builder { - return HttpUrl.Builder() - .scheme(SCHEME_HTTPS) - .host(if (subdomain == null) domain else "$subdomain.$domain") -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt new file mode 100644 index 000000000..e5e9798d8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.parsers.util + +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.model.* + +/** + * Extension properties to help with smart-casting across modules. + * These return a local copy of the property value. + */ +public val MangaListFilter.queryOrNull: String? + get() = query + +public val MangaListFilter.authorOrNull: String? + get() = author + +public inline fun T?.ifNotNull(block: (T) -> Unit) { + if (this != null) block(this) +} + +public fun String?.nullIfEmpty(): String? = if (isNullOrEmpty()) null else this diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt deleted file mode 100644 index 3c63a6669..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -@file:JvmName("MangaParsersUtils") - -package org.koitharu.kotatsu.parsers.util - -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaListFilter -import kotlin.contracts.contract - -public fun MangaListFilter?.isNullOrEmpty(): Boolean { - contract { - returns(false) implies (this@isNullOrEmpty != null) - } - return this == null || this.isEmpty() -} - -public fun Collection.findById(chapterId: Long): MangaChapter? = find { x -> - x.id == chapterId -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt deleted file mode 100644 index 53227fecc..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt +++ /dev/null @@ -1,85 +0,0 @@ -@file:JvmName("NumberUtils") - -package org.koitharu.kotatsu.parsers.util - -import java.text.DecimalFormat -import java.text.NumberFormat -import java.util.* -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.math.absoluteValue - -public fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String { - val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat - val symbols = formatter.decimalFormatSymbols - if (thousandsSep != null) { - symbols.groupingSeparator = thousandsSep - formatter.isGroupingUsed = true - } else { - formatter.isGroupingUsed = false - } - symbols.decimalSeparator = decPoint - formatter.decimalFormatSymbols = symbols - formatter.minimumFractionDigits = decimals - formatter.maximumFractionDigits = decimals - return when (this) { - is Float, - is Double, - -> formatter.format(this.toDouble()) - - else -> formatter.format(this.toLong()) - } -} - -public fun Float.toIntUp(): Int { - val intValue = toInt() - return if ((this - intValue.toFloat()).absoluteValue <= 0.00001) { - intValue - } else { - intValue + 1 - } -} - -public infix fun Int.upBy(step: Int): Int { - val mod = this % step - return if (mod == 0) { - this - } else { - this - mod + step - } -} - -public fun Number.formatSimple(): String { - val raw = toString() - return if (raw.endsWith(".0") || raw.endsWith(",0")) { - raw.dropLast(2) - } else { - raw - } -} - -public inline fun Int.ifZero(defaultValue: () -> Int): Int { - contract { - callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) - } - return if (this == 0) { - defaultValue() - } else { - this - } -} - -public inline fun Long.ifZero(defaultValue: () -> Long): Long { - contract { - callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) - } - return if (this == 0L) { - defaultValue() - } else { - this - } -} - -public fun longOf(a: Int, b: Int): Long { - return a.toLong() shl 32 or (b.toLong() and 0xffffffffL) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt deleted file mode 100644 index 85d5be4f1..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt +++ /dev/null @@ -1,55 +0,0 @@ -@file:JvmName("OkHttpUtils") - -package org.koitharu.kotatsu.parsers.util - -import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.* -import okhttp3.internal.toLongOrDefault -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -public suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation -> - val callback = ContinuationCallCallback(this, continuation) - continuation.invokeOnCancellation(callback) - enqueue(callback) -} - -public val Response.mimeType: String? - get() = header("content-type")?.substringBefore(';')?.trim()?.nullIfEmpty()?.lowercase() - -public val HttpUrl.isHttpOrHttps: Boolean - get() = scheme.equals("https", ignoreCase = true) || scheme.equals("http", ignoreCase = true) - -public fun Headers.Builder.mergeWith(other: Headers, replaceExisting: Boolean): Headers.Builder { - for ((name, value) in other) { - if (replaceExisting || this[name] == null) { - this[name] = value - } - } - return this -} - -public fun Response.copy(): Response = newBuilder() - .body(peekBody(Long.MAX_VALUE)) - .build() - -public fun Response.Builder.setHeader(name: String, value: String?): Response.Builder = if (value == null) { - removeHeader(name) -} else { - header(name, value) -} - -public inline fun Response.map(mapper: (ResponseBody) -> ResponseBody): Response { - contract { - callsInPlace(mapper, InvocationKind.AT_MOST_ONCE) - } - return body.use { responseBody -> - newBuilder() - .body(mapper(responseBody)) - .build() - } -} - -public fun Response.headersContentLength( - defaultValue: Long = -1, -): Long = headers["Content-Length"]?.toLongOrDefault(defaultValue) ?: defaultValue diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt deleted file mode 100644 index 45e15ca6f..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import androidx.collection.SparseArrayCompat -import androidx.collection.set - -public class Paginator internal constructor(private val initialPageSize: Int) { - - public var firstPage: Int = 1 - private var pages = SparseArrayCompat() - - internal fun getPage(offset: Int): Int { - if (offset == 0) { // just an optimization - return firstPage - } - pages[offset]?.let { return it } - val pageSize = initialPageSize - val intPage = offset / pageSize - val tail = offset % pageSize - return intPage + firstPage + if (tail == 0) 0 else 1 - } - - internal fun onListReceived(offset: Int, page: Int, count: Int) { - pages[offset + count] = if (count > 0) page + 1 else page - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt deleted file mode 100644 index 9cc4517b9..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt +++ /dev/null @@ -1,101 +0,0 @@ -@file:JvmName("ParseUtils") - -package org.koitharu.kotatsu.parsers.util - -import okhttp3.Response -import okhttp3.ResponseBody -import org.json.JSONArray -import org.json.JSONObject -import org.jsoup.Jsoup -import org.jsoup.nodes.Document -import org.koitharu.kotatsu.parsers.InternalParsersApi -import java.text.DateFormat - -private val REGEX_SCHEME_PREFIX = Regex("^\\w{2,6}://", RegexOption.IGNORE_CASE) -internal const val SCHEME_HTTPS = "https" - -/** - * Parse [Response] body as html document using Jsoup - * @see [parseJson] - * @see [parseJsonArray] - */ -// TODO suspend -public fun Response.parseHtml(): Document = use { response -> - val body = response.body - Jsoup.parse(body.string(), response.request.url.toString()) -} - -/** - * Parse [Response] body as [JSONObject] - * @see [parseJsonArray] - * @see [parseHtml] - */ -public fun Response.parseJson(): JSONObject = use { response -> - JSONObject(response.body.string()) -} - -/** - * Parse [Response] body as [JSONArray] - * @see [parseJson] - * @see [parseHtml] - */ -public fun Response.parseJsonArray(): JSONArray = use { response -> - JSONArray(response.body.string()) -} - -public fun Response.parseRaw(): String = use { response -> - response.body.string() -} - -public fun Response.parseBytes(): ByteArray = use { response -> - response.body.bytes() -} - -/** - * Convert url to relative if it is on [domain] - * @return an url relative to the [domain] or absolute, if domain is mismatching - */ -public fun String.toRelativeUrl(domain: String): String { - if (isEmpty() || startsWith("/")) { - return this - } - return replace(Regex("^[^/]{2,6}://${Regex.escape(domain)}+/", RegexOption.IGNORE_CASE), "/") -} - -/** - * Convert url to absolute with specified domain - * @return an absolute url with [domain] if this is relative - */ -public fun String.toAbsoluteUrl(domain: String): String = when { - startsWith("//") -> "$SCHEME_HTTPS:$this" - startsWith('/') -> "$SCHEME_HTTPS://$domain$this" - REGEX_SCHEME_PREFIX.containsMatchIn(this) -> this - else -> "$SCHEME_HTTPS://$domain/$this" -} - -public fun concatUrl(host: String, path: String): String { - val hostWithSlash = host.endsWith('/') - val pathWithSlash = path.startsWith('/') - val hostWithScheme = if (host.startsWith("//")) "https:$host" else host - return when { - hostWithSlash && pathWithSlash -> hostWithScheme + path.drop(1) - !hostWithSlash && !pathWithSlash -> "$hostWithScheme/$path" - else -> hostWithScheme + path - } -} - -@InternalParsersApi -public fun DateFormat.parseSafe(str: String?): Long = if (str.isNullOrEmpty()) { - 0L -} else { - runCatching { - parse(str)?.time ?: 0L - }.onFailure { - if (javaClass.desiredAssertionStatus()) { - throw AssertionError("Cannot parse date $str", it) - } - }.getOrDefault(0L) -} - -@Deprecated("Useless since OkHttp 5.0", replaceWith = ReplaceWith("body")) -public fun Response.requireBody(): ResponseBody = body diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt deleted file mode 100644 index 1639eaa85..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt +++ /dev/null @@ -1,81 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.OkHttpClient -import org.koitharu.kotatsu.parsers.network.RateLimitHelper -import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -/** - * An OkHttp interceptor that enforces rate limiting across all requests. - * - * Examples: - * - `permits = 5`, `period = 1.seconds` => 5 requests per second - * - `permits = 10`, `period = 2.minutes` => 10 requests per 2 minutes - * - * @param permits [Int] Number of requests allowed within a period of units. - * @param period [Duration] The limiting duration. Defaults to 1.seconds. - * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information - */ -public fun OkHttpClient.Builder.rateLimit( - permits: Int, - period: Duration = 1.seconds -): OkHttpClient.Builder = rateLimit(permits, period) { true } - -/** - * An OkHttp interceptor that handles given url host's rate limiting. - * - * Examples: - * - `url = "https://api.manga.example"`, `permits = 5`, `period = 1.seconds` => 5 requests per second to any url with host "api.manga.example" - * - `url = "https://cdn.manga.example/image"`, `permits = 10`, `period = 2.minutes` => 10 requests per 2 minutes to any url with host "cdn.manga.example" - * - * @param url [String] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host() - * @param permits [Int] Number of requests allowed within a period of units. - * @param period [Duration] The limiting duration. Defaults to 1.seconds. - * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information - */ -public fun OkHttpClient.Builder.rateLimit( - url: String, - permits: Int, - period: Duration = 1.seconds -): OkHttpClient.Builder = rateLimit(url.toHttpUrl(), permits, period) - -/** - * An OkHttp interceptor that handles given url host's rate limiting. - * - * Examples: - * - `httpUrl = "https://api.manga.example".toHttpUrlOrNull()`, `permits = 5`, `period = 1.seconds` => 5 requests per second to any url with host "api.manga.example" - * - `httpUrl = "https://cdn.manga.example/image".toHttpUrlOrNull()`, `permits = 10`, `period = 2.minutes` => 10 requests per 2 minutes to any url with host "cdn.manga.example - * - * @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host() - * @param permits [Int] Number of requests allowed within a period of units. - * @param period [Duration] The limiting duration. Defaults to 1.seconds. - * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information - */ -public fun OkHttpClient.Builder.rateLimit( - httpUrl: HttpUrl, - permits: Int, - period: Duration = 1.seconds -): OkHttpClient.Builder = rateLimit(permits, period) { it.host == httpUrl.host } - -/** - * An OkHttp interceptor that enforces conditional rate limiting based on a given condition. - * - * Examples: - * - `permits = 5`, `period = 1.seconds`, `shouldLimit = { it.host == "api.manga.example" }` => 5 requests per second to any url with host "api.manga.example". - * - `permits = 10`, `period = 2.minutes`, `shouldLimit = { it.encodedPath.startsWith("/images/") }` => 10 requests per 2 minutes to paths starting with "/images/". - * - * @param permits [Int] Number of requests allowed within a period of units. - * @param period [Duration] The limiting duration. Defaults to 1.seconds. - * @param shouldLimit A predicate to determine whether the rate limit should apply to a given request. - * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information - */ -public fun OkHttpClient.Builder.rateLimit( - permits: Int, - period: Duration = 1.seconds, - shouldLimit: (HttpUrl) -> Boolean -): OkHttpClient.Builder { - return addInterceptor(RateLimitHelper(permits, period, shouldLimit)) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt deleted file mode 100644 index 73e89b42c..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import kotlinx.coroutines.* -import org.koitharu.kotatsu.parsers.MangaParser -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaListFilter -import org.koitharu.kotatsu.parsers.model.SortOrder - -public class RelatedMangaFinder( - private val parsers: Collection, -) { - - public suspend operator fun invoke(seed: Manga): List = withContext(Dispatchers.Default) { - coroutineScope { - parsers.singleOrNull()?.let { parser -> - findRelatedImpl(this, parser, seed) - } ?: parsers.map { parser -> - async { - findRelatedImpl(this, parser, seed) - } - }.awaitAll().flatten() - } - } - - private suspend fun findRelatedImpl(scope: CoroutineScope, parser: MangaParser, seed: Manga): List { - val words = HashSet() - words += seed.title.splitByWhitespace() - seed.altTitles.forEach { - words += it.splitByWhitespace() - } - if (words.isEmpty()) { - return emptyList() - } - val results = words.map { keyword -> - scope.async { - val result = parser.getList( - 0, - if (SortOrder.RELEVANCE in parser.availableSortOrders) { - SortOrder.RELEVANCE - } else { - parser.availableSortOrders.first() - }, - MangaListFilter( - query = keyword, - ), - ) - result.filter { it.id != seed.id && it.containKeyword(keyword) } - } - }.awaitAll() - return results.minBy { if (it.isEmpty()) Int.MAX_VALUE else it.size } - } - - private fun Manga.containKeyword(keyword: String): Boolean { - return title.contains(keyword, ignoreCase = true) - || altTitles.any { it.contains(keyword, ignoreCase = true) } - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt deleted file mode 100644 index 3b4bc91aa..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import kotlinx.coroutines.CancellationException -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -@Suppress("WRONG_INVOCATION_KIND") // https://youtrack.jetbrains.com/issue/KT-70714 -public inline fun T.runCatchingCancellable(block: T.() -> R): Result { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - return try { - Result.success(block()) - } catch (e: InterruptedException) { - throw e - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - Result.failure(e) - } -} - -public inline fun Result.recoverCatchingCancellable(transform: (exception: Throwable) -> R): Result { - contract { - callsInPlace(transform, InvocationKind.AT_MOST_ONCE) - } - return when (val exception = exceptionOrNull()) { - null -> this - else -> runCatchingCancellable { transform(exception) } - } -} - -public inline fun Result.recoverNotNull(transform: (exception: Throwable) -> R?): Result { - contract { - callsInPlace(transform, InvocationKind.AT_MOST_ONCE) - } - return when (val exception = exceptionOrNull()) { - null -> this - else -> transform(exception)?.let(Result.Companion::success) ?: this - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt deleted file mode 100644 index 1f02cdee0..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt +++ /dev/null @@ -1,250 +0,0 @@ -@file:Suppress("DEPRECATION") - -package org.koitharu.kotatsu.parsers.util - -import org.koitharu.kotatsu.parsers.model.MangaListFilter -import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities -import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* -import org.koitharu.kotatsu.parsers.model.search.SearchCapability -import org.koitharu.kotatsu.parsers.model.search.SearchableField.* - -/** - * Converts a [MangaListFilter] into a [MangaSearchQuery]. - * - * This function iterates through the filter attributes in [MangaListFilter] and creates corresponding - * search criteria in a [MangaSearchQuery.Builder]. - * - * @param filter The [MangaListFilter] to convert. - * @return A [MangaSearchQuery] constructed based on the given [filter]. - */ -internal fun convertToMangaSearchQuery(offset: Int, sortOrder: SortOrder, filter: MangaListFilter): MangaSearchQuery { - return MangaSearchQuery.Builder().apply { - offset(offset) - order(sortOrder) - if (filter.tags.isNotEmpty()) criterion(Include(TAG, filter.tags)) - if (filter.tagsExclude.isNotEmpty()) criterion(Exclude(TAG, filter.tagsExclude)) - if (filter.states.isNotEmpty()) criterion(Include(STATE, filter.states)) - if (filter.types.isNotEmpty()) criterion(Include(CONTENT_TYPE, filter.types)) - if (filter.contentRating.isNotEmpty()) criterion(Include(CONTENT_RATING, filter.contentRating)) - if (filter.demographics.isNotEmpty()) criterion(Include(DEMOGRAPHIC, filter.demographics)) - if (validateYear(filter.yearFrom) || validateYear(filter.yearTo)) { - criterion(QueryCriteria.Range(PUBLICATION_YEAR, filter.yearFrom, filter.yearTo)) - } - if (validateYear(filter.year)) { - criterion(Match(PUBLICATION_YEAR, filter.year)) - } - filter.locale?.let { - criterion(Include(LANGUAGE, setOf(it))) - } - filter.originalLocale?.let { - criterion(Include(ORIGINAL_LANGUAGE, setOf(it))) - } - filter.query?.takeIf { it.isNotBlank() }?.let { - criterion(Match(TITLE_NAME, it)) - } - }.build() -} - -/** - * Converts a {@link MangaSearchQuery} into a {@link MangaListFilter}. - *

- * This method iterates through the search criteria defined in the provided {@code searchQuery} - * and applies them to a {@link MangaListFilter.Builder}. The criteria are processed based on - * their types, such as inclusion, exclusion, equality checks, range filtering, and pattern matching. - *

- *

- * Supported criteria: - *

    - *
  • {@link QueryCriteria.Include} - Adds tags, states, content types, content ratings, demographics, and languages.
  • - *
  • {@link QueryCriteria.Exclude} - Excludes tags.
  • - *
  • {@link QueryCriteria.Equals} - Sets specific values like publication year.
  • - *
  • {@link QueryCriteria.Between} - Sets a range of values like publication year range.
  • - *
  • {@link QueryCriteria.Match} - Adds a search pattern for the title name.
  • - *
- *

- *

- * If an unsupported field is encountered, an {@link UnsupportedOperationException} is thrown. - *

- * - * @param searchQuery The {@link MangaSearchQuery} to convert. - * @return A {@link MangaListFilter} constructed based on the given {@code searchQuery}. - * @throws UnsupportedOperationException If the search criteria contain unsupported fields. - */ -internal fun convertToMangaListFilter(searchQuery: MangaSearchQuery): MangaListFilter { - return MangaListFilter.Builder().apply { - for (criterion in searchQuery.criteria) { - when (criterion) { - is Include<*> -> handleInclude(this, criterion) - is Exclude<*> -> handleExclude(this, criterion) - is Range<*> -> handleBetween(this, criterion) - is Match<*> -> handleMatch(this, criterion) - } - } - }.build() -} - -internal fun MangaSearchQueryCapabilities.toMangaListFilterCapabilities() = MangaListFilterCapabilities( - isMultipleTagsSupported = capabilities.any { x -> x.field == TAG && x.isMultiple }, - isTagsExclusionSupported = capabilities.any { x -> x.field == TAG && x.criteriaTypes.contains(Exclude::class) }, - isSearchSupported = capabilities.any { x -> x.field == TITLE_NAME }, - isSearchWithFiltersSupported = capabilities.any { x -> x.field == TITLE_NAME && !x.isExclusive }, - isYearSupported = capabilities.any { x -> x.field == PUBLICATION_YEAR && x.criteriaTypes.contains(Match::class) }, - isYearRangeSupported = capabilities.any { x -> x.field == PUBLICATION_YEAR && x.criteriaTypes.contains(Range::class) }, - isOriginalLocaleSupported = capabilities.any { x -> x.field == ORIGINAL_LANGUAGE }, - isAuthorSearchSupported = capabilities.any { x -> x.field == AUTHOR }, -) - -internal fun MangaListFilterCapabilities.toMangaSearchQueryCapabilities(): MangaSearchQueryCapabilities = - MangaSearchQueryCapabilities( - capabilities = setOfNotNull( - isMultipleTagsSupported.takeIf { it }?.let { - SearchCapability( - field = TAG, - criteriaTypes = setOf(Include::class), - isMultiple = true, - ) - }, - isTagsExclusionSupported.takeIf { it }?.let { - SearchCapability( - field = TAG, - criteriaTypes = setOf(Exclude::class), - isMultiple = true, - ) - }, - isSearchSupported.takeIf { it }?.let { - SearchCapability( - field = TITLE_NAME, - criteriaTypes = setOf(Match::class), - isMultiple = false, - isExclusive = true, - ) - }, - isSearchWithFiltersSupported.takeIf { it }?.let { - SearchCapability( - field = TITLE_NAME, - criteriaTypes = setOf(Match::class), - isMultiple = false, - ) - }, - isYearSupported.takeIf { it }?.let { - SearchCapability( - field = PUBLICATION_YEAR, - criteriaTypes = setOf(Match::class), - isMultiple = false, - ) - }, - isYearRangeSupported.takeIf { it }?.let { - SearchCapability( - field = PUBLICATION_YEAR, - criteriaTypes = setOf(Range::class), - isMultiple = false, - ) - }, - isOriginalLocaleSupported.takeIf { it }?.let { - SearchCapability( - field = ORIGINAL_LANGUAGE, - criteriaTypes = setOf(Include::class), - isMultiple = true, - ) - }, - SearchCapability( - field = LANGUAGE, - criteriaTypes = setOf(Include::class), - isMultiple = true, - ), - SearchCapability( - field = STATE, criteriaTypes = setOf(Include::class), isMultiple = true, - ), - SearchCapability( - field = CONTENT_TYPE, - criteriaTypes = setOf(Include::class), - isMultiple = true, - ), - SearchCapability( - field = CONTENT_RATING, - criteriaTypes = setOf(Include::class), - isMultiple = true, - ), - SearchCapability( - field = DEMOGRAPHIC, - criteriaTypes = setOf(Include::class), - isMultiple = true, - ), - ), - ) - -private fun handleInclude(builder: MangaListFilter.Builder, criterion: Include<*>) { - val type = criterion.field.type - - when (criterion.field) { - TAG -> builder.addTags(filterValues(criterion, type)) - STATE -> builder.addStates(filterValues(criterion, type)) - CONTENT_TYPE -> builder.addTypes(filterValues(criterion, type)) - CONTENT_RATING -> builder.addContentRatings(filterValues(criterion, type)) - DEMOGRAPHIC -> builder.addDemographics(filterValues(criterion, type)) - LANGUAGE -> builder.locale(getFirstValue(criterion, type)) - ORIGINAL_LANGUAGE -> builder.originalLocale(getFirstValue(criterion, type)) - else -> throw IllegalArgumentException("Unsupported field for Include criterion: ${criterion.field}") - } -} - -private fun handleExclude(builder: MangaListFilter.Builder, criterion: Exclude<*>) { - val type = criterion.field.type - - when (criterion.field) { - TAG -> builder.excludeTags(filterValues(criterion, type)) - else -> throw IllegalArgumentException("Unsupported field for Exclude criterion: ${criterion.field}") - } -} - -private fun handleBetween(builder: MangaListFilter.Builder, criterion: Range<*>) { - val type = criterion.field.type - - when (criterion.field) { - PUBLICATION_YEAR -> { - builder.yearFrom(getValue(criterion.from, type, YEAR_UNKNOWN)) - builder.yearTo(getValue(criterion.to, type, YEAR_UNKNOWN)) - } - - else -> throw IllegalArgumentException("Unsupported field for Between criterion: ${criterion.field}") - } -} - -private fun handleMatch(builder: MangaListFilter.Builder, criterion: Match<*>) { - val type = criterion.field.type - - when (criterion.field) { - TITLE_NAME -> builder.query(getValue(criterion.value, type, "")) - PUBLICATION_YEAR -> builder.year(getValue(criterion.value, type, YEAR_UNKNOWN)) - else -> throw IllegalArgumentException("Unsupported field for Match criterion: ${criterion.field}") - } -} - -@Suppress("UNCHECKED_CAST") -private fun filterValues(criterion: Include<*>, type: Class<*>): List { - return criterion.values.filter { type.isInstance(it) } as List -} - -@Suppress("UNCHECKED_CAST") -private fun filterValues(criterion: Exclude<*>, type: Class<*>): List { - return criterion.values.filter { type.isInstance(it) } as List -} - -@Suppress("UNCHECKED_CAST") -private fun getFirstValue(criterion: Include<*>, type: Class<*>): T? { - return criterion.values.firstOrNull { type.isInstance(it) } as? T -} - -@Suppress("UNCHECKED_CAST") -private fun getValue(value: Any?, type: Class<*>, default: T): T { - val isCompatibleIntType = (type == Int::class.java && Integer::class.isInstance(value)) - - return if (type.isInstance(value) || isCompatibleIntType) value as T else default -} - -private fun validateYear(year: Int) = year != YEAR_UNKNOWN diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt deleted file mode 100644 index e6875282a..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt +++ /dev/null @@ -1,237 +0,0 @@ -@file:JvmName("StringUtils") - -package org.koitharu.kotatsu.parsers.util - -import androidx.collection.MutableIntList -import java.math.BigInteger -import java.net.URLDecoder -import java.net.URLEncoder -import java.security.MessageDigest -import java.util.* -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.math.min - -private val REGEX_WHITESPACE = Regex("\\s+") -internal const val LONG_HASH_SEED = 1125899906842597L - -public fun String.removeSurrounding(vararg chars: Char): String { - if (isEmpty()) { - return this - } - for (c in chars) { - if (first() == c && last() == c) { - return substring(1, length - 1) - } - } - return this -} - -public inline fun C?.ifNullOrEmpty(defaultValue: () -> R): R { - contract { - callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) - } - return if (this.isNullOrEmpty()) defaultValue() else this -} - -public fun String.longHashCode(): Long { - var h = LONG_HASH_SEED - val len: Int = this.length - for (i in 0 until len) { - h = 31 * h + this[i].code - } - return h -} - -public fun String.toCamelCase(): String { - if (isEmpty()) { - return this - } - val result = StringBuilder(length) - var capitalize = true - for (char in this) { - result.append( - if (capitalize) { - char.uppercase() - } else { - char.lowercase() - }, - ) - capitalize = char.isWhitespace() - } - return result.toString() -} - -public fun String.digits(): String = filter { it.isDigit() } - -public fun String.toTitleCase(): String { - return replaceFirstChar { x -> x.uppercase() } -} - -public fun String.toTitleCase(locale: Locale): String { - return replaceFirstChar { x -> x.uppercase(locale) } -} - -public fun String.ellipsize(maxLength: Int): String = if (this.length > maxLength) { - this.take(maxLength - 1) + Typography.ellipsis -} else this - -public fun String.splitTwoParts(delimiter: Char): Pair? { - val indices = MutableIntList(4) - for ((i, c) in this.withIndex()) { - if (c == delimiter) { - indices += i - } - } - if (indices.isEmpty() || (indices.size and 1) == 0) { - return null - } - val index = indices[indices.size / 2] - return substring(0, index) to substring(index + 1) -} - -public fun String.urlEncoded(): String = URLEncoder.encode(this, Charsets.UTF_8.name()) - -public fun String.urlDecode(): String = URLDecoder.decode(this, Charsets.UTF_8.name()) - -public fun String.nl2br(): String = replace("\n", "
") - -public fun String.splitByWhitespace(): List = trim().split(REGEX_WHITESPACE) - -public fun T.nullIfEmpty(): T? = takeUnless { it.isEmpty() } - -public fun ByteArray.byte2HexFormatted(): String { - val str = StringBuilder(size * 2) - for (i in indices) { - var h = Integer.toHexString(this[i].toInt()) - val l = h.length - if (l == 1) { - h = "0$h" - } - if (l > 2) { - h = h.substring(l - 2, l) - } - str.append(h.uppercase(Locale.ROOT)) - if (i < size - 1) { - str.append(':') - } - } - return str.toString() -} - -public fun String.md5(): String { - val md = MessageDigest.getInstance("MD5") - return BigInteger(1, md.digest(toByteArray())) - .toString(16) - .padStart(32, '0') -} - -public fun String.substringBetween(from: String, to: String, fallbackValue: String = this): String { - val fromIndex = indexOf(from) - if (fromIndex == -1) { - return fallbackValue - } - val toIndex = lastIndexOf(to) - return if (toIndex == -1) { - fallbackValue - } else { - substring(fromIndex + from.length, toIndex) - } -} - -public fun String.substringBetweenFirst(from: String, to: String): String? { - val fromIndex = indexOf(from) - if (fromIndex == -1) { - return null - } - val toIndex = indexOf(to, fromIndex) - return if (toIndex == -1) { - null - } else { - substring(fromIndex + from.length, toIndex) - } -} - -public fun String.substringBetweenLast(from: String, to: String, fallbackValue: String = this): String { - val fromIndex = lastIndexOf(from) - if (fromIndex == -1) { - return fallbackValue - } - val toIndex = lastIndexOf(to) - return if (toIndex == -1) { - fallbackValue - } else { - substring(fromIndex + from.length, toIndex) - } -} - -public fun String.find(regex: Regex): String? = regex.find(this)?.value - -public fun String.findGroupValue(regex: Regex): String? = regex.find(this)?.groupValues?.getOrNull(1) - -public fun String.removeSuffix(suffix: Char): String { - if (lastOrNull() == suffix) { - return substring(0, length - 1) - } - return this -} - -public fun String.levenshteinDistance(other: String): Int { - if (this == other) { - return 0 - } - if (this.isEmpty()) { - return other.length - } - if (other.isEmpty()) { - return this.length - } - - val lhsLength = this.length + 1 - val rhsLength = other.length + 1 - - var cost = Array(lhsLength) { it } - var newCost = Array(lhsLength) { 0 } - - for (i in 1 until rhsLength) { - newCost[0] = i - - for (j in 1 until lhsLength) { - val match = if (this[j - 1] == other[i - 1]) 0 else 1 - - val costReplace = cost[j - 1] + match - val costInsert = cost[j] + 1 - val costDelete = newCost[j - 1] + 1 - - newCost[j] = min(min(costInsert, costDelete), costReplace) - } - - val swap = cost - cost = newCost - newCost = swap - } - - return cost[lhsLength - 1] -} - -/** - * @param threshold 0 = exact match - */ -public fun String.almostEquals(other: String, threshold: Float): Boolean { - if (threshold <= 0f) { - return equals(other, ignoreCase = true) - } - val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f) - return diff < threshold -} - -public fun String.isNumeric(): Boolean = all { c -> c.isDigit() } - -internal fun StringBuilder.removeTrailingZero() { - if (length > 2 && get(length - 1) == '0') { - val dot = get(length - 2) - if (dot == ',' || dot == '.') { - delete(length - 2, length) - } - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt index bc10c58f7..12f7cad71 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt @@ -7,7 +7,7 @@ public class WebViewHelper( ) { public suspend fun getLocalStorageValue(domain: String, key: String): String? { - return context.evaluateJs("$SCHEME_HTTPS://$domain/", "window.localStorage.getItem(\"$key\")") + return context.evaluateJs("https://$domain/", "window.localStorage.getItem(\"$key\")") } public suspend fun getUrlValue(url: String, value: String): String? { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt deleted file mode 100644 index 0c5ac5477..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.json - -public fun String.unescapeJson(): String { - val builder = StringBuilder() - var i = 0 - while (i < length) { - val delimiter = this[i] - i++ // consume letter or backslash - if (delimiter == '\\' && i < length) { - val ch = this[i] - i++ - - when (ch) { - '\\', '/', '"', '\'' -> builder.append(ch) - 'n' -> builder.append('\n') - 'r' -> builder.append('\r') - 't' -> builder.append('\t') - 'b' -> builder.append('\b') - 'u' -> { - val hex = StringBuilder(4) - require(i + 4 <= length) { "Not enough unicode digits!" } - for (x in substring(i, i + 4)) { - require(x.isLetterOrDigit()) { "Bad character in unicode escape" } - hex.append(x.lowercase()) - } - i += 4 // consume those four digits. - val code = hex.toString().toInt(16) - builder.append(code.toChar()) - } - - else -> throw IllegalArgumentException("Illegal escape sequence: \\$ch") - } - } else { - builder.append(delimiter) - } - } - return builder.toString() -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt deleted file mode 100644 index b5ebf25ac..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.json - -import org.json.JSONArray - -internal class JSONArrayTypedIterator( - private val array: JSONArray, - private val typeClass: Class, - startIndex: Int, -) : ListIterator { - - private val total = array.length() - private var index = startIndex - - override fun hasNext() = index < total - - override fun next(): T { - if (!hasNext()) throw NoSuchElementException() - return get(index++) - } - - override fun hasPrevious(): Boolean = index > 0 - - override fun nextIndex(): Int = index - - override fun previous(): T { - if (!hasPrevious()) throw NoSuchElementException() - return get(--index) - } - - override fun previousIndex(): Int = index - 1 - - private fun get(i: Int): T = typeClass.cast(array[i]) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt deleted file mode 100644 index 2caa8d75e..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.json - -import org.json.JSONArray - -internal class JSONArrayTypedListWrapper( - private val jsonArray: JSONArray, - private val typeClass: Class, -) : List { - - override fun contains(element: T): Boolean = indexOf(element) != -1 - - override fun containsAll(elements: Collection): Boolean = elements.all { contains(it) } - - override fun get(index: Int): T = typeClass.cast(jsonArray[index]) - - override fun indexOf(element: T): Int { - repeat(jsonArray.length()) { i -> - if (jsonArray[i] == element) { - return i - } - } - return -1 - } - - override fun isEmpty(): Boolean = jsonArray.length() == 0 - - override fun iterator(): Iterator = listIterator(0) - - override fun lastIndexOf(element: T): Int { - val total = jsonArray.length() - repeat(total) { i -> - if (jsonArray[total - i - 1] == element) { - return i - } - } - return -1 - } - - override fun listIterator(): ListIterator = listIterator(0) - - override fun listIterator(index: Int): ListIterator = JSONArrayTypedIterator(jsonArray, typeClass, index) - - override fun subList(fromIndex: Int, toIndex: Int): List { - val result = ArrayList(toIndex - fromIndex + 1) - for (i in fromIndex..toIndex) { - result.add(get(i)) - } - return result - } - - override val size: Int - get() = jsonArray.length() -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt deleted file mode 100644 index 32c94c971..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.json - -import org.json.JSONObject - -internal class JSONObjectTypedIterableWrapper( - private val json: JSONObject, - private val typeClass: Class, -) : Iterable> { - - override fun iterator(): Iterator> = IteratorImpl() - - private inner class IteratorImpl : Iterator> { - - private val keyIterator = json.keys().iterator() - - override fun hasNext(): Boolean = keyIterator.hasNext() - - override fun next(): Map.Entry = keyIterator.next().let { key -> - JSONEntry(key, typeClass.cast(json.get(key))) - } - } - - private class JSONEntry(override val key: String, override val value: T) : Map.Entry -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt deleted file mode 100644 index b95a7ddcf..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt +++ /dev/null @@ -1,167 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.json - -import androidx.collection.ArraySet -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import org.koitharu.kotatsu.parsers.util.nullIfEmpty -import java.util.* -import kotlin.contracts.contract - -public fun String.toJSONObjectOrNull(): JSONObject? = try { - JSONObject(this) -} catch (_: JSONException) { - null -} - -public fun String.toJSONArrayOrNull(): JSONArray? = try { - JSONArray(this) -} catch (_: JSONException) { - null -} - -public inline fun > JSONArray.mapJSONTo( - destination: C, - block: (JSONObject) -> R, -): C { - val len = length() - for (i in 0 until len) { - val jo = getJSONObject(i) - destination.add(block(jo)) - } - return destination -} - -public inline fun > JSONArray.mapJSONNotNullTo( - destination: C, - block: (JSONObject) -> R?, -): C { - val len = length() - for (i in 0 until len) { - val jo = getJSONObject(i) - destination.add(block(jo) ?: continue) - } - return destination -} - -public inline fun JSONArray.mapJSON(block: (JSONObject) -> T): List { - return mapJSONTo(ArrayList(length()), block) -} - -public inline fun JSONArray.mapJSONNotNull(block: (JSONObject) -> T?): List { - return mapJSONNotNullTo(ArrayList(length()), block) -} - -public inline fun JSONArray.mapJSONToSet(mapper: (JSONObject) -> T): Set { - return mapJSONTo(ArraySet(length()), mapper) -} - -public inline fun JSONArray.mapJSONNotNullToSet(mapper: (JSONObject) -> T?): Set { - return mapJSONNotNullTo(ArraySet(length()), mapper) -} - -public fun JSONArray.mapJSONIndexed(block: (Int, JSONObject) -> T): List { - val len = length() - val result = ArrayList(len) - for (i in 0 until len) { - val jo = getJSONObject(i) - result.add(block(i, jo)) - } - return result -} - -public fun JSONObject.getStringOrNull(name: String): String? = opt(name)?.takeUnless { - it === JSONObject.NULL -}?.toString()?.nullIfEmpty() - -public fun JSONObject.getBooleanOrDefault(name: String, defaultValue: Boolean): Boolean { - return when (val rawValue = opt(name)) { - null, JSONObject.NULL -> defaultValue - is Boolean -> rawValue - is Number -> rawValue.toInt() != 0 - is String -> rawValue.lowercase(Locale.ROOT).toBooleanStrictOrNull() ?: defaultValue - else -> defaultValue - } -} - -public fun JSONObject.getLongOrDefault(name: String, defaultValue: Long): Long { - return when (val rawValue = opt(name)) { - null, JSONObject.NULL -> defaultValue - is Long -> rawValue - is Number -> rawValue.toLong() - is String -> rawValue.toLongOrNull() ?: defaultValue - else -> defaultValue - } -} - -public fun JSONObject.getIntOrDefault(name: String, defaultValue: Int): Int { - return when (val rawValue = opt(name)) { - null, JSONObject.NULL -> defaultValue - is Int -> rawValue - is Number -> rawValue.toInt() - is String -> rawValue.toIntOrNull() ?: defaultValue - else -> defaultValue - } -} - -public fun JSONObject.getDoubleOrDefault(name: String, defaultValue: Double): Double { - return when (val rawValue = opt(name)) { - null, JSONObject.NULL -> defaultValue - is Double -> rawValue - is Number -> rawValue.toDouble() - is String -> rawValue.toDoubleOrNull() ?: defaultValue - else -> defaultValue - } -} - -public fun JSONObject.getFloatOrDefault(name: String, defaultValue: Float): Float { - return when (val rawValue = opt(name)) { - null, JSONObject.NULL -> defaultValue - is Float -> rawValue - is Number -> rawValue.toFloat() - is String -> rawValue.toFloatOrNull() ?: defaultValue - else -> defaultValue - } -} - -public fun > JSONObject.getEnumValueOrNull(name: String, enumClass: Class): E? { - val enumName = getStringOrNull(name) ?: return null - return enumClass.enumConstants?.find { x -> - enumName.equals(x.name, ignoreCase = true) - } -} - -public fun > JSONObject.getEnumValueOrDefault(name: String, defaultValue: E): E { - return getEnumValueOrNull(name, defaultValue.javaClass) ?: defaultValue -} - -public fun JSONArray?.isNullOrEmpty(): Boolean { - contract { - returns(false) implies (this@isNullOrEmpty != null) - } - - return this == null || this.length() == 0 -} - -public fun JSONArray.asTypedList(typeClass: Class): List { - return JSONArrayTypedListWrapper(this, typeClass) -} - -public inline fun JSONArray.asTypedList(): List = asTypedList(T::class.java) - -public fun JSONObject.entries(typeClass: Class): Iterable> { - return JSONObjectTypedIterableWrapper(this, typeClass) -} - -public inline fun JSONObject.entries(): Iterable> = entries(T::class.java) - -public fun JSONArray.toStringSet(): Set { - val set = ArraySet(length()) - repeat(length()) { i -> - val str = optString(i) - if (!str.isNullOrEmpty()) { - set.add(str) - } - } - return set -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt deleted file mode 100644 index 9d4743edc..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Utility for extracting Next.js data from HTML or RSC responses. - * Based on the Next.js extraction logic from the Keiyoushi repository. - */ -@file:JvmName("NextJsUtils") - -package org.koitharu.kotatsu.parsers.util.json - -import okhttp3.Response -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import org.jsoup.nodes.Document -import org.koitharu.kotatsu.parsers.util.parseHtml - -private val NEXT_F_REGEX = Regex("""self\.__next_f\.push\(\s*(\[.*])\s*\)\s*;?\s*$""", RegexOption.DOT_MATCHES_ALL) - -private fun extractValueNextJs( - payload: Any, - predicate: (Any) -> Boolean, -): Any? { - if (payload !is JSONObject && payload !is JSONArray) return null - if (predicate(payload)) { - return payload - } - - if (payload is JSONObject) { - val keys = payload.keys() - while (keys.hasNext()) { - val key = keys.next() - val child = payload.opt(key) - if (child != null) { - val result = extractValueNextJs(child, predicate) - if (result != null) return result - } - } - } else if (payload is JSONArray) { - for (i in 0 until payload.length()) { - val child = payload.opt(i) - if (child != null) { - val result = extractValueNextJs(child, predicate) - if (result != null) return result - } - } - } - return null -} - -private fun Document.extractAppRouterPayloads(): List = select("script:not([src])") - .map { it.data() } - .filter { "self.__next_f.push" in it } - .flatMap { script -> - try { - val raw = NEXT_F_REGEX.find(script)?.groupValues?.get(1) ?: return@flatMap emptyList() - val arr = JSONArray(raw) - val content = arr.optString(1, null) ?: return@flatMap emptyList() - - extractRscPayloads(content) - } catch (_: Exception) { - emptyList() - } - } - -private fun Document.extractPagesRouterPayloads(): List { - val data = selectFirst("script#__NEXT_DATA__")?.data() ?: return emptyList() - return try { - val root = JSONObject(data) - val pageProps = root.optJSONObject("props")?.optJSONObject("pageProps") - listOfNotNull(pageProps, root) - } catch (_: Exception) { - emptyList() - } -} - -private fun extractRscPayloads(body: String): List { - val results = mutableListOf() - var pos = 0 - - while (pos < body.length) { - val colonIdx = body.indexOf(':', pos) - if (colonIdx == -1) break - - val id = body.substring(pos, colonIdx) - if (id.isEmpty() || !id.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' }) { - pos++ - continue - } - - pos = colonIdx + 1 - if (pos >= body.length) break - - if (body[pos] == 'T') { - pos++ - val commaIdx = body.indexOf(',', pos) - if (commaIdx == -1) break - val byteLen = body.substring(pos, commaIdx).toIntOrNull(16) ?: break - pos = commaIdx + 1 - var bytes = 0 - val start = pos - while (pos < body.length && bytes < byteLen) { - when { - body[pos].code < 0x80 -> bytes += 1 - body[pos].code < 0x800 -> bytes += 2 - Character.isHighSurrogate(body[pos]) -> { - bytes += 4 - pos++ - } - else -> bytes += 3 - } - pos++ - } - try { - results.add(parseJsonType(body.substring(start, pos))) - } catch (_: Exception) {} - } else { - val (element, end) = parseJsonAt(body, pos) - if (element != null) results.add(element) - pos = end - } - } - - return results -} - -private fun parseJsonType(jsonStr: String): Any { - val trimmed = jsonStr.trim() - return when { - trimmed.startsWith("{") -> JSONObject(trimmed) - trimmed.startsWith("[") -> JSONArray(trimmed) - else -> throw JSONException("Not a JSONObject or JSONArray") - } -} - -private fun parseJsonAt(body: String, start: Int): Pair { - if (start >= body.length) return Pair(null, start) - - var depth = 0 - var inString = false - var escape = false - var i = start - - while (i < body.length) { - val c = body[i++] - if (escape) { - escape = false - continue - } - if (c == '\\' && inString) { - escape = true - continue - } - if (c == '"') { - inString = !inString - continue - } - if (inString) continue - when (c) { - '{', '[' -> depth++ - '}', ']' -> if (--depth == 0) { - return try { - Pair(parseJsonType(body.substring(start, i)), i) - } catch (_: Exception) { - Pair(null, i) - } - } - } - if (depth == 0 && c.isWhitespace()) { - return try { - Pair(parseJsonType(body.substring(start, i - 1)), i) - } catch (_: Exception) { - Pair(null, i) - } - } - } - return Pair(null, i) -} - -public fun Document.extractNextJs( - predicate: (Any) -> Boolean, -): Any? { - val payloads = extractAppRouterPayloads().ifEmpty { extractPagesRouterPayloads() } - for (payload in payloads) { - val result = extractValueNextJs(payload, predicate) - if (result != null) return result - } - return null -} - -public inline fun Document.extractNextJsTyped( - noinline predicate: (Any) -> Boolean, -): T? = extractNextJs(predicate) as? T - -public fun String.extractNextJsRsc( - predicate: (Any) -> Boolean, -): Any? { - for (payload in extractRscPayloads(this)) { - val result = extractValueNextJs(payload, predicate) - if (result != null) return result - } - return null -} - -public inline fun String.extractNextJsRscTyped( - noinline predicate: (Any) -> Boolean, -): T? = extractNextJsRsc(predicate) as? T - -public fun Response.extractNextJs( - predicate: (Any) -> Boolean, -): Any? { - val contentType = header("Content-Type") ?: "" - return when { - "text/x-component" in contentType -> body.string().extractNextJsRsc(predicate) - "text/html" in contentType -> parseHtml().extractNextJs(predicate) - else -> error("Unsupported Content-Type for Next.js extraction: $contentType") - } -} - -public inline fun Response.extractNextJsTyped( - noinline predicate: (Any) -> Boolean, -): T? = extractNextJs(predicate) as? T diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt deleted file mode 100644 index 8c106e3e0..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.suspendlazy - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import java.lang.ref.SoftReference -import kotlin.coroutines.CoroutineContext - -/** - * Like a [SuspendLazy] but with [SoftReference] under the hood - */ -internal class SoftSuspendLazyImpl( - private val coroutineContext: CoroutineContext, - private val initializer: SuspendLazyInitializer, -) : SuspendLazy { - - private val mutex: Mutex = Mutex() - private var cachedValue: SoftReference? = null - - override val isInitialized: Boolean - get() = cachedValue?.get() != null - - override suspend fun get(): T { - // fast way - cachedValue?.get()?.let { - return it - } - return mutex.withLock { - cachedValue?.get()?.let { - return it - } - val result = withContext(coroutineContext) { - initializer() - } - cachedValue = SoftReference(result) - result - } - } - - override fun peek(): T? = cachedValue?.get() -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt deleted file mode 100644 index a923f71f8..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.suspendlazy - -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -internal typealias SuspendLazyInitializer = suspend () -> T - -public interface SuspendLazy { - - public val isInitialized: Boolean - - public suspend fun get(): T - - public fun peek(): T? -} - -public suspend fun SuspendLazy.getOrNull(): T? = runCatchingCancellable { - get() -}.getOrNull() - -public suspend fun SuspendLazy.getOrDefault(defaultValue: R): R = runCatchingCancellable { - get() -}.getOrDefault(defaultValue) - -public fun suspendLazy( - context: CoroutineContext = EmptyCoroutineContext, - initializer: SuspendLazyInitializer, -): SuspendLazy = SuspendLazyImpl(context, initializer) - -public fun suspendLazy( - context: CoroutineContext = EmptyCoroutineContext, - soft: Boolean, - initializer: SuspendLazyInitializer, -): SuspendLazy = if (soft) { - SoftSuspendLazyImpl(context, initializer) -} else { - SuspendLazyImpl(context, initializer) -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt deleted file mode 100644 index bb5bdb6ec..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.koitharu.kotatsu.parsers.util.suspendlazy - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import kotlin.coroutines.CoroutineContext - -internal class SuspendLazyImpl( - private val coroutineContext: CoroutineContext, - private val initializer: SuspendLazyInitializer, -) : SuspendLazy { - - private val mutex: Mutex = Mutex() - private var cachedValue: Any? = Uninitialized - - override val isInitialized: Boolean - get() = cachedValue !== Uninitialized - - @Suppress("UNCHECKED_CAST") - override suspend fun get(): T { - // fast way - cachedValue.let { - if (it !== Uninitialized) { - return it as T - } - } - return mutex.withLock { - cachedValue.let { - if (it !== Uninitialized) { - return it as T - } - } - val result = withContext(coroutineContext) { - initializer() - } - cachedValue = result - result - } - } - - @Suppress("UNCHECKED_CAST") - override fun peek(): T? { - return cachedValue?.takeUnless { it === Uninitialized } as T? - } - - private object Uninitialized -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt deleted file mode 100644 index 8b3617636..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.extension.BeforeAllCallback -import org.junit.jupiter.api.extension.ExtensionContext -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable - -class AuthCheckExtension : BeforeAllCallback { - - private val loaderContext: MangaLoaderContext = MangaLoaderContextMock - - override fun beforeAll(context: ExtensionContext) { - for (source in MangaParserSource.entries) { - val parser = loaderContext.newParserInstance(source) - if (parser is MangaParserAuthProvider) { - checkAuthorization(source, parser) - } - } - } - - private fun checkAuthorization(source: MangaParserSource, parser: MangaParserAuthProvider) = runTest { - runCatchingCancellable { - parser.getUsername() - }.onSuccess { username -> - println("Signed in to ${source.name} as $username") - }.onFailure { error -> - System.err.println("Auth failed for ${source.name}: ${error.javaClass.name}(${error.message})") - } - } -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt deleted file mode 100644 index 69efa9a40..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import okhttp3.Interceptor -import okhttp3.Response -import okhttp3.internal.closeQuietly -import org.koitharu.kotatsu.parsers.network.CloudFlareHelper - -internal class CloudFlareInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val response = chain.proceed(request) - if (CloudFlareHelper.checkResponseForProtection(response) != CloudFlareHelper.PROTECTION_NOT_DETECTED) { - response.closeQuietly() - throw CloudFlareProtectedException( - url = response.request.url.toString(), - headers = request.headers, - ) - } - return response - } -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt deleted file mode 100644 index bbfdae8ae..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import okhttp3.Headers -import okio.IOException - -class CloudFlareProtectedException( - val url: String, - val headers: Headers, -) : IOException("Protected by CloudFlare: $url") diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt deleted file mode 100644 index 48b47c111..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.MangaSource - -private const val HEADER_REFERER = CommonHeaders.REFERER - -internal class CommonHeadersInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val source = request.tag(MangaSource::class.java) - val parser = if (source is MangaParserSource) { - MangaLoaderContextMock.newParserInstance(source) - } else { - null - } - val headersBuilder = request.headers.newBuilder() - if (headersBuilder[HEADER_REFERER] == null && parser != null) { - headersBuilder[HEADER_REFERER] = "https://${parser.domain}/" - } - val newRequest = request.newBuilder().headers(headersBuilder.build()).build() - return if (parser is Interceptor) { - parser.intercept(ProxyChain(chain, newRequest)) - } else { - chain.proceed(newRequest) - } - } - - private class ProxyChain( - private val delegate: Interceptor.Chain, - private val request: Request, - ) : Interceptor.Chain by delegate { - - override fun request(): Request = request - } -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt deleted file mode 100644 index 1fad9c6c2..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import okhttp3.Cookie -import okhttp3.CookieJar -import okhttp3.HttpUrl -import org.koitharu.kotatsu.parsers.util.insertCookie -import org.koitharu.kotatsu.parsers.util.splitByWhitespace -import org.koitharu.kotatsu.test_util.component6 -import org.koitharu.kotatsu.test_util.component7 -import java.io.InputStream -import java.util.* -import java.util.concurrent.TimeUnit - -class InMemoryCookieJar : CookieJar { - - private val cache = HashMap() - - override fun loadForRequest(url: HttpUrl): List { - val time = System.currentTimeMillis() - return cache.values.filter { it.matches(url) && it.expiresAt >= time } - } - - override fun saveFromResponse(url: HttpUrl, cookies: List) { - cookies.forEach { - val key = CookieKey(url.host, it.name) - cache[key] = it - } - } - - fun loadFromStream(stream: InputStream) { - val reader = stream.bufferedReader() - for (line in reader.lineSequence()) { - if (line.isBlank() || line.startsWith("# ")) { - continue - } - val (host, _, path, secure, expire, name, value) = line.splitByWhitespace() - val domain = host.removePrefix("#HttpOnly_").trimStart('.') - val httpOnly = host.startsWith("#HttpOnly_") - val cookie = Cookie.Builder() - cookie.domain(domain) - if (httpOnly) cookie.httpOnly() - cookie.path(path) - if (secure.lowercase(Locale.ROOT).toBooleanStrict()) cookie.secure() - cookie.expiresAt(TimeUnit.SECONDS.toMillis(expire.toLong())) - cookie.name(name) - cookie.value(value) - insertCookie(domain, cookie.build()) - } - } - - private data class CookieKey( - val host: String, - val name: String, - ) -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt deleted file mode 100644 index 04dad7f83..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt +++ /dev/null @@ -1,107 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import com.koushikdutta.quack.QuackContext -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody -import org.koitharu.kotatsu.parsers.bitmap.Bitmap -import org.koitharu.kotatsu.parsers.config.MangaSourceConfig -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.network.UserAgents -import org.koitharu.kotatsu.parsers.util.await -import org.koitharu.kotatsu.parsers.util.requireBody -import org.koitharu.kotatsu.test_util.BitmapTestImpl -import java.awt.image.BufferedImage -import java.security.SecureRandom -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import javax.imageio.ImageIO -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLSocketFactory -import javax.net.ssl.X509TrustManager - -internal object MangaLoaderContextMock : MangaLoaderContext() { - - override val cookieJar = InMemoryCookieJar() - - override val httpClient: OkHttpClient = OkHttpClient.Builder() - .cookieJar(cookieJar) - .permissiveSSL() - .addInterceptor(CommonHeadersInterceptor()) - .addInterceptor(CloudFlareInterceptor()) - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(20, TimeUnit.SECONDS) - .build() - - init { - loadTestCookies() - } - - @Deprecated("Provide a base url") - override suspend fun evaluateJs(script: String): String? = evaluateJs("", script) - - override suspend fun evaluateJs(baseUrl: String, script: String): String? { - return QuackContext.create().use { - it.evaluate(script)?.toString() - } - } - - override fun getConfig(source: MangaSource): MangaSourceConfig { - return SourceConfigMock() - } - - override fun getDefaultUserAgent(): String = UserAgents.FIREFOX_MOBILE - - override fun redrawImageResponse(response: Response, redraw: (Bitmap) -> Bitmap): Response { - val srcImage = response.requireBody().byteStream().use(ImageIO::read) - checkNotNull(srcImage) { "Cannot decode image" } - val resImage = (redraw(BitmapTestImpl(srcImage)) as BitmapTestImpl) - return response.newBuilder() - .body(resImage.compress("png").toResponseBody("image/png".toMediaTypeOrNull())) - .build() - } - - override fun createBitmap(width: Int, height: Int): Bitmap { - return BitmapTestImpl(BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)) - } - - suspend fun doRequest(url: String, source: MangaSource?): Response { - val request = Request.Builder() - .get() - .url(url) - if (source != null) { - request.tag(MangaSource::class.java, source) - } - return httpClient.newCall(request.build()).await() - } - - private fun loadTestCookies() { - // https://addons.mozilla.org/ru/firefox/addon/cookies-txt/ - javaClass.getResourceAsStream("/cookies.txt")?.use { - cookieJar.loadFromStream(it) - } ?: println("No cookies loaded!") - } - - private fun OkHttpClient.Builder.permissiveSSL() = also { builder -> - runCatching { - val trustAllCerts = object : X509TrustManager { - override fun checkClientTrusted(chain: Array, authType: String) = Unit - - override fun checkServerTrusted(chain: Array, authType: String) = Unit - - override fun getAcceptedIssuers(): Array = emptyArray() - } - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, arrayOf(trustAllCerts), SecureRandom()) - val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory - builder.sslSocketFactory(sslSocketFactory, trustAllCerts) - builder.hostnameVerifier { _, _ -> true } - }.onFailure { - it.printStackTrace() - } - } - -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt deleted file mode 100644 index f1444c5bf..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt +++ /dev/null @@ -1,300 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import kotlinx.coroutines.test.runTest -import okhttp3.HttpUrl -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.params.ParameterizedTest -import org.koitharu.kotatsu.parsers.core.PagedMangaParser -import org.koitharu.kotatsu.parsers.core.SinglePageMangaParser -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.Include -import org.koitharu.kotatsu.parsers.model.search.SearchableField.* -import org.koitharu.kotatsu.parsers.util.medianOrNull -import org.koitharu.kotatsu.parsers.util.mimeType -import org.koitharu.kotatsu.test_util.* -import kotlin.time.Duration.Companion.minutes - -//@ExtendWith(AuthCheckExtension::class) -internal class MangaParserTest { - - private val context = MangaLoaderContextMock - private val timeout = 2.minutes - - @ParameterizedTest(name = "{index}|list|{0}") - @MangaSources - fun list(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val list = parser.getList(MangaSearchQuery.Builder().build()) - checkMangaList(list, "list") - assert(list.all { it.source == source }) - } - - @ParameterizedTest(name = "{index}|pagination|{0}") - @MangaSources - fun pagination(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - if (parser is SinglePageMangaParser) { - return@runTest - } - val page1 = parser.getList(MangaSearchQuery.EMPTY) - val page2 = - parser.getList(MangaSearchQuery.Builder().offset(page1.size).build()) - if (parser is PagedMangaParser) { - assert(parser.pageSize >= page1.size) { - "Page size is ${page1.size} but ${parser.pageSize} expected" - } - } - assert(page1.isNotEmpty()) { "Page 1 is empty" } - assert(page2.isNotEmpty()) { "Page 2 is empty" } - assert(page1 != page2) { "Pages are equal" } - val intersection = page1.intersect(page2.toSet()) - assert(intersection.isEmpty()) { - "Pages are intersected by " + intersection.size - } - } - - @ParameterizedTest(name = "{index}|search|{0}") - @MangaSources - fun searchByTitleName(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val subject = parser.getList(MangaSearchQuery.EMPTY).minByOrNull { - it.title.length - } ?: error("No manga found") - - val query = subject.title - check(query.isNotBlank()) { "Manga title '$query' is blank" } - val list = parser.getList( - MangaSearchQuery.Builder() - .order(SortOrder.RELEVANCE) - .criterion(QueryCriteria.Match(TITLE_NAME, query)) - .build(), - ) - assert(list.isNotEmpty()) { "Empty search results by \"$query\"" } - assert(list.singleOrNull { it.url == subject.url && it.id == subject.id } != null) { - "Single subject '${subject.title} (${subject.publicUrl})' not found in search results" - } - checkMangaList(list, "search('$query')") - assert(list.all { it.source == source }) - } - - @ParameterizedTest(name = "{index}|tags|{0}") - @MangaSources - fun tags(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val tags = parser.getFilterOptions().availableTags - assert(tags.isNotEmpty()) { "No tags found" } - val keys = tags.map { it.key } - assert(keys.isDistinct()) - assert("" !in keys) - val titles = tags.map { it.title } - assert(titles.isDistinct()) - assert("" !in titles) - assert(titles.all { it.isCapitalized() }) { - val badTags = titles.filterNot { it.isCapitalized() }.joinToString() - "Not all tags are capitalized: $badTags" - } - assert(tags.all { it.source == source }) - - val tag = tags.last() - val list = parser.getList( - MangaSearchQuery.Builder() - .offset(0) - .criterion(Include(TAG, setOf(tag))) - .build(), - ) - checkMangaList(list, "${tag.title} (${tag.key})") - assert(list.all { it.source == source }) - } - - @ParameterizedTest(name = "{index}|tags_multiple|{0}") - @MangaSources - fun tagsMultiple(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) -// if (!parser.filterCapabilities.isMultipleTagsSupported) return@runTest - val tags = parser.getFilterOptions().availableTags.shuffled().take(2).toSet() - - val list = parser.getList( - MangaSearchQuery.Builder() - .offset(0) - .criterion(Include(TAG, tags)) - .build(), - ) - - checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})") - assert(list.all { it.source == source }) - } - - @ParameterizedTest(name = "{index}|locale|{0}") - @MangaSources - fun locale(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val locales = parser.getFilterOptions().availableLocales - if (locales.isEmpty()) { - return@runTest - } - val locale = locales.random() - val list = parser.getList( - MangaSearchQuery.Builder() - .criterion(Include(LANGUAGE, setOf(locale))) - .criterion(Include(LANGUAGE, setOf(locale))) - .criterion(Include(ORIGINAL_LANGUAGE, setOf(locales.random()))) - .build(), - ) - checkMangaList(list, locale.toString()) - assert(list.all { it.source == source }) - } - - - @ParameterizedTest(name = "{index}|details|{0}") - @MangaSources - fun details(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val list = parser.getList(MangaSearchQuery.EMPTY) - - val manga = list.random() - parser.getDetails(manga).apply { - assert(!chapters.isNullOrEmpty()) { "Chapters are null or empty" } - assert(publicUrl.isUrlAbsolute()) { "Manga public url is not absolute: '$publicUrl'" } - assert(description != null) { "Detailed description is null: '$publicUrl'" } - assert(title.startsWith(manga.title)) { - "Titles are mismatch: '$title' and '${manga.title}' for $publicUrl" - } - assert(this.source == source) - val c = checkNotNull(chapters) - assert(c.isDistinctBy { it.id }) { - "Chapters are not distinct by id: ${c.maxDuplicates { it.id }} for $publicUrl" - } - assert(c.isDistinctByNotNull { it.key() }) { - val dup = c.mapNotNull { it.key() }.maxDuplicates { it } - "Chapters are not distinct by branch/volume/number: $dup for $publicUrl" - } - assert(c.all { it.source == source }) - checkImageRequest(coverUrl, source) - largeCoverUrl?.let { - checkImageRequest(it, source) - } - } - } - - @ParameterizedTest(name = "{index}|pages|{0}") - @MangaSources - fun pages(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val list = parser.getList(MangaSearchQuery.EMPTY) - val manga = list.first() - val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null at ${manga.publicUrl}") - val pages = parser.getPages(chapter) - - assert(pages.isNotEmpty()) - assert(pages.isDistinctBy { it.id }) - assert(pages.all { it.source == source }) - - arrayOf( - pages.first(), - pages.medianOrNull() ?: error("No page"), - ).forEach { page -> - val pageUrl = parser.getPageUrl(page) - assert(pageUrl.isNotEmpty()) - assert(pageUrl.isUrlAbsolute()) - checkImageRequest(pageUrl, page.source) - } - } - - @ParameterizedTest(name = "{index}|favicon|{0}") - @MangaSources - fun favicon(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val favicons = parser.getFavicons() - val types = setOf("png", "svg", "ico", "gif", "jpg", "jpeg", "webp", "avif") - assert(favicons.isNotEmpty()) - favicons.forEach { - assert(it.url.isUrlAbsolute()) { "Favicon url is not absolute: ${it.url}" } - assert(it.type in types) { "Unknown icon type: ${it.type}" } - } - val favicon = favicons.find(24) - checkNotNull(favicon) - checkImageRequest(favicon.url, source) - } - - @ParameterizedTest(name = "{index}|domain|{0}") - @MangaSources - fun domain(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val defaultDomain = parser.domain - val url = HttpUrl.Builder().host(defaultDomain).scheme("https").toString() - val response = context.doRequest(url, source) - val realUrl = response.request.url - val realDomain = realUrl.topPrivateDomain() - val realHost = realUrl.host - assert(defaultDomain == realHost || defaultDomain == realDomain) { - "Domain mismatch:\nRequired:\t\t\t$defaultDomain\nActual:\t\t\t$realDomain\nHost:\t\t\t$realHost" - } - } - - @ParameterizedTest(name = "{index}|link|{0}") - @MangaSources - fun link(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - val manga = parser.getList(MangaSearchQuery.Builder().build()).first() - val resolved = context.newLinkResolver(manga.publicUrl).getManga() - Assertions.assertNotNull(resolved) - resolved ?: return@runTest - Assertions.assertEquals(manga.id, resolved.id) - Assertions.assertEquals(manga.publicUrl, resolved.publicUrl) - Assertions.assertEquals(manga.url, resolved.url) - Assertions.assertEquals(manga.title, resolved.title) - } - - @ParameterizedTest(name = "{index}|authorization|{0}") - @MangaSources - @Disabled - fun authorization(source: MangaParserSource) = runTest(timeout = timeout) { - val parser = context.newParserInstance(source) - if (parser is MangaParserAuthProvider) { - val username = parser.getUsername() - assert(username.isNotBlank()) { "Username is blank" } - println("Signed in to ${source.name} as $username") - } - } - - private suspend fun checkMangaList(list: List, cause: String) { - assert(list.isNotEmpty()) { "Manga list for '$cause' is empty" } - assert(list.isDistinctBy { it.id }) { "Manga list for '$cause' contains duplicated ids" } - for (item in list) { - assert(item.url.isNotEmpty()) { "Url is empty" } - assert(!item.url.isUrlAbsolute()) { "Url looks like absolute: ${item.url}" } - item.coverUrl?.let { - assert(it.isUrlAbsolute()) { "Cover url is not absolute: ${item.coverUrl}" } - } - assert(item.title.isNotEmpty()) { "Title for ${item.publicUrl} is empty" } - assert(item.publicUrl.isUrlAbsolute()) - } - val testItem = list.random() - checkImageRequest(testItem.coverUrl, testItem.source) - } - - private suspend fun checkImageRequest(url: String?, source: MangaSource) { - if (url == null) { - return - } - context.doRequest(url, source).use { - assert(it.isSuccessful) { "Request failed: ${it.code}(${it.message}): $url" } - assert(it.mimeType?.startsWith("image/") == true) { - "Wrong response mime type: ${it.mimeType}" - } - } - } - - private fun String.isCapitalized(): Boolean { - return !first().isLowerCase() - } - - private fun MangaChapter.key(): Any? = when { - number > 0f && volume > 0 -> Triple(branch, volume, number) - number > 0f -> Pair(branch, number) - else -> null - } -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt deleted file mode 100644 index 752930766..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import org.junit.jupiter.params.provider.EnumSource -import org.koitharu.kotatsu.parsers.model.MangaParserSource - -// Change 'names' to test specified parsers -@EnumSource(MangaParserSource::class, names = [], mode = EnumSource.Mode.INCLUDE) -internal annotation class MangaSources diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt deleted file mode 100644 index 1141c0094..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.config.MangaSourceConfig - -internal class SourceConfigMock : MangaSourceConfig { - - override fun get(key: ConfigKey): T = key.defaultValue -} \ No newline at end of file diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt deleted file mode 100644 index 86b1757fa..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -package org.koitharu.kotatsu.parsers.model.search - -import org.junit.jupiter.api.Assertions.assertDoesNotThrow -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Test -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.MangaState -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* -import org.koitharu.kotatsu.parsers.model.search.SearchableField.* -import java.util.* - -class MangaSearchQueryCapabilitiesTest { - - private val capabilities = MangaSearchQueryCapabilities( - capabilities = setOf( - SearchCapability(TITLE_NAME, setOf(Match::class), isMultiple = false, isExclusive = true), - SearchCapability(TAG, setOf(Include::class, Exclude::class), isMultiple = true, isExclusive = false), - SearchCapability(PUBLICATION_YEAR, setOf(Range::class), isMultiple = false, isExclusive = false), - SearchCapability(STATE, setOf(Include::class), isMultiple = false, isExclusive = false), - ), - ) - - @Test - fun validateValidSingleCriterionQuery() { - val query = MangaSearchQuery.Builder() - .criterion(Match(TITLE_NAME, "title")) - .build() - - assertDoesNotThrow { capabilities.validate(query) } - } - - @Test - fun validateUnsupportedFieldThrowsException() { - val query = MangaSearchQuery.Builder() - .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.ENGLISH))) - .build() - - assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } - } - - @Test - fun validateUnsupportedMultiValueThrowsException() { - val query = MangaSearchQuery.Builder() - .criterion(Include(STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))) - .build() - - assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } - } - - @Test - fun validateMultipleCriteriaWithOtherCriteriaAllowed() { - val query = MangaSearchQuery.Builder() - .criterion(Include(TAG, setOf(buildTag("tag1"), buildTag("tag2")))) - .criterion(Exclude(TAG, setOf(buildTag("tag3")))) - .build() - - assertDoesNotThrow { capabilities.validate(query) } - } - - @Test - fun validateMultipleCriteriaWithStrictCapabilityThrowsException() { - val query = MangaSearchQuery.Builder() - .criterion(Match(TITLE_NAME, "title")) - .criterion(Range(PUBLICATION_YEAR, 1990, 2000)) - .build() - - assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } - } - - private fun buildTag(name: String) = MangaTag(title = name, key = "${name}Key", source = MangaParserSource.MANGADEX) -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt deleted file mode 100644 index 9f4501f6f..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import org.junit.jupiter.api.Test -import org.koitharu.kotatsu.parsers.MangaLoaderContextMock -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.newParser -import java.io.File - -class IntentFilterGenerator { - - @Test - fun generateIntentFilter() { - val output = File("out/test/resources/intent-filter.xml") - output.printWriter(Charsets.UTF_8).use { writer -> - writer.appendLine("") - writer.appendTab().appendLine("") - writer.appendLine() - writer.appendTab().appendLine("") - writer.appendTab().appendLine("") - writer.appendLine() - writer.appendTab().appendLine("") - writer.appendTab().appendLine("") - writer.appendLine() - for (source in MangaParserSource.entries) { - val parser = source.newParser(MangaLoaderContextMock) - parser.configKeyDomain.presetValues.forEach { domain -> - writer.appendTab().append("") - } - } - writer.appendLine() - writer.appendLine("") - } - println(output.absolutePath) - } - - private fun Appendable.appendTab() = append('\t') -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt deleted file mode 100644 index 78bf4e5c3..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.koitharu.kotatsu.parsers.MangaLoaderContextMock -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import kotlin.time.Duration.Companion.minutes - -internal class LinkResolverTest { - - private val context = MangaLoaderContextMock - - @Test - fun supportedSource() = runTest(timeout = 2.minutes) { - val resolver = context.newLinkResolver("REDACTED" /* do not publish links to manga on GitHub */) - Assertions.assertEquals(MangaParserSource.MANGADEX, resolver.getSource()) - val manga = resolver.getManga() - Assertions.assertEquals(resolver.link.toString(), manga?.publicUrl) - } - - @Test - fun unsupportedSource2() = runTest(timeout = 2.minutes) { - val resolver = context.newLinkResolver("REDACTED" /* do not publish links to manga on GitHub */) - Assertions.assertEquals(MangaParserSource.BATOTO, resolver.getSource()) - val manga = resolver.getManga() - Assertions.assertEquals(resolver.link.toString(), manga?.publicUrl) - } -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt deleted file mode 100644 index a6ed694ae..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.ContentType.MANGA -import org.koitharu.kotatsu.parsers.model.ContentType.MANHUA -import org.koitharu.kotatsu.parsers.model.Demographic.SEINEN -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* -import org.koitharu.kotatsu.parsers.model.search.SearchableField.* -import java.util.* - -class ListFilterToSearchQueryConverterTest { - - @Test - fun convertToMangaSearchQueryTest() { - val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) - val excludedTags = setOf(buildMangaTag("exclude_tag")) - val states = setOf(MangaState.ONGOING) - val contentRatings = setOf(ContentRating.SAFE) - val contentTypes = setOf(MANGA, MANHUA) - val demographics = setOf(SEINEN) - - val filter = MangaListFilter( - query = "title_name", - tags = tags, - tagsExclude = excludedTags, - locale = Locale.ENGLISH, - originalLocale = Locale.JAPANESE, - states = states, - contentRating = contentRatings, - types = contentTypes, - demographics = demographics, - year = 2020, - yearFrom = 1997, - yearTo = 2024, - ) - - val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) - - val expectedQuery = MangaSearchQuery.Builder() - .offset(0) - .order(SortOrder.NEWEST) - .criterion(Match(TITLE_NAME, "title_name")) - .criterion(Include(TAG, tags)) - .criterion(Exclude(TAG, excludedTags)) - .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) - .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) - .criterion(Include(STATE, states)) - .criterion(Include(CONTENT_RATING, contentRatings)) - .criterion(Include(CONTENT_TYPE, contentTypes)) - .criterion(Include(DEMOGRAPHIC, demographics)) - .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) - .criterion(Match(PUBLICATION_YEAR, 2020)) - .build() - - assertEquals(expectedQuery, searchQuery) - } - - @Test - fun convertToMangaSearchQueryWithEmptyFieldsTest() { - val filter = MangaListFilter() - - val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) - - assertEquals(MangaSearchQuery.Builder().offset(0).order(SortOrder.NEWEST).build(), searchQuery) - } - - private fun buildMangaTag(name: String): MangaTag { - return MangaTag( - key = "${name}Key", - title = name, - source = MangaParserSource.MANGADEX, - ) - } -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt deleted file mode 100644 index 7b7fe70e7..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -internal class PaginatorTest { - - @Test - fun singlePaginationTest() { - val paginator = Paginator(24) - assertEquals(1, paginator.getPage(0)) - assertEquals(2, paginator.getPage(24)) - assertEquals(3, paginator.getPage(48)) - } - - @Test - fun adaptivePaginationTest() { - val paginator = Paginator(12) - assertEquals(1, paginator.getPage(0)) - paginator.onListReceived(0, 1, 24) - assertEquals(2, paginator.getPage(24)) - paginator.onListReceived(24, 2, 18) - assertEquals(3, paginator.getPage(42)) - } - - @Test - fun endReachPaginationTest() { - val pageSize = 24 - val paginator = Paginator(pageSize) - var size = 0 - repeat(5) { i -> - val offset = i * pageSize - assertEquals(i + 1, paginator.getPage(offset)) - paginator.onListReceived(offset, i + 1, pageSize) - size += pageSize - } - val nextPage = paginator.getPage(size) - assertEquals(nextPage, paginator.getPage(size)) - paginator.onListReceived(size, nextPage, 0) - assertEquals(nextPage, paginator.getPage(size)) - } -} \ No newline at end of file diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt deleted file mode 100644 index f9dfbc11e..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.koitharu.kotatsu.parsers.model.ContentRating -import org.koitharu.kotatsu.parsers.model.ContentType.MANGA -import org.koitharu.kotatsu.parsers.model.ContentType.MANHUA -import org.koitharu.kotatsu.parsers.model.Demographic.SEINEN -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.MangaState -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery -import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* -import org.koitharu.kotatsu.parsers.model.search.SearchableField.* -import java.util.* - -class ConvertToMangaListFilterTest { - - @Test - fun convertToMangaListFilterTest() { - val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) - val excludedTags = setOf(buildMangaTag("exclude_tag")) - val states = setOf(MangaState.ONGOING) - val contentRatings = setOf(ContentRating.SAFE) - val contentTypes = setOf(MANGA, MANHUA) - val demographics = setOf(SEINEN) - - val query = MangaSearchQuery.Builder() - .criterion(Match(TITLE_NAME, "title_name")) - .criterion(Include(TAG, tags)) - .criterion(Exclude(TAG, excludedTags)) - .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) - .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) - .criterion(Include(STATE, states)) - .criterion(Include(CONTENT_RATING, contentRatings)) - .criterion(Include(CONTENT_TYPE, contentTypes)) - .criterion(Include(DEMOGRAPHIC, demographics)) - .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) - .criterion(Match(PUBLICATION_YEAR, 2020)) - .build() - - val listFilter = convertToMangaListFilter(query) - - assertEquals(listFilter.query, "title_name") - assertEquals(listFilter.tags, tags) - assertEquals(listFilter.tagsExclude, excludedTags) - assertEquals(listFilter.locale, Locale.ENGLISH) - assertEquals(listFilter.originalLocale, Locale.JAPANESE) - assertEquals(listFilter.states, states) - assertEquals(listFilter.contentRating, contentRatings) - assertEquals(listFilter.types, contentTypes) - assertEquals(listFilter.demographics, demographics) - assertEquals(listFilter.year, 2020) - assertEquals(listFilter.yearFrom, 1997) - assertEquals(listFilter.yearTo, 2024) - } - - @Test - fun convertToMangaListFilterWithMultipleTagsIncludeTest() { - val tags1 = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) - val tags2 = setOf(buildMangaTag("tag3"), buildMangaTag("tag4")) - - val query = MangaSearchQuery.Builder() - .criterion(Include(TAG, tags1)) - .criterion(Include(TAG, tags2)) - .build() - - val listFilter = convertToMangaListFilter(query) - - assertEquals(listFilter.tags, tags1 union tags2) - } - - @Test - fun convertToMangaListFilterWithUnsupportedFieldTest() { - val query = MangaSearchQuery.Builder() - .criterion(Include(AUTHOR, setOf(buildMangaTag("author")))) - .build() - - val exception = assertThrows { - convertToMangaListFilter(query) - } - - assert(exception.message!!.contains("Unsupported field for Include criterion: AUTHOR")) - } - - private fun buildMangaTag(name: String): MangaTag { - return MangaTag( - key = "${name}Key", - title = name, - source = MangaParserSource.MANGADEX, - ) - } -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt b/src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt deleted file mode 100644 index b566bafbf..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.koitharu.kotatsu.test_util - -import org.koitharu.kotatsu.parsers.bitmap.Bitmap -import org.koitharu.kotatsu.parsers.bitmap.Rect -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import javax.imageio.ImageIO - -class BitmapTestImpl( - val image: BufferedImage, -) : Bitmap { - - override val width: Int - get() = image.width - - override val height: Int - get() = image.height - - override fun drawBitmap( - sourceBitmap: Bitmap, - src: Rect, - dst: Rect, - ) { - val graphics = image.createGraphics() - val subImage = (sourceBitmap as BitmapTestImpl).image.getSubimage( - src.left, src.top, src.width, src.height, - ) - graphics.drawImage(subImage, dst.left, dst.top, dst.width, dst.height, null) - graphics.dispose() - } - - fun compress(format: String): ByteArray = ByteArrayOutputStream().also { stream -> - ImageIO.write(image, format, stream) - }.toByteArray() -} diff --git a/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt b/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt deleted file mode 100644 index 5a631d8a0..000000000 --- a/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.koitharu.kotatsu.test_util - -import androidx.collection.ArraySet -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN -import org.koitharu.kotatsu.parsers.util.LONG_HASH_SEED -import org.koitharu.kotatsu.parsers.util.toRelativeUrl - -private val PATTERN_URL_ABSOLUTE = Regex("^https?://[\\s\\S]+", setOf(RegexOption.IGNORE_CASE)) -private val PATTERN_URL_RELATIVE = Regex("^/[\\s\\S]+", setOf(RegexOption.IGNORE_CASE)) - -internal fun Collection.isDistinct(): Boolean { - val set = ArraySet(size) - for (item in this) { - if (!set.add(item)) { - return false - } - } - return set.size == size -} - -internal fun Collection.isDistinctBy(selector: (T) -> K): Boolean { - val set = ArraySet(size) - for (item in this) { - if (!set.add(selector(item))) { - return false - } - } - return set.size == size -} - -internal fun Collection.isDistinctByNotNull(selector: (T) -> K?): Boolean { - val set = ArraySet(size) - for (item in this) { - if (!set.add(selector(item) ?: continue)) { - return false - } - } - return true -} - -internal fun String.isUrlRelative() = matches(PATTERN_URL_RELATIVE) -internal fun String.isUrlAbsolute() = matches(PATTERN_URL_ABSOLUTE) - -internal inline fun Collection.maxDuplicates(selector: (T) -> K): K? { - return groupBy(selector).maxByOrNull { it.value.size }?.key -} - -@Suppress("NOTHING_TO_INLINE") -inline operator fun List.component6(): T = get(5) - -@Suppress("NOTHING_TO_INLINE") -inline operator fun List.component7(): T = get(6) - -fun mangaOf(source: MangaParserSource, url: String): Manga { - val httpUrl = url.toHttpUrlOrNull() - var id = LONG_HASH_SEED - source.name.forEach { c -> id = 31 * id + c.code } - url.forEach { c -> id = 31 * id + c.code } - return Manga( - id = id, - title = httpUrl?.pathSegments?.last() ?: url, - altTitles = emptySet(), - url = httpUrl?.let { url.toRelativeUrl(it.host) } ?: url, - publicUrl = url, - rating = RATING_UNKNOWN, - contentRating = null, - coverUrl = "", - tags = emptySet(), - state = null, - authors = emptySet(), - largeCoverUrl = null, - description = null, - chapters = null, - source = source, - ) -} diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties deleted file mode 100644 index 82de190bf..000000000 --- a/src/test/resources/junit-platform.properties +++ /dev/null @@ -1,2 +0,0 @@ -junit.jupiter.execution.parallel.enabled = true -junit.jupiter.execution.parallel.mode.default = concurrent \ No newline at end of file From bae934b9a0942e63b6b9f3f7dd9afbc05adf71b3 Mon Sep 17 00:00:00 2001 From: InvalidDavid <118831950+InvalidDavid@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:38:26 +0200 Subject: [PATCH 2/5] Revert "Migrate to new parsers structure (#416)" (#421) This reverts commit c20c30c0ee505a27f8905e0b017f720bce393a57. --- build.gradle.kts | 12 +- gradle/libs.versions.toml | 9 +- .../build.gradle.kts | 0 .../kotatsu/parsers/ksp/ParserProcessor.kt | 133 +------- .../parsers/ksp/ParserProcessorProvider.kt | 0 .../org/koitharu/kotatsu/parsers/ksp/Utils.kt | 0 ...ols.ksp.processing.SymbolProcessorProvider | 0 settings.gradle.kts | 7 +- .../org/koitharu/kotatsu/parsers/Broken.kt | 14 + .../koitharu/kotatsu/parsers/ErrorMessages.kt | 18 ++ .../kotatsu/parsers/InternalParsersApi.kt | 14 + .../kotatsu/parsers/MangaLoaderContext.kt | 78 +++++ .../koitharu/kotatsu/parsers/MangaParser.kt | 88 +++++ .../parsers/MangaParserAuthProvider.kt | 30 ++ .../kotatsu/parsers/MangaSourceParser.kt | 28 ++ .../koitharu/kotatsu/parsers/bitmap/Bitmap.kt | 9 + .../koitharu/kotatsu/parsers/bitmap/Rect.kt | 15 + .../parsers/compat/TachiyomiSourceAdapter.kt | 242 -------------- .../kotatsu/parsers/config/ConfigKey.kt | 37 +++ .../parsers/config/MangaSourceConfig.kt | 6 + .../parsers/core/AbstractMangaParser.kt | 107 +++++++ .../parsers/core/FlexibleMangaParser.kt | 96 ++++++ .../parsers/core/FlexiblePagedMangaParser.kt | 63 ++++ .../parsers/core/MangaParserWrapper.kt | 79 +++++ .../kotatsu/parsers/core/PagedMangaParser.kt | 57 ++++ .../parsers/core/SinglePageMangaParser.kt | 24 ++ .../exception/AuthRequiredException.kt | 13 + .../exception/ContentUnavailableException.kt | 3 + .../parsers/exception/GraphQLException.kt | 16 + .../parsers/exception/NotFoundException.kt | 9 + .../parsers/exception/ParseException.kt | 9 + .../exception/TooManyRequestExceptions.kt | 31 ++ .../kotatsu/parsers/model/Constants.kt | 11 + .../kotatsu/parsers/model/ContentRating.kt | 7 + .../kotatsu/parsers/model/ContentType.kt | 36 +++ .../kotatsu/parsers/model/Demographic.kt | 10 + .../koitharu/kotatsu/parsers/model/Favicon.kt | 28 ++ .../kotatsu/parsers/model/Favicons.kt | 59 ++++ .../koitharu/kotatsu/parsers/model/Manga.kt | 203 ++++++++++++ .../kotatsu/parsers/model/MangaChapter.kt | 65 ++++ .../kotatsu/parsers/model/MangaListFilter.kt | 88 +++++ .../model/MangaListFilterCapabilities.kt | 56 ++++ .../parsers/model/MangaListFilterOptions.kt | 45 +++ .../kotatsu/parsers/model/MangaPage.kt | 22 ++ .../kotatsu/parsers/model/MangaSource.kt | 6 + .../kotatsu/parsers/model/MangaState.kt | 5 + .../kotatsu/parsers/model/MangaTag.kt | 16 + .../kotatsu/parsers/model/SortOrder.kt | 22 ++ .../koitharu/kotatsu/parsers/model/WordSet.kt | 11 + .../parsers/model/search/MangaSearchQuery.kt | 92 ++++++ .../search/MangaSearchQueryCapabilities.kt | 50 +++ .../parsers/model/search/QueryCriteria.kt | 108 +++++++ .../parsers/model/search/SearchCapability.kt | 36 +++ .../parsers/model/search/SearchableField.kt | 24 ++ .../parsers/network/CloudFlareHelper.kt | 58 ++++ .../kotatsu/parsers/network/CommonHeaders.kt | 77 +++++ .../parsers/network/OkHttpWebClient.kt | 133 ++++++++ .../parsers/network/RateLimitHelper.kt | 41 +++ .../kotatsu/parsers/network/UserAgents.kt | 27 ++ .../kotatsu/parsers/network/WebClient.kt | 117 +++++++ .../parsers/network/utils/RateLimiter.kt | 46 +++ .../kotatsu/parsers/site/all/ComicKLive.kt | 7 +- .../parsers/site/all/ExHentaiParser.kt | 5 +- .../parsers/site/all/HitomiLaParser.kt | 9 +- .../kotatsu/parsers/site/all/ImHentai.kt | 5 +- .../kotatsu/parsers/site/all/Koharu.kt | 16 +- .../parsers/site/all/LineWebtoonsParser.kt | 5 +- .../parsers/site/all/MangaFireParser.kt | 7 +- .../kotatsu/parsers/site/all/MangaPark.kt | 5 +- .../parsers/site/all/MangaPlusParser.kt | 5 +- .../parsers/site/all/MangaReaderToParser.kt | 5 +- .../kotatsu/parsers/site/all/Manhwa210.kt | 7 +- .../kotatsu/parsers/site/all/Misskon.kt | 5 +- .../kotatsu/parsers/site/all/Multporn.kt | 5 +- .../parsers/site/all/MyReadingManga.kt | 5 +- .../parsers/site/all/NineMangaParser.kt | 7 +- .../site/all/NineNineNineHentaiParser.kt | 5 +- .../parsers/site/all/WebtoonsParser.kt | 5 +- .../kotatsu/parsers/site/all/WeebDex.kt | 10 +- .../animebootstrap/AnimeBootstrapParser.kt | 5 +- .../parsers/site/animebootstrap/fr/PapScan.kt | 5 +- .../kotatsu/parsers/site/ar/AzoraMoon.kt | 5 +- .../kotatsu/parsers/site/ar/FlixScans.kt | 5 +- .../kotatsu/parsers/site/ar/HentaMan.kt | 5 +- .../kotatsu/parsers/site/ar/MangaStorm.kt | 5 +- .../kotatsu/parsers/site/ar/TeamXNovel.kt | 5 +- .../kotatsu/parsers/site/be/AnibelParser.kt | 11 +- .../parsers/site/cupfox/CupFoxParser.kt | 5 +- .../parsers/site/en/AsuraScansParser.kt | 10 +- .../kotatsu/parsers/site/en/Atsumaru.kt | 5 +- .../kotatsu/parsers/site/en/BatCave.kt | 5 +- .../kotatsu/parsers/site/en/BeeToon.kt | 5 +- .../kotatsu/parsers/site/en/ComicExtra.kt | 5 +- .../kotatsu/parsers/site/en/DemonicScans.kt | 5 +- .../kotatsu/parsers/site/en/DynastyScans.kt | 5 +- .../kotatsu/parsers/site/en/FanFoxParser.kt | 5 +- .../kotatsu/parsers/site/en/FlameComics.kt | 5 +- .../kotatsu/parsers/site/en/FlixScansOrg.kt | 3 +- .../kotatsu/parsers/site/en/Hentalk.kt | 12 +- .../kotatsu/parsers/site/en/HeyToonParser.kt | 5 +- .../kotatsu/parsers/site/en/KdtScans.kt | 5 +- .../kotatsu/parsers/site/en/MangaGeko.kt | 5 +- .../parsers/site/en/MangaHomeParser.kt | 5 +- .../kotatsu/parsers/site/en/MangaKatana.kt | 5 +- .../kotatsu/parsers/site/en/MangaKawaiiEn.kt | 5 +- .../kotatsu/parsers/site/en/MangaPill.kt | 7 +- .../parsers/site/en/MangaTownParser.kt | 7 +- .../kotatsu/parsers/site/en/Mangago.kt | 5 +- .../kotatsu/parsers/site/en/Mangaowl.kt | 5 +- .../kotatsu/parsers/site/en/Manhwa18Com.kt | 9 +- .../kotatsu/parsers/site/en/Manhwa18Parser.kt | 5 +- .../kotatsu/parsers/site/en/ManhwasMen.kt | 5 +- .../kotatsu/parsers/site/en/MyComicList.kt | 5 +- .../kotatsu/parsers/site/en/Po2Scans.kt | 5 +- .../kotatsu/parsers/site/en/Pururin.kt | 5 +- .../kotatsu/parsers/site/en/RoliaScan.kt | 3 +- .../kotatsu/parsers/site/en/VioletScans.kt | 5 +- .../kotatsu/parsers/site/en/VyManga.kt | 9 +- .../kotatsu/parsers/site/en/WeebCentral.kt | 7 +- .../kotatsu/parsers/site/en/comix/Comix.kt | 5 +- .../parsers/site/es/HouseMangasParser.kt | 1 - .../kotatsu/parsers/site/es/InMangaParser.kt | 3 +- .../parsers/site/es/TuMangaOnlineParser.kt | 5 +- .../kotatsu/parsers/site/es/ZonaTmoParser.kt | 3 +- .../parsers/site/fmreader/FmreaderParser.kt | 10 +- .../parsers/site/foolslide/FoolSlideParser.kt | 5 +- .../kotatsu/parsers/site/fr/AnimeSama.kt | 3 +- .../kotatsu/parsers/site/fr/AstralManga.kt | 23 +- .../parsers/site/fr/BentomangaParser.kt | 5 +- .../kotatsu/parsers/site/fr/BigSolo.kt | 8 +- .../kotatsu/parsers/site/fr/FuryoSociety.kt | 3 +- .../kotatsu/parsers/site/fr/InovaScanManga.kt | 3 +- .../parsers/site/fr/LegacyScansParser.kt | 5 +- .../kotatsu/parsers/site/fr/LireScan.kt | 5 +- .../kotatsu/parsers/site/fr/LugnicaScans.kt | 3 +- .../kotatsu/parsers/site/fr/MangaKawaii.kt | 5 +- .../kotatsu/parsers/site/fr/MangaMana.kt | 5 +- .../kotatsu/parsers/site/fr/MangaMoins.kt | 3 +- .../kotatsu/parsers/site/fr/OrtegaScans.kt | 1 - .../parsers/site/fr/PhenixscansParser.kt | 5 +- .../kotatsu/parsers/site/fr/PoseidonScans.kt | 9 +- .../kotatsu/parsers/site/fr/PunkRecordz.kt | 4 +- .../kotatsu/parsers/site/fr/ScansMangasMe.kt | 5 +- .../kotatsu/parsers/site/fr/ScantradUnion.kt | 5 +- .../site/fuzzydoodle/FuzzyDoodleParser.kt | 5 +- .../parsers/site/gallery/GalleryParser.kt | 3 +- .../site/galleryadults/GalleryAdultsParser.kt | 9 +- .../parsers/site/galleryadults/all/Hentai3.kt | 19 +- .../site/galleryadults/all/HentaiEnvy.kt | 17 +- .../site/galleryadults/all/HentaiEra.kt | 21 +- .../site/galleryadults/all/HentaiForce.kt | 19 +- .../site/galleryadults/all/HentaiFox.kt | 19 +- .../site/galleryadults/all/NHentaiParser.kt | 5 +- .../site/galleryadults/all/NHentaiToParser.kt | 7 +- .../galleryadults/all/NHentaiXxxParser.kt | 5 +- .../site/galleryadults/en/HentaiNexus.kt | 10 +- .../site/galleryadults/en/HentaiRead.kt | 17 +- .../parsers/site/gattsu/GattsuParser.kt | 5 +- .../kotatsu/parsers/site/guya/GuyaParser.kt | 5 +- .../kotatsu/parsers/site/heancms/HeanCms.kt | 7 +- .../parsers/site/heancms/en/TempleScan.kt | 5 +- .../parsers/site/heancms/fr/PerfScan.kt | 3 +- .../parsers/site/heancmsalt/HeanCmsAlt.kt | 3 +- .../parsers/site/hotcomics/HotComicsParser.kt | 5 +- .../parsers/site/id/DoujinDesuParser.kt | 7 +- .../kotatsu/parsers/site/id/HentaiCrot.kt | 5 +- .../kotatsu/parsers/site/id/Komikcast.kt | 5 +- .../kotatsu/parsers/site/id/Kumapage.kt | 5 +- .../kotatsu/parsers/site/id/PixHentai.kt | 5 +- .../kotatsu/parsers/site/id/Shinigami.kt | 10 +- .../kotatsu/parsers/site/id/ThemangaParser.kt | 3 +- .../parsers/site/id/WestmangaParser.kt | 7 +- .../kotatsu/parsers/site/iken/IkenParser.kt | 5 +- .../koitharu/kotatsu/parsers/site/ja/Klz9.kt | 5 +- .../parsers/site/ja/NicovideoSeigaParser.kt | 23 +- .../parsers/site/keyoapp/KeyoappParser.kt | 8 +- .../parsers/site/likemanga/LikeMangaParser.kt | 5 +- .../parsers/site/liliana/LilianaParser.kt | 5 +- .../parsers/site/madara/MadaraParser.kt | 20 +- .../parsers/site/madara/all/Manga18Fx.kt | 5 +- .../parsers/site/madara/all/Manhwa18Cc.kt | 5 +- .../parsers/site/madara/ar/ArabsHentai.kt | 5 +- .../parsers/site/madara/ar/RocksManga.kt | 5 +- .../parsers/site/madara/en/AdultWebtoon.kt | 5 +- .../parsers/site/madara/en/Hentai4Free.kt | 5 +- .../parsers/site/madara/en/HentaiManga.kt | 5 +- .../parsers/site/madara/en/HentaiWebtoon.kt | 5 +- .../parsers/site/madara/en/IsekaiScan.kt | 5 +- .../site/madara/en/IsekaiScanEuParser.kt | 5 +- .../parsers/site/madara/en/MangaDass.kt | 5 +- .../parsers/site/madara/en/MangaDna.kt | 5 +- .../parsers/site/madara/en/MangaPure.kt | 5 +- .../kotatsu/parsers/site/madara/en/Manhwaz.kt | 5 +- .../parsers/site/madara/en/ManyToon.kt | 5 +- .../parsers/site/madara/es/DoujinHentaiNet.kt | 5 +- .../parsers/site/madara/es/TmoManga.kt | 5 +- .../parsers/site/madara/fr/RaijinScans.kt | 3 +- .../parsers/site/madara/id/ManhwaHub.kt | 5 +- .../parsers/site/madara/vi/HentaiCube.kt | 10 +- .../parsers/site/madara/vi/HentaiVnPlus.kt | 10 +- .../parsers/site/madara/vi/Saytruyenhay.kt | 5 +- .../parsers/site/madtheme/MadthemeParser.kt | 5 +- .../parsers/site/madtheme/en/ManhuaScan.kt | 8 +- .../parsers/site/manga18/Manga18Parser.kt | 7 +- .../parsers/site/mangabox/MangaboxParser.kt | 7 +- .../site/mangadventure/MangAdventureParser.kt | 5 +- .../site/mangareader/MangaReaderParser.kt | 5 +- .../parsers/site/mangareader/ar/Normoyun.kt | 5 +- .../parsers/site/mangareader/en/RizzComic.kt | 7 +- .../parsers/site/mangareader/en/Zahard.kt | 5 +- .../site/mangareader/es/CerberuSeries.kt | 5 +- .../site/mangareader/es/HentaiReader.kt | 5 +- .../site/mangareader/es/LectorHentai.kt | 5 +- .../parsers/site/mangareader/es/MangaTv.kt | 5 +- .../parsers/site/mangareader/es/TenkaiScan.kt | 3 +- .../parsers/site/mangareader/es/TuManhwas.kt | 5 +- .../site/mangareader/fr/RevolutionScantrad.kt | 3 +- .../parsers/site/mangareader/id/KomikIndo.kt | 5 +- .../parsers/site/mangareader/id/KomikSan.kt | 5 +- .../site/mangaworld/MangaWorldParser.kt | 10 +- .../kotatsu/parsers/site/manhwaz/ManhwaZ.kt | 5 +- .../parsers/site/mmrcms/MmrcmsParser.kt | 7 +- .../kotatsu/parsers/site/natsu/NatsuParser.kt | 10 +- .../parsers/site/nepnep/NepnepParser.kt | 7 +- .../otakusanctuary/OtakuSanctuaryParser.kt | 5 +- .../site/pizzareader/PizzaReaderParser.kt | 5 +- .../kotatsu/parsers/site/pt/AnimeXNovel.kt | 3 +- .../kotatsu/parsers/site/pt/BrMangas.kt | 7 +- .../kotatsu/parsers/site/pt/InstaHentai.kt | 5 +- .../kotatsu/parsers/site/pt/LerManga.kt | 3 +- .../kotatsu/parsers/site/pt/LerMangaOnline.kt | 5 +- .../parsers/site/pt/LuratoonScansParser.kt | 3 +- .../kotatsu/parsers/site/pt/MangaOnline.kt | 5 +- .../kotatsu/parsers/site/pt/Manhastro.kt | 5 +- .../kotatsu/parsers/site/pt/MediocreToons.kt | 8 +- .../kotatsu/parsers/site/pt/MuitoHentai.kt | 5 +- .../kotatsu/parsers/site/pt/SssScanlator.kt | 5 +- .../kotatsu/parsers/site/pt/SussyScan.kt | 6 +- .../kotatsu/parsers/site/pt/TaiyoParser.kt | 2 +- .../kotatsu/parsers/site/pt/YugenMangas.kt | 5 +- .../kotatsu/parsers/site/ru/AComics.kt | 5 +- .../kotatsu/parsers/site/ru/ComXParser.kt | 5 +- .../kotatsu/parsers/site/ru/DesuMeParser.kt | 7 +- .../kotatsu/parsers/site/ru/MangaWtfParser.kt | 5 +- .../kotatsu/parsers/site/ru/NudeMoonParser.kt | 5 +- .../kotatsu/parsers/site/ru/RemangaParser.kt | 7 +- .../kotatsu/parsers/site/ru/ZenMangaParser.kt | 10 +- .../parsers/site/ru/grouple/GroupleParser.kt | 4 +- .../parsers/site/ru/multichan/ChanParser.kt | 6 +- .../site/ru/multichan/HenChanParser.kt | 47 ++- .../parsers/site/ru/rulib/LibSocialParser.kt | 5 +- .../kotatsu/parsers/site/scan/ScanParser.kt | 11 +- .../kotatsu/parsers/site/sinmh/SinmhParser.kt | 5 +- .../kotatsu/parsers/site/tr/ElderManga.kt | 5 +- .../kotatsu/parsers/site/tr/MangaAy.kt | 5 +- .../kotatsu/parsers/site/tr/SadScans.kt | 5 +- .../kotatsu/parsers/site/tr/TenshiManga.kt | 5 +- .../kotatsu/parsers/site/tr/TrWebtoon.kt | 7 +- .../kotatsu/parsers/site/tr/UzayManga.kt | 5 +- .../parsers/site/uk/HentaiUkrParser.kt | 13 +- .../parsers/site/uk/HoneyMangaParser.kt | 7 +- .../parsers/site/uk/MangaInUaParser.kt | 5 +- .../kotatsu/parsers/site/vi/BFANGTeam.kt | 10 +- .../parsers/site/vi/BlogTruyenParser.kt | 5 +- .../kotatsu/parsers/site/vi/BlogTruyenVN.kt | 5 +- .../kotatsu/parsers/site/vi/CMangaParser.kt | 5 +- .../parsers/site/vi/CuuTruyenParser.kt | 7 +- .../kotatsu/parsers/site/vi/DamCoNuong.kt | 5 +- .../kotatsu/parsers/site/vi/DuaLeoTruyen.kt | 5 +- .../kotatsu/parsers/site/vi/GocTruyenTranh.kt | 3 +- .../parsers/site/vi/GocTruyenTranhVui.kt | 5 +- .../kotatsu/parsers/site/vi/HangTruyen.kt | 5 +- .../kotatsu/parsers/site/vi/Hentai18VN.kt | 5 +- .../kotatsu/parsers/site/vi/HentaiVNParser.kt | 5 +- .../kotatsu/parsers/site/vi/HentaiVnBuzz.kt | 5 +- .../kotatsu/parsers/site/vi/HentaiVnSU.kt | 5 +- .../kotatsu/parsers/site/vi/KuroNeko.kt | 10 +- .../kotatsu/parsers/site/vi/LangGeekParser.kt | 5 +- .../kotatsu/parsers/site/vi/LxManga.kt | 7 +- .../kotatsu/parsers/site/vi/MimiHentai.kt | 14 +- .../kotatsu/parsers/site/vi/NhentaiWorld.kt | 10 +- .../kotatsu/parsers/site/vi/OTruyenParser.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenGG.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenHentai18.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenHentaiVN.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenQQ.kt | 5 +- .../kotatsu/parsers/site/vi/TruyenTranh3Q.kt | 5 +- .../kotatsu/parsers/site/vi/TuSachXinhXinh.kt | 7 +- .../kotatsu/parsers/site/vi/VcomycsParser.kt | 5 +- .../parsers/site/vi/YuriGardenParser.kt | 10 +- .../kotatsu/parsers/site/vi/YurinekoParser.kt | 5 +- .../kotatsu/parsers/site/vmp/VmpParser.kt | 5 +- .../parsers/site/wpcomics/WpComicsParser.kt | 5 +- .../parsers/site/wpcomics/en/XoxoComics.kt | 5 +- .../parsers/site/wpcomics/ja/MangaRaw.kt | 5 +- .../parsers/site/wpcomics/vi/DocTruyen3Q.kt | 5 +- .../parsers/site/wpcomics/vi/MeHentaiVN.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyen1975.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenFE.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenHE.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenLL.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenSSR.kt | 5 +- .../parsers/site/wpcomics/vi/NetTruyenUU.kt | 5 +- .../parsers/site/wpcomics/vi/TopTruyen.kt | 5 +- .../site/zeistmanga/ZeistMangaParser.kt | 5 +- .../kotatsu/parsers/site/zh/Baozimh.kt | 5 +- .../kotatsu/parsers/site/zh/KomiicParser.kt | 7 +- .../parsers/site/zh/ManhuaguiParser.kt | 5 +- .../parsers/site/zmanga/ZMangaParser.kt | 9 +- .../koitharu/kotatsu/parsers/util/Assert.kt | 8 + .../kotatsu/parsers/util/CSSBackground.kt | 52 +++ .../koitharu/kotatsu/parsers/util/Chapters.kt | 75 +++++ .../kotatsu/parsers/util/Collection.kt | 101 ++++++ .../parsers/util/ContinuationCallCallback.kt | 34 ++ .../kotatsu/parsers/util/CookieJar.kt | 48 +++ .../kotatsu/parsers/util/Coroutines.kt | 35 ++ .../kotatsu/parsers/util/CryptoAES.kt | 127 ++++++++ .../org/koitharu/kotatsu/parsers/util/Enum.kt | 13 + .../kotatsu/parsers/util/FaviconParser.kt | 92 ++++++ .../koitharu/kotatsu/parsers/util/Jsoup.kt | 225 +++++++++++++ .../kotatsu/parsers/util/LinkResolver.kt | 11 +- .../kotatsu/parsers/util/MangaParserEnv.kt | 96 ++++++ .../kotatsu/parsers/util/MangaParserExt.kt | 20 -- .../kotatsu/parsers/util/MangaParsersUtils.kt | 18 ++ .../koitharu/kotatsu/parsers/util/Number.kt | 85 +++++ .../koitharu/kotatsu/parsers/util/OkHttp.kt | 55 ++++ .../kotatsu/parsers/util/Paginator.kt | 25 ++ .../koitharu/kotatsu/parsers/util/Parse.kt | 101 ++++++ .../kotatsu/parsers/util/RateLimit.kt | 81 +++++ .../parsers/util/RelatedMangaFinder.kt | 57 ++++ .../koitharu/kotatsu/parsers/util/Result.kt | 41 +++ .../parsers/util/SearchQueryConverter.kt | 250 +++++++++++++++ .../koitharu/kotatsu/parsers/util/String.kt | 237 ++++++++++++++ .../kotatsu/parsers/util/WebViewHelper.kt | 2 +- .../kotatsu/parsers/util/json/EscapeUtils.kt | 38 +++ .../util/json/JSONArrayTypedIterator.kt | 33 ++ .../util/json/JSONArrayTypedListWrapper.kt | 53 ++++ .../json/JSONObjectTypedIterableWrapper.kt | 24 ++ .../kotatsu/parsers/util/json/JsonExt.kt | 167 ++++++++++ .../kotatsu/parsers/util/json/NextJs.kt | 220 +++++++++++++ .../util/suspendlazy/SoftSuspendLazyImpl.kt | 41 +++ .../parsers/util/suspendlazy/SuspendLazy.kt | 39 +++ .../util/suspendlazy/SuspendLazyImpl.kt | 47 +++ .../kotatsu/parsers/AuthCheckExtension.kt | 31 ++ .../kotatsu/parsers/CloudFlareInterceptor.kt | 22 ++ .../parsers/CloudFlareProtectedException.kt | 9 + .../parsers/CommonHeadersInterceptor.kt | 40 +++ .../kotatsu/parsers/InMemoryCookieJar.kt | 55 ++++ .../kotatsu/parsers/MangaLoaderContextMock.kt | 107 +++++++ .../kotatsu/parsers/MangaParserTest.kt | 300 ++++++++++++++++++ .../koitharu/kotatsu/parsers/MangaSources.kt | 8 + .../kotatsu/parsers/SourceConfigMock.kt | 9 + .../model/search/MangaSearchQueryTest.kt | 72 +++++ .../parsers/util/IntentFilterGenerator.kt | 37 +++ .../kotatsu/parsers/util/LinkResolverTest.kt | 29 ++ .../ListFilterToSearchQueryConverterTest.kt | 77 +++++ .../kotatsu/parsers/util/PaginatorTest.kt | 42 +++ .../SearchQueryToListFilterConverterTest.kt | 94 ++++++ .../kotatsu/test_util/BitmapTestImpl.kt | 35 ++ .../org/koitharu/kotatsu/test_util/Util.kt | 79 +++++ src/test/resources/junit-platform.properties | 2 + 361 files changed, 6571 insertions(+), 1341 deletions(-) rename {plugins-ksp => kotatsu-parsers-ksp}/build.gradle.kts (100%) rename {plugins-ksp => kotatsu-parsers-ksp}/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt (52%) rename {plugins-ksp => kotatsu-parsers-ksp}/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt (100%) rename {plugins-ksp => kotatsu-parsers-ksp}/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt (100%) rename {plugins-ksp => kotatsu-parsers-ksp}/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider (100%) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt delete mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt create mode 100644 src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt create mode 100644 src/test/resources/junit-platform.properties diff --git a/build.gradle.kts b/build.gradle.kts index c18cdd566..37f640c47 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,6 @@ plugins { `maven-publish` alias(libs.plugins.kotlin.jvm) alias(libs.plugins.ksp) - alias(libs.plugins.kotlin.serialization) } group = "org.koitharu" @@ -21,9 +20,6 @@ ksp { tasks.jar { archiveFileName.set("kotatsu-parsers.jar") - exclude("android/**") - exclude("androidx/annotation/**") - exclude("androidx/preference/**") } tasks.withType().configureEach { @@ -40,6 +36,7 @@ tasks.withType().configureEach kotlin { jvmToolchain(11) + explicitApiWarning() sourceSets["main"].kotlin.srcDirs("build/generated/ksp/main/kotlin") } @@ -53,18 +50,13 @@ publishing { dependencies { implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) implementation(libs.okhttp) implementation(libs.okio) implementation(libs.json) implementation(libs.androidx.collection) - - api(libs.core.parsers) api(libs.jsoup) - compileOnly(libs.android.stubs) - - ksp(project(":plugins-ksp")) + ksp(project(":kotatsu-parsers-ksp")) testImplementation(libs.junit.api) testImplementation(libs.junit.engine) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4fa61a4f4..77ce34a61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,4 @@ [versions] -androidx-collection = "1.6.0" -android-stubs = "4.1.1.4" -core = "b62a0681d7" kotlin = "2.2.10" ksp = "2.2.10-2.0.2" coroutines = "1.10.2" @@ -9,26 +6,22 @@ junit = "5.14.3" okhttp = "5.3.2" okio = "3.17.0" json = "20251224" +androidx-collection = "1.6.0" # Keep Android 5, 6 compatible (with SDK 21 - 23) #noinspection NewerVersionAvailable jsoup = "1.17.2" quickjs = "1.1.0" korte = "4.0.10" -serialization = "1.8.0" simplexml = "2.7.1" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } [libraries] -android-stubs = { module = "com.google.android:android", version.ref = "android-stubs" } -core-parsers = { module = "com.github.UsagiApp:core-parsers", version.ref = "core"} ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } diff --git a/plugins-ksp/build.gradle.kts b/kotatsu-parsers-ksp/build.gradle.kts similarity index 100% rename from plugins-ksp/build.gradle.kts rename to kotatsu-parsers-ksp/build.gradle.kts diff --git a/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt b/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt similarity index 52% rename from plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt rename to kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt index bacd2079c..c44795ca1 100644 --- a/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt +++ b/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt @@ -20,14 +20,10 @@ class ParserProcessor( private val sourceNamePattern = Regex("[A-Z_][A-Z0-9_]{3,}") override fun process(resolver: Resolver): List { - val kotatsuSymbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.MangaSourceParser") - val tachiyomiSymbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.TachiyomiSource") - - val kotatsuRet = kotatsuSymbols.filterNot { it.validate() }.toList() - val tachiyomiRet = tachiyomiSymbols.filterNot { it.validate() }.toList() - - if (!kotatsuSymbols.iterator().hasNext() && !tachiyomiSymbols.iterator().hasNext()) { - return kotatsuRet + tachiyomiRet + val symbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.MangaSourceParser") + val ret = symbols.filterNot { it.validate() }.toList() + if (!symbols.iterator().hasNext()) { + return ret } val dependencies = Dependencies.ALL_FILES val factoryFile = @@ -54,18 +50,17 @@ class ParserProcessor( } val totalCount = sourcesFile?.writer().use { sourcesWriter -> factoryFile?.writer().use { factoryWriter -> - writeContent(sourcesWriter, factoryWriter, kotatsuSymbols, tachiyomiSymbols) + writeContent(sourcesWriter, factoryWriter, symbols) } } writeSummary(totalCount) - return kotatsuRet + tachiyomiRet + return ret } private fun writeContent( sourcesWriter: Writer?, factoryWriter: Writer?, - kotatsuSymbols: Sequence, - tachiyomiSymbols: Sequence, + symbols: Sequence, ): Int { if (sourcesWriter == null && factoryWriter == null) { return 0 @@ -76,7 +71,6 @@ class ParserProcessor( import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.core.MangaParserWrapper - import org.koitharu.kotatsu.parsers.compat.TachiyomiSourceAdapter internal fun MangaParserSource.newParser(context: MangaLoaderContext): MangaParser = when (this) { @@ -87,27 +81,19 @@ class ParserProcessor( package org.koitharu.kotatsu.parsers.model public enum class MangaParserSource( - override val title: String, - override val locale: String, - override val contentType: ContentType, - override val isBroken: Boolean, + public val title: String, + public val locale: String, + public val contentType: ContentType, + public val isBroken: Boolean, ): MangaSource { """.trimIndent(), ) - // Process native Kotatsu parsers - val kotatsuVisitor = ParserVisitor(sourcesWriter, factoryWriter) - val kotatsuCount = kotatsuSymbols - .filter { it is KSClassDeclaration && it.validate() } - .onEach { it.accept(kotatsuVisitor, Unit) } - .count() - - // Process Tachiyomi source bridges - val tachiyomiVisitor = TachiyomiVisitor(sourcesWriter, factoryWriter, kotatsuVisitor.titles) - val tachiyomiCount = tachiyomiSymbols + val visitor = ParserVisitor(sourcesWriter, factoryWriter) + val totalCount = symbols .filter { it is KSClassDeclaration && it.validate() } - .onEach { it.accept(tachiyomiVisitor, Unit) } + .onEach { it.accept(visitor, Unit) } .count() factoryWriter?.write( @@ -126,7 +112,7 @@ class ParserProcessor( } """.trimIndent(), ) - return kotatsuCount + tachiyomiCount + return totalCount } private fun writeSummary(totalCount: Int) { @@ -138,7 +124,7 @@ class ParserProcessor( private val sourcesWriter: Writer?, private val factoryWriter: Writer?, ) : KSVisitorVoid() { - val titles = HashMap() + private val titles = HashMap() override fun visitClassDeclaration( classDeclaration: KSClassDeclaration, @@ -197,91 +183,4 @@ class ParserProcessor( ) } } - - /** - * Visitor for @TachiyomiSource annotations. - * Generates factory entries that instantiate the Tachiyomi HttpSource and wrap it with TachiyomiSourceAdapter. - */ - private inner class TachiyomiVisitor( - private val sourcesWriter: Writer?, - private val factoryWriter: Writer?, - private val titles: HashMap, - ) : KSVisitorVoid() { - - override fun visitClassDeclaration( - classDeclaration: KSClassDeclaration, - data: Unit, - ) { - val annotation = classDeclaration.annotations.single { it.shortName.asString() == "TachiyomiSource" } - - // Get annotation arguments with defaults - val nameArg = annotation.arguments.find { it.name?.asString() == "name" }?.value as? String ?: "" - val titleArg = annotation.arguments.find { it.name?.asString() == "title" }?.value as? String ?: "" - val localeArg = annotation.arguments.find { it.name?.asString() == "locale" }?.value as? String ?: "" - val typeArg = annotation.arguments.find { it.name?.asString() == "type" }?.value?.toString() - val type = if (typeArg.isNullOrBlank() || typeArg == "null") "ContentType.MANGA" else typeArg - - // Auto-detect values from class if not provided - val className = classDeclaration.simpleName.asString() - val qualifiedClassName = checkNotNull(classDeclaration.qualifiedName?.asString()) { - "Class qualified name is null" - } - - // Generate source name from class name (UPPER_CASE) - val sourceName = nameArg.takeIf { it.isNotBlank() } - ?: className.uppercase().replace(Regex("[^A-Z0-9_]"), "_").let { - // Ensure it starts with a letter - if (it.firstOrNull()?.isLetter() == true) it else "SOURCE_$it" - } - - // Use provided title or class name - val sourceTitle = titleArg.takeIf { it.isNotBlank() } ?: className - - // Auto-detect locale from package name if not provided - val locale = localeArg.takeIf { it.isNotBlank() } - ?: detectLocaleFromPackage(qualifiedClassName) - - val localeString = "\"$locale\"" - val localeObj = if (locale.isEmpty()) null else Locale(locale) - val localeTitle = localeObj?.getDisplayLanguage(localeObj) - - if (localeObj != null && localeObj !in availableLocales) { - logger.error("Tachiyomi source $sourceName has wrong locale: $localeTitle") - } - - if (!sourceNamePattern.matches(sourceName)) { - logger.error("Tachiyomi source name must be uppercase: $sourceName") - } - - val prevTitleClass = titles.put(sourceTitle, qualifiedClassName) - if (prevTitleClass != null) { - logger.warn("Source title duplication: \"$sourceTitle\" is assigned to both $prevTitleClass and $qualifiedClassName") - } - - // Factory: wrap the Tachiyomi source with TachiyomiSourceAdapter - factoryWriter?.write( - "\tMangaParserSource.$sourceName -> TachiyomiSourceAdapter(context, MangaParserSource.$sourceName, $qualifiedClassName())\n" - ) - - // Enum entry - val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty() - sourcesWriter?.write( - "\t$sourceName(\"$sourceTitle\", $localeString$localeComment, $type, false),\n", - ) - } - - private fun detectLocaleFromPackage(qualifiedName: String): String { - // Common locale patterns in package names - val localePatterns = mapOf( - ".vi." to "vi", ".en." to "en", ".ja." to "ja", ".ko." to "ko", - ".zh." to "zh", ".es." to "es", ".fr." to "fr", ".de." to "de", - ".it." to "it", ".pt." to "pt", ".ru." to "ru", ".ar." to "ar", - ".th." to "th", ".id." to "id", ".tr." to "tr", ".pl." to "pl", - ".all." to "", ".multi." to "" - ) - - return localePatterns.entries.find { qualifiedName.contains(it.key) }?.value - ?: "" - } - } } diff --git a/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt b/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt similarity index 100% rename from plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt rename to kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessorProvider.kt diff --git a/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt b/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt similarity index 100% rename from plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt rename to kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/Utils.kt diff --git a/plugins-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/kotatsu-parsers-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider similarity index 100% rename from plugins-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider rename to kotatsu-parsers-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider diff --git a/settings.gradle.kts b/settings.gradle.kts index 4cd34da13..834fbf540 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,13 +11,8 @@ dependencyResolutionManagement { google() mavenCentral() gradlePluginPortal() - maven { - url = uri( - "https://jitpack.io" - ) - } } } rootProject.name = "kotatsu-parsers" -include("plugins-ksp") +include("kotatsu-parsers-ksp") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt new file mode 100644 index 000000000..74d03077c --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/Broken.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.parsers + +/** + * Annotate [MangaParser] implementation to mark this parser as broken instead of removing it + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +internal annotation class Broken( + + /** + * Reason why this parser is broken + */ + val message: String = "", +) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt new file mode 100644 index 000000000..d20006eb7 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/ErrorMessages.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers + +public object ErrorMessages { + + public const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED: String = "Multiple states are not supported by this source" + public const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED: String = "Multiple genres are not supported by this source" + public const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED: String = + "Multiple Content ratings are not supported by this source" + public const val FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED: String = + "Multiple Content types are not supported by this source" + public const val FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED: String = + "Multiple Demographics are not supported by this source" + public const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED: String = + "Filtering by both genres and locale is not supported by this source" + public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String = + "Filtering by both genres and states is not supported by this source" + public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt new file mode 100644 index 000000000..b3efbd678 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/InternalParsersApi.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.parsers + +/** + * This marker distinguishes the internal API and is used to opt-in for that feature when parsers developing. + * + * Any usage of a declaration annotated with `@InternalParsersApi` must be accepted either by + * annotating that usage with the [OptIn] annotation, e.g. `@OptIn(InternalParsersApi::class)`, + * or by using the compiler argument `-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi`. + */ +@Retention(AnnotationRetention.BINARY) +@SinceKotlin("1.3") +@RequiresOptIn +@MustBeDocumented +public annotation class InternalParsersApi diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt new file mode 100644 index 000000000..438cb1a59 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt @@ -0,0 +1,78 @@ +package org.koitharu.kotatsu.parsers + +import okhttp3.CookieJar +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Response +import org.koitharu.kotatsu.parsers.bitmap.Bitmap +import org.koitharu.kotatsu.parsers.config.MangaSourceConfig +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.LinkResolver +import java.util.* + +public abstract class MangaLoaderContext { + + public abstract val httpClient: OkHttpClient + + public abstract val cookieJar: CookieJar + + public fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this) + + public fun newLinkResolver(link: HttpUrl): LinkResolver = LinkResolver(this, link) + + public fun newLinkResolver(link: String): LinkResolver = newLinkResolver(link.toHttpUrl()) + + public open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data) + + public open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data) + + public open fun getPreferredLocales(): List = listOf(Locale.getDefault()) + + /** + * Execute JavaScript code and return result + * @param script JavaScript source code + * @return execution result as string, may be null + */ + @Deprecated("Provide a base url") + public abstract suspend fun evaluateJs(script: String): String? + + /** + * Execute JavaScript code and return result + * @param script JavaScript source code + * @param baseUrl url of page script will be executed in context of + * @return execution result as string, may be null + */ + public abstract suspend fun evaluateJs(baseUrl: String, script: String): String? + + /** + * Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization) + */ + public open fun requestBrowserAction(parser: MangaParser, url: String): Nothing { + throw UnsupportedOperationException("Browser is not available") + } + + public abstract fun getConfig(source: MangaSource): MangaSourceConfig + + public abstract fun getDefaultUserAgent(): String + + /** + * Helper function to be used in an interceptor + * to descramble images + * @param response Image response + * @param redraw lambda function to implement descrambling logic + */ + public abstract fun redrawImageResponse( + response: Response, + redraw: (image: Bitmap) -> Bitmap, + ): Response + + /** + * create a new empty Bitmap with given dimensions + */ + public abstract fun createBitmap( + width: Int, + height: Int, + ): Bitmap +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt new file mode 100644 index 000000000..24fff1db4 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt @@ -0,0 +1,88 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers + +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.Interceptor +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.config.MangaSourceConfig +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities +import org.koitharu.kotatsu.parsers.util.LinkResolver +import java.util.* + +public interface MangaParser : Interceptor { + + public val source: MangaParserSource + + /** + * Supported [SortOrder] variants. Must not be empty. + * + * For better performance use [EnumSet] for more than one item. + */ + public val availableSortOrders: Set + + @Deprecated("Too complex. Use filterCapabilities instead") + public val searchQueryCapabilities: MangaSearchQueryCapabilities + + public val filterCapabilities: MangaListFilterCapabilities + + public val config: MangaSourceConfig + + public val authorizationProvider: MangaParserAuthProvider? + get() = this as? MangaParserAuthProvider + + /** + * Provide default domain and available alternatives, if any. + * + * Never hardcode domain in requests, use [domain] instead. + */ + public val configKeyDomain: ConfigKey.Domain + + public val domain: String + + @Deprecated("Too complex. Use getList with filter instead") + public suspend fun getList(query: MangaSearchQuery): List + + public suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List + + /** + * Parse details for [Manga]: chapters list, description, large cover, etc. + * Must return the same manga, may change any fields excepts id, url and source + * @see Manga.copy + */ + public suspend fun getDetails(manga: Manga): Manga + + /** + * Parse pages list for specified chapter. + * @see MangaPage for details + */ + public suspend fun getPages(chapter: MangaChapter): List + + /** + * Fetch direct link to the page image. + */ + public suspend fun getPageUrl(page: MangaPage): String + + public suspend fun getFilterOptions(): MangaListFilterOptions + + /** + * Parse favicons from the main page of the source`s website + */ + public suspend fun getFavicons(): Favicons + + public fun onCreateConfig(keys: MutableCollection>) + + public suspend fun getRelatedManga(seed: Manga): List + + public fun getRequestHeaders(): Headers + + /** + * Return [Manga] object by web link to it + * @see [Manga.publicUrl] + */ + @InternalParsersApi + public suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt new file mode 100644 index 000000000..dc25d5862 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt @@ -0,0 +1,30 @@ +package org.koitharu.kotatsu.parsers + +import org.koitharu.kotatsu.parsers.exception.AuthRequiredException +import org.koitharu.kotatsu.parsers.exception.ParseException + +/** + * Implement this in your parser for authorization support + */ +public interface MangaParserAuthProvider { + + /** + * Return link to the login page, which will be opened in browser. + * Must be an absolute url + */ + public val authUrl: String + + /** + * Quick check if user is logged in. + * In most case you should check for cookies in [MangaLoaderContext.cookieJar]. + */ + public suspend fun isAuthorized(): Boolean + + /** + * Fetch and return current user`s name or login. + * Normally should not be called if [isAuthorized] returns false + * @throws [AuthRequiredException] if user is not logged in or authorization is expired + * @throws [ParseException] on parsing error + */ + public suspend fun getUsername(): String +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt new file mode 100644 index 000000000..b708d547e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaSourceParser.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.parsers + +import org.koitharu.kotatsu.parsers.model.ContentType + +/** + * Annotate each [MangaParser] implementation with this annotation, used by codegen + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +internal annotation class MangaSourceParser( + /** + * Name of manga source. Used as an Enum value, must be UPPER_CASE and unique. + */ + val name: String, + /** + * User-friendly title of manga source. In most case equals the website name. + * Avoid extra whitespaces between the words if it is not required. + */ + val title: String, + /** + * Language code (for example "en" or "ru") or blank if parser provide manga on different languages. + */ + val locale: String = "", + /** + * Type of content provided by parser. See [ContentType] for more info + */ + val type: ContentType = ContentType.MANGA, +) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt new file mode 100644 index 000000000..f39d29dee --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Bitmap.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu.parsers.bitmap + +public interface Bitmap { + + public val width: Int + public val height: Int + + public fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt new file mode 100644 index 000000000..602182967 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/bitmap/Rect.kt @@ -0,0 +1,15 @@ +package org.koitharu.kotatsu.parsers.bitmap + +public data class Rect( + val left: Int = 0, + val top: Int = 0, + val right: Int = 0, + val bottom: Int = 0, +) { + + val width: Int + get() = right - left + + val height: Int + get() = bottom - top +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt deleted file mode 100644 index b1a7ab6fb..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt +++ /dev/null @@ -1,242 +0,0 @@ -@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") - -package org.koitharu.kotatsu.parsers.compat - -import android.app.Application -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.Headers -import okhttp3.Interceptor -import okhttp3.Response -import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.core.AbstractMangaParser -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.generateUid -import uy.kohesive.injekt.Injekt - -/** - * Adapter that wraps a Tachiyomi [HttpSource] and exposes it as a Kotatsu parser via [AbstractMangaParser]. - * Enables Tachiyomi extension sources to be used within the Kotatsu/Usagi ecosystem. - */ -@OptIn(InternalParsersApi::class) -internal class TachiyomiSourceAdapter( - context: MangaLoaderContext, - source: MangaParserSource, - private val tachiyomiSource: HttpSource, -) : AbstractMangaParser(context, source) { - - init { - // Inject MangaLoaderContext's OkHttpClient into Tachiyomi's NetworkHelper - // so extensions use the same client (cookies, CF-bypass interceptors, etc.) - NetworkHelper.setClient(context.httpClient) - - // Register Application context so Injekt.get() works - // (used by extensions that access WebView or SharedPreferences) - // We use reflection to grab the real Android Application context from ActivityThread - runCatching { - val activityThreadClass = Class.forName("android.app.ActivityThread") - val currentApplicationMethod = activityThreadClass.getMethod("currentApplication") - val app = currentApplicationMethod.invoke(null) as? Application - if (app != null) { - Injekt.addSingleton(app) - } else { - Injekt.addSingleton(Application()) - } - }.onFailure { - Injekt.addSingleton(Application()) - } - } - - override val configKeyDomain: ConfigKey.Domain by lazy { - val host = runCatching { - java.net.URI(tachiyomiSource.baseUrl).host - ?: tachiyomiSource.baseUrl.toHostOnly() - }.getOrDefault(tachiyomiSource.baseUrl.toHostOnly()) - ConfigKey.Domain(host) - } - - override val availableSortOrders: Set = buildSet { - add(SortOrder.POPULARITY) - add(SortOrder.UPDATED) - if (tachiyomiSource.supportsLatest) { - add(SortOrder.NEWEST) - } - } - - @InternalParsersApi - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isSearchSupported = true, - isMultipleTagsSupported = false, - ) - - // ============================== List =================================== - - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val page = (offset / PAGE_SIZE) + 1 - - val mangasPage: MangasPage = withContext(Dispatchers.IO) { - when { - !query.isNullOrEmpty() -> { - val filterList = tachiyomiSource.getFilterList() - tachiyomiSource.fetchSearchManga(page, query, filterList) - .toBlocking().single() - } - order == SortOrder.NEWEST && tachiyomiSource.supportsLatest -> { - tachiyomiSource.fetchLatestUpdates(page).toBlocking().single() - } - else -> { - tachiyomiSource.fetchPopularManga(page).toBlocking().single() - } - } - } - - return mangasPage.mangas.map { it.toManga() } - } - - // ============================== Details ================================ - - override suspend fun getDetails(manga: Manga): Manga { - val sManga = manga.toSManga() - - return withContext(Dispatchers.IO) { - val details = tachiyomiSource.fetchMangaDetails(sManga).toBlocking().single() - val chapters = tachiyomiSource.fetchChapterList(sManga).toBlocking().single() - - manga.copy( - title = details.title.ifEmpty { manga.title }, - altTitles = emptySet(), - coverUrl = details.thumbnail_url ?: manga.coverUrl, - largeCoverUrl = details.thumbnail_url, - description = details.description, - authors = setOfNotNull(details.author, details.artist) - .filter { it.isNotBlank() }.toSet(), - tags = parseGenreTags(details.genre), - state = mapStatus(details.status), - chapters = chapters.mapIndexed { index, sChapter -> - sChapter.toMangaChapter(index, chapters.size) - }, - ) - } - } - - // ============================== Pages ================================== - - override suspend fun getPages(chapter: MangaChapter): List { - val sChapter = SChapter.create().apply { - url = chapter.url - name = chapter.title ?: "" - chapter_number = chapter.number - } - - val pages = withContext(Dispatchers.IO) { - tachiyomiSource.fetchPageList(sChapter).toBlocking().single() - } - - return pages.map { page -> - val imgUrl = page.imageUrl ?: page.url - MangaPage( - id = generateUid(imgUrl.ifEmpty { "${chapter.url}#${page.index}" }), - url = imgUrl, - preview = null, - source = source, - ) - } - } - - override suspend fun getPageUrl(page: MangaPage): String { - val url = page.url - if (url.startsWith("http://") || url.startsWith("https://")) return url - return "https://$domain$url" - } - - // ============================== Filter Options ========================= - - override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() - - // ============================== Headers ================================ - - override fun getRequestHeaders(): Headers = tachiyomiSource.headers - - // ============================== Interceptor ============================ - - override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) - - // ============================== Private Helpers ======================== - - private fun SManga.toManga(): Manga { - val mangaUrl = this.url - val publicUrl = if (mangaUrl.startsWith("http")) mangaUrl - else "${tachiyomiSource.baseUrl}$mangaUrl" - - return Manga( - id = generateUid(mangaUrl), - title = this.title, - altTitles = emptySet(), - url = mangaUrl, - publicUrl = publicUrl, - rating = RATING_UNKNOWN, - contentRating = null, - coverUrl = this.thumbnail_url, - tags = parseGenreTags(this.genre), - state = mapStatus(this.status), - authors = setOfNotNull(this.author, this.artist) - .filter { it.isNotBlank() }.toSet(), - source = source, - ) - } - - private fun Manga.toSManga(): SManga = SManga.create().apply { - url = this@toSManga.url - title = this@toSManga.title - thumbnail_url = this@toSManga.coverUrl - } - - private fun SChapter.toMangaChapter(index: Int, total: Int): MangaChapter { - return MangaChapter( - id = generateUid(this.url), - title = this.name.ifBlank { null }, - number = if (this.chapter_number > 0f) this.chapter_number else (total - index).toFloat(), - volume = 0, - url = this.url, - scanlator = this.scanlator, - uploadDate = this.date_upload, - branch = null, - source = source, - ) - } - - private fun parseGenreTags(genreStr: String?): Set { - if (genreStr.isNullOrBlank()) return emptySet() - return genreStr.split(",") - .map { it.trim() } - .filter { it.isNotBlank() } - .mapTo(mutableSetOf()) { genre -> - MangaTag(title = genre, key = genre.lowercase(), source = source) - } - } - - private fun mapStatus(status: Int): MangaState? = when (status) { - SManga.ONGOING -> MangaState.ONGOING - SManga.COMPLETED -> MangaState.FINISHED - SManga.ON_HIATUS -> MangaState.PAUSED - SManga.CANCELLED -> MangaState.ABANDONED - else -> null - } - - private fun String.toHostOnly(): String = - removePrefix("https://").removePrefix("http://").substringBefore("/") - - companion object { - private const val PAGE_SIZE = 20 - } -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt new file mode 100644 index 000000000..cd233f1d9 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/config/ConfigKey.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.parsers.config + +public sealed class ConfigKey( + @JvmField public val key: String, +) { + + public abstract val defaultValue: T + + public class Domain( + @JvmField @JvmSuppressWildcards public vararg val presetValues: String, + ) : ConfigKey("domain") { + + init { + require(presetValues.isNotEmpty()) { "You must provide at least one domain" } + } + + override val defaultValue: String + get() = presetValues.first() + } + + public class ShowSuspiciousContent( + override val defaultValue: Boolean, + ) : ConfigKey("show_suspicious") + + public class UserAgent( + override val defaultValue: String, + ) : ConfigKey("user_agent") + + public class SplitByTranslations( + override val defaultValue: Boolean, + ) : ConfigKey("split_translations") + + public class PreferredImageServer( + public val presetValues: Map, + override val defaultValue: String?, + ) : ConfigKey("img_server") +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt new file mode 100644 index 000000000..d4e11c49f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/config/MangaSourceConfig.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.parsers.config + +public interface MangaSourceConfig { + + public operator fun get(key: ConfigKey): T +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt new file mode 100644 index 000000000..9edbd6b8c --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/AbstractMangaParser.kt @@ -0,0 +1,107 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.core + +import androidx.annotation.CallSuper +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.config.MangaSourceConfig +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities +import org.koitharu.kotatsu.parsers.network.CommonHeaders +import org.koitharu.kotatsu.parsers.network.OkHttpWebClient +import org.koitharu.kotatsu.parsers.network.WebClient +import org.koitharu.kotatsu.parsers.util.* +import java.util.* + +@Suppress("OVERRIDE_DEPRECATION") +@InternalParsersApi +public abstract class AbstractMangaParser @InternalParsersApi constructor( + @property:InternalParsersApi public val context: MangaLoaderContext, + public final override val source: MangaParserSource, +) : MangaParser { + + public final override val searchQueryCapabilities: MangaSearchQueryCapabilities + get() = filterCapabilities.toMangaSearchQueryCapabilities() + + public override val config: MangaSourceConfig by lazy { context.getConfig(source) } + + public open val sourceLocale: Locale + get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale) + + protected val sourceContentRating: ContentRating? + get() = if (source.contentType == ContentType.HENTAI) { + ContentRating.ADULT + } else { + null + } + + protected val isNsfwSource: Boolean = source.contentType == ContentType.HENTAI + + protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent()) + + override fun getRequestHeaders(): Headers = Headers.Builder() + .add(CommonHeaders.USER_AGENT, config[userAgentKey]) + .build() + + /** + * Used as fallback if value of `order` passed to [getList] is null + */ + public open val defaultSortOrder: SortOrder + get() { + val supported = availableSortOrders + return SortOrder.entries.first { it in supported } + } + + final override val domain: String + get() = config[configKeyDomain] + + protected open val webClient: WebClient = OkHttpWebClient(context.httpClient, source) + + /** + * Search list of manga by specified searchQuery + * + * @param query searchQuery + */ + public final override suspend fun getList(query: MangaSearchQuery): List = getList( + offset = query.offset, + order = query.order ?: defaultSortOrder, + filter = convertToMangaListFilter(query), + ) + + /** + * Fetch direct link to the page image. + */ + public override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain) + + /** + * Parse favicons from the main page of the source`s website + */ + public override suspend fun getFavicons(): Favicons { + return FaviconParser(webClient, domain).parseFavicons() + } + + @CallSuper + public override fun onCreateConfig(keys: MutableCollection>) { + keys.add(configKeyDomain) + } + + public override suspend fun getRelatedManga(seed: Manga): List { + return RelatedMangaFinder(listOf(this)).invoke(seed) + } + + /** + * Return [Manga] object by web link to it + * @see [Manga.publicUrl] + */ + override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null + + override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt new file mode 100644 index 000000000..1346246a1 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexibleMangaParser.kt @@ -0,0 +1,96 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.core + +import androidx.annotation.CallSuper +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.config.MangaSourceConfig +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.network.CommonHeaders +import org.koitharu.kotatsu.parsers.network.OkHttpWebClient +import org.koitharu.kotatsu.parsers.network.WebClient +import org.koitharu.kotatsu.parsers.util.* +import java.util.* + +@Deprecated("Too complex. Use AbstractMangaParser instead") +internal abstract class FlexibleMangaParser @InternalParsersApi constructor( + @property:InternalParsersApi val context: MangaLoaderContext, + final override val source: MangaParserSource, +) : MangaParser { + + override val config: MangaSourceConfig by lazy { context.getConfig(source) } + + open val sourceLocale: Locale + get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale) + + protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent()) + + final override val filterCapabilities: MangaListFilterCapabilities + get() = searchQueryCapabilities.toMangaListFilterCapabilities() + + protected val sourceContentRating: ContentRating? + get() = if (source.contentType == ContentType.HENTAI) { + ContentRating.ADULT + } else { + null + } + + final override val domain: String + get() = config[configKeyDomain] + + @Deprecated("Override intercept() instead") + override fun getRequestHeaders(): Headers = Headers.Builder() + .add(CommonHeaders.USER_AGENT, config[userAgentKey]) + .build() + + /** + * Used as fallback if value of `order` passed to [getList] is null + */ + open val defaultSortOrder: SortOrder + get() { + val supported = availableSortOrders + return SortOrder.entries.first { it in supported } + } + + protected open val webClient: WebClient = OkHttpWebClient(context.httpClient, source) + + /** + * Fetch direct link to the page image. + */ + override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain) + + final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + return getList(convertToMangaSearchQuery(offset, order, filter)) + } + + /** + * Parse favicons from the main page of the source`s website + */ + override suspend fun getFavicons(): Favicons { + return FaviconParser(webClient, domain).parseFavicons() + } + + @CallSuper + override fun onCreateConfig(keys: MutableCollection>) { + keys.add(configKeyDomain) + } + + override suspend fun getRelatedManga(seed: Manga): List { + return RelatedMangaFinder(listOf(this)).invoke(seed) + } + + /** + * Return [Manga] object by web link to it + * @see [Manga.publicUrl] + */ + override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null + + override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt new file mode 100644 index 000000000..369cb23f0 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/FlexiblePagedMangaParser.kt @@ -0,0 +1,63 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.core + +import androidx.annotation.VisibleForTesting +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.SearchableField +import org.koitharu.kotatsu.parsers.util.Paginator + +@Deprecated("Too complex. Use PagedMangaParser instead") +internal abstract class FlexiblePagedMangaParser( + context: MangaLoaderContext, + source: MangaParserSource, + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int, + searchPageSize: Int = pageSize, +) : FlexibleMangaParser(context, source) { + + @JvmField + protected val paginator: Paginator = Paginator(pageSize) + + @JvmField + protected val searchPaginator: Paginator = Paginator(searchPageSize) + + @Deprecated("Too complex. Use getList with filter instead") + final override suspend fun getList(query: MangaSearchQuery): List { + var containTitleNameCriteria = false + query.criteria.forEach { + if (it.field == SearchableField.TITLE_NAME) { + containTitleNameCriteria = true + } + } + + return searchManga( + paginator = if (containTitleNameCriteria) { + paginator + } else { + searchPaginator + }, + query = query, + ) + } + + public abstract suspend fun getListPage(query: MangaSearchQuery, page: Int): List + + protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) { + paginator.firstPage = firstPage + searchPaginator.firstPage = firstPageForSearch + } + + private suspend fun searchManga( + paginator: Paginator, + query: MangaSearchQuery, + ): List { + val offset: Int = query.offset + val page = paginator.getPage(offset) + val list = getListPage(query, page) + paginator.onListReceived(offset, page, list.size) + return list + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt new file mode 100644 index 000000000..39abc6536 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/MangaParserWrapper.kt @@ -0,0 +1,79 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.core + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.MangaParserAuthProvider +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.util.mergeWith + +internal class MangaParserWrapper( + private val delegate: MangaParser, +) : MangaParser by delegate { + + override val authorizationProvider: MangaParserAuthProvider? + get() = delegate as? MangaParserAuthProvider + + @Deprecated("Too complex. Use getList with filter instead") + override suspend fun getList(query: MangaSearchQuery): List = withContext(Dispatchers.Default) { + if (!query.skipValidation) { + searchQueryCapabilities.validate(query) + } + delegate.getList(query) + } + + override suspend fun getList( + offset: Int, + order: SortOrder, + filter: MangaListFilter, + ): List = withContext(Dispatchers.Default) { + delegate.getList(offset, order, filter) + } + + override suspend fun getDetails(manga: Manga): Manga = withContext(Dispatchers.Default) { + delegate.getDetails(manga) + } + + override suspend fun getPages(chapter: MangaChapter): List = withContext(Dispatchers.Default) { + delegate.getPages(chapter) + } + + override suspend fun getPageUrl(page: MangaPage): String = withContext(Dispatchers.Default) { + delegate.getPageUrl(page) + } + + override suspend fun getFilterOptions(): MangaListFilterOptions = withContext(Dispatchers.Default) { + delegate.getFilterOptions() + } + + override suspend fun getFavicons(): Favicons = withContext(Dispatchers.Default) { + delegate.getFavicons() + } + + override suspend fun getRelatedManga(seed: Manga): List = withContext(Dispatchers.Default) { + delegate.getRelatedManga(seed) + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val headers = request.headers.newBuilder() + .mergeWith(delegate.getRequestHeaders(), replaceExisting = false) + .build() + val newRequest = request.newBuilder().headers(headers).build() + return delegate.intercept(ProxyChain(chain, newRequest)) + } + + private class ProxyChain( + private val delegate: Interceptor.Chain, + private val request: Request, + ) : Interceptor.Chain by delegate { + + override fun request(): Request = request + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt new file mode 100644 index 000000000..d8744eae0 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/PagedMangaParser.kt @@ -0,0 +1,57 @@ +package org.koitharu.kotatsu.parsers.core + +import androidx.annotation.VisibleForTesting +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.Paginator + +@InternalParsersApi +public abstract class PagedMangaParser( + context: MangaLoaderContext, + source: MangaParserSource, + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int, + searchPageSize: Int = pageSize, +) : AbstractMangaParser(context, source) { + + @JvmField + protected val paginator: Paginator = Paginator(pageSize) + + @JvmField + protected val searchPaginator: Paginator = Paginator(searchPageSize) + + final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + return getList( + paginator = if (filter.query.isNullOrEmpty()) { + paginator + } else { + searchPaginator + }, + offset = offset, + order = order, + filter = filter, + ) + } + + public abstract suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List + + protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) { + paginator.firstPage = firstPage + searchPaginator.firstPage = firstPageForSearch + } + + private suspend fun getList( + paginator: Paginator, + offset: Int, + order: SortOrder, + filter: MangaListFilter, + ): List { + val page = paginator.getPage(offset) + val list = getListPage(page, order, filter) + paginator.onListReceived(offset, page, list.size) + return list + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt new file mode 100644 index 000000000..5e06b69e4 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/core/SinglePageMangaParser.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.parsers.core + +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.SortOrder + +@InternalParsersApi +public abstract class SinglePageMangaParser( + context: MangaLoaderContext, + source: MangaParserSource, +) : AbstractMangaParser(context, source) { + + final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { + if (offset > 0) { + return emptyList() + } + return getList(order, filter) + } + + public abstract suspend fun getList(order: SortOrder, filter: MangaListFilter): List +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt new file mode 100644 index 000000000..caa291b84 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/AuthRequiredException.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.parsers.exception + +import okio.IOException +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.model.MangaSource + +/** + * Authorization is required for access to the requested content + */ +public class AuthRequiredException @InternalParsersApi @JvmOverloads constructor( + public val source: MangaSource, + cause: Throwable? = null, +) : IOException("Authorization required", cause) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt new file mode 100644 index 000000000..65f47f693 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ContentUnavailableException.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.parsers.exception + +public class ContentUnavailableException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt new file mode 100644 index 000000000..c36da842f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/GraphQLException.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.parsers.exception + +import okio.IOException +import org.json.JSONArray +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull + +public class GraphQLException @InternalParsersApi constructor(errors: JSONArray) : IOException() { + + public val messages: List = errors.mapJSONNotNull { + it.getString("message") + } + + override val message: String + get() = messages.joinToString("\n") +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt new file mode 100644 index 000000000..fb1a03e2c --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/NotFoundException.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu.parsers.exception + +import org.jsoup.HttpStatusException +import java.net.HttpURLConnection + +public class NotFoundException( + message: String, + url: String, +) : HttpStatusException(message, HttpURLConnection.HTTP_NOT_FOUND, url) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt new file mode 100644 index 000000000..4ba7c53f8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/ParseException.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu.parsers.exception + +import org.koitharu.kotatsu.parsers.InternalParsersApi + +public class ParseException @InternalParsersApi @JvmOverloads constructor( + public val shortMessage: String?, + public val url: String, + cause: Throwable? = null, +) : RuntimeException("$shortMessage at $url", cause) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt new file mode 100644 index 000000000..cf35fe0ac --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/exception/TooManyRequestExceptions.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.parsers.exception + +import okio.IOException +import java.time.Instant +import java.time.temporal.ChronoUnit + +public class TooManyRequestExceptions( + public val url: String, + retryAfter: Long, +) : IOException("Too man requests") { + + public val retryAt: Instant? = if (retryAfter > 0 && retryAfter < Long.MAX_VALUE) { + Instant.now().plusMillis(retryAfter) + } else { + null + } + + public fun getRetryDelay(): Long { + if (retryAt == null) { + return -1L + } + return Instant.now().until(retryAt, ChronoUnit.MILLIS).coerceAtLeast(0L) + } + + override val message: String? + get() = if (retryAt != null) { + "${super.message}, retry at $retryAt" + } else { + super.message + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt new file mode 100644 index 000000000..a1ed22c82 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Constants.kt @@ -0,0 +1,11 @@ +@file:JvmName("Constants") + +package org.koitharu.kotatsu.parsers.model + +public const val RATING_UNKNOWN: Float = -1f + +public const val YEAR_UNKNOWN: Int = 0 + +public const val YEAR_MIN: Int = 1900 + +public const val YEAR_MAX: Int = 2099 diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt new file mode 100644 index 000000000..6658f1e8f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentRating.kt @@ -0,0 +1,7 @@ +package org.koitharu.kotatsu.parsers.model + +public enum class ContentRating { + SAFE, + SUGGESTIVE, + ADULT +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt new file mode 100644 index 000000000..2027fe7a1 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt @@ -0,0 +1,36 @@ +package org.koitharu.kotatsu.parsers.model + +public enum class ContentType { + + /** + * Standard manga, manhua, webtoons, etc + */ + MANGA, + + MANHWA, + + MANHUA, + + /** + * Use this if the source provides mostly nsfw content. + */ + HENTAI, + + /** + * Western comics + */ + COMICS, + + NOVEL, + + /** + * Use this type if no other suits your needs. For example, for an indie manga + */ + + ONE_SHOT, + DOUJINSHI, + IMAGE_SET, + ARTIST_CG, + GAME_CG, + OTHER, +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt new file mode 100644 index 000000000..6d32d8bbe --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.parsers.model + +public enum class Demographic { + SHOUNEN, + SHOUJO, + SEINEN, + JOSEI, + KODOMO, + NONE, +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt new file mode 100644 index 000000000..28d7c717e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicon.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.parsers.model + +import okhttp3.HttpUrl.Companion.toHttpUrl + +public data class Favicon( + @JvmField public val url: String, + @JvmField public val size: Int, + @JvmField internal val rel: String?, +) : Comparable { + + @JvmField + public val type: String = url.toHttpUrl().pathSegments.last() + .substringAfterLast('.', "").lowercase() + + override fun compareTo(other: Favicon): Int { + val res = size.compareTo(other.size) + if (res != 0) { + return res + } + return relWeightOf(rel).compareTo(relWeightOf(other.rel)) + } + + private fun relWeightOf(rel: String?) = when (rel) { + "apple-touch-icon" -> 1 // Prefer apple-touch-icon because it has a better quality + "mask-icon" -> -1 + else -> 0 + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt new file mode 100644 index 000000000..b11add359 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Favicons.kt @@ -0,0 +1,59 @@ +package org.koitharu.kotatsu.parsers.model + +public class Favicons( + favicons: Collection, + @JvmField public val referer: String?, +) : Collection { + + private val icons = favicons.sortedDescending() + + override val size: Int + get() = icons.size + + override fun contains(element: Favicon): Boolean = icons.contains(element) + + override fun containsAll(elements: Collection): Boolean = icons.containsAll(elements) + + override fun isEmpty(): Boolean = icons.isEmpty() + + override fun iterator(): Iterator = icons.iterator() + + public operator fun minus(victim: Favicon): Favicons = Favicons( + favicons = icons.filterNot { it == victim }, + referer = referer, + ) + + /** + * Finds a favicon whose size in pixels is greater than or equal to the specified size. + * If such icon is not available returns the largest icon + * @param size in pixels + * @param types supported file types, e.g. png, svg, ico. May be null but not empty + */ + @JvmOverloads + public fun find(size: Int, types: Set? = null): Favicon? { + if (icons.isEmpty()) { + return null + } + var result: Favicon? = null + for (icon in icons) { + if (types != null && icon.type !in types) { + continue + } + if (result == null || icon.size >= size) { + result = icon + } else { + break + } + } + return result + } + + public companion object { + + @JvmStatic + public val EMPTY: Favicons = Favicons(emptySet(), null) + + @JvmStatic + public fun single(url: String): Favicons = Favicons(setOf(Favicon(url, 0, null)), null) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt new file mode 100644 index 000000000..7ff1874da --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Manga.kt @@ -0,0 +1,203 @@ +package org.koitharu.kotatsu.parsers.model + +import androidx.collection.ArrayMap +import org.koitharu.kotatsu.parsers.util.findById +import org.koitharu.kotatsu.parsers.util.nullIfEmpty + +public data class Manga( + /** + * Unique identifier for manga + */ + @JvmField public val id: Long, + /** + * Manga title, human-readable + */ + @JvmField public val title: String, + /** + * Alternative titles (for example on other language), may be empty + */ + @JvmField public val altTitles: Set, + /** + * Relative url to manga (**without** a domain) or any other uri. + * Used principally in parsers + */ + @JvmField public val url: String, + /** + * Absolute url to manga, must be ready to open in browser + */ + @JvmField public val publicUrl: String, + /** + * Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown + * @see hasRating + */ + @JvmField public val rating: Float, + /** + * Indicates that manga may contain sensitive information (18+, NSFW) + */ + @JvmField public val contentRating: ContentRating?, + /** + * Absolute link to the cover + * @see largeCoverUrl + */ + @JvmField public val coverUrl: String?, + /** + * Tags (genres) of the manga + */ + @JvmField public val tags: Set, + /** + * Manga status (ongoing, finished) or null if unknown + */ + @JvmField public val state: MangaState?, + /** + * Authors of the manga + */ + @JvmField public val authors: Set, + /** + * Large cover url (absolute), null if is no large cover + * @see coverUrl + */ + @JvmField public val largeCoverUrl: String? = null, + /** + * Manga description, may be html or null + */ + @JvmField public val description: String? = null, + /** + * List of chapters + */ + @JvmField public val chapters: List? = null, + /** + * Manga source + */ + @JvmField public val source: MangaSource, +) { + + @Deprecated("Use other constructor") + public constructor( + /** + * Unique identifier for manga + */ + id: Long, + /** + * Manga title, human-readable + */ + title: String, + /** + * Alternative title (for example on other language), may be null + */ + altTitle: String?, + /** + * Relative url to manga (**without** a domain) or any other uri. + * Used principally in parsers + */ + url: String, + /** + * Absolute url to manga, must be ready to open in browser + */ + publicUrl: String, + /** + * Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown + * @see hasRating + */ + rating: Float, + /** + * Indicates that manga may contain sensitive information (18+, NSFW) + */ + isNsfw: Boolean, + /** + * Absolute link to the cover + * @see largeCoverUrl + */ + coverUrl: String?, + /** + * Tags (genres) of the manga + */ + tags: Set, + /** + * Manga status (ongoing, finished) or null if unknown + */ + state: MangaState?, + /** + * Authors of the manga + */ + author: String?, + /** + * Large cover url (absolute), null if is no large cover + * @see coverUrl + */ + largeCoverUrl: String? = null, + /** + * Manga description, may be html or null + */ + description: String? = null, + /** + * List of chapters + */ + chapters: List? = null, + /** + * Manga source + */ + source: MangaSource, + ) : this( + id = id, + title = title, + altTitles = setOfNotNull(altTitle?.nullIfEmpty()), + url = url, + publicUrl = publicUrl, + rating = rating, + contentRating = if (isNsfw) ContentRating.ADULT else null, + coverUrl = coverUrl?.nullIfEmpty(), + tags = tags, + state = state, + authors = setOfNotNull(author), + largeCoverUrl = largeCoverUrl?.nullIfEmpty(), + description = description?.nullIfEmpty(), + chapters = chapters, + source = source, + ) + + /** + * Author of the manga, may be null + */ + @Deprecated("Please use authors") + public val author: String? + get() = authors.firstOrNull() + + /** + * Alternative title (for example on other language), may be null + */ + @Deprecated("Please use altTitles") + public val altTitle: String? + get() = altTitles.firstOrNull() + + /** + * Return if manga has a specified rating + * @see rating + */ + public val hasRating: Boolean + get() = rating > 0f && rating <= 1f + + @Deprecated("Use contentRating instead", ReplaceWith("contentRating == ContentRating.ADULT")) + public val isNsfw: Boolean + get() = contentRating == ContentRating.ADULT + + public fun getChapters(branch: String?): List { + return chapters?.filter { x -> x.branch == branch }.orEmpty() + } + + public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id) + + public fun requireChapterById(id: Long): MangaChapter = findChapterById(id) + ?: throw NoSuchElementException("Chapter with id $id not found") + + public fun getBranches(): Map { + if (chapters.isNullOrEmpty()) { + return emptyMap() + } + val result = ArrayMap() + chapters.forEach { + val key = it.branch + result[key] = result.getOrDefault(key, 0) + 1 + } + return result + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt new file mode 100644 index 000000000..8790b8dae --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt @@ -0,0 +1,65 @@ +package org.koitharu.kotatsu.parsers.model + +import org.koitharu.kotatsu.parsers.util.formatSimple +import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty + +public data class MangaChapter( + /** + * An unique id of chapter + */ + @JvmField public val id: Long, + /** + * User-readable name of chapter if provided by parser or null instead + * Do not pass manga title or chapter number here + */ + @JvmField public val title: String?, + /** + * Chapter number starting from 1, 0 if unknown + */ + @JvmField public val number: Float, + /** + * Volume number starting from 1, 0 if unknown + */ + @JvmField public val volume: Int, + /** + * Relative url to chapter (**without** a domain) or any other uri. + * Used principally in parsers + */ + @JvmField public val url: String, + /** + * User-readable name of scanlator (releaser) or null if unknown + */ + @JvmField public val scanlator: String?, + /** + * Chapter upload date in milliseconds + */ + @JvmField public val uploadDate: Long, + /** + * User-readable name of branch. + * A branch is a group of chapters that overlap (e.g. different languages) + */ + @JvmField public val branch: String?, + @JvmField public val source: MangaSource, +) { + + @Deprecated("Use title instead", ReplaceWith("title")) + val name: String + get() = title.ifNullOrEmpty { + buildString { + if (volume > 0) append("Vol ").append(volume).append(' ') + if (number > 0) append("Chapter ").append(number.formatSimple()) else append("Unnamed") + } + } + + public fun numberString(): String? = if (number > 0f) { + number.formatSimple() + } else { + null + } + + public fun volumeString(): String? = if (volume > 0) { + volume.toString() + } else { + null + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt new file mode 100644 index 000000000..d643e5e27 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt @@ -0,0 +1,88 @@ +package org.koitharu.kotatsu.parsers.model + +import java.util.* + +public data class MangaListFilter( + @JvmField val query: String? = null, + @JvmField val tags: Set = emptySet(), + @JvmField val tagsExclude: Set = emptySet(), + @JvmField val locale: Locale? = null, + @JvmField val originalLocale: Locale? = null, + @JvmField val states: Set = emptySet(), + @JvmField val contentRating: Set = emptySet(), + @JvmField val types: Set = emptySet(), + @JvmField val demographics: Set = emptySet(), + @JvmField val year: Int = YEAR_UNKNOWN, + @JvmField val yearFrom: Int = YEAR_UNKNOWN, + @JvmField val yearTo: Int = YEAR_UNKNOWN, + @JvmField val author: String? = null, +) { + + private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() && + tagsExclude.isEmpty() && + locale == null && + originalLocale == null && + states.isEmpty() && + contentRating.isEmpty() && + year == YEAR_UNKNOWN && + yearFrom == YEAR_UNKNOWN && + yearTo == YEAR_UNKNOWN && + types.isEmpty() && + demographics.isEmpty() && + author.isNullOrEmpty() + + public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty() + + public fun isNotEmpty(): Boolean = !isEmpty() + + public fun hasNonSearchOptions(): Boolean = !isNonSearchOptionsEmpty() + + public companion object { + + @JvmStatic + public val EMPTY: MangaListFilter = MangaListFilter() + } + + internal class Builder { + private var query: String? = null + private val tags: MutableSet = mutableSetOf() + private val tagsExclude: MutableSet = mutableSetOf() + private var locale: Locale? = null + private var originalLocale: Locale? = null + private val states: MutableSet = mutableSetOf() + private val contentRating: MutableSet = mutableSetOf() + private val types: MutableSet = mutableSetOf() + private val demographics: MutableSet = mutableSetOf() + private var year: Int = YEAR_UNKNOWN + private var yearFrom: Int = YEAR_UNKNOWN + private var yearTo: Int = YEAR_UNKNOWN + + fun query(query: String?): Builder = apply { this.query = query } + fun addTag(tag: MangaTag): Builder = apply { tags.add(tag) } + fun addTags(tags: Collection): Builder = apply { this.tags.addAll(tags) } + fun excludeTag(tag: MangaTag): Builder = apply { tagsExclude.add(tag) } + fun excludeTags(tags: Collection): Builder = apply { this.tagsExclude.addAll(tags) } + fun locale(locale: Locale?): Builder = apply { this.locale = locale } + fun originalLocale(locale: Locale?): Builder = apply { this.originalLocale = locale } + fun addState(state: MangaState): Builder = apply { states.add(state) } + fun addStates(states: Collection): Builder = apply { this.states.addAll(states) } + fun addContentRating(rating: ContentRating): Builder = apply { contentRating.add(rating) } + fun addContentRatings(ratings: Collection): Builder = + apply { this.contentRating.addAll(ratings) } + + fun addType(type: ContentType): Builder = apply { types.add(type) } + fun addTypes(types: Collection): Builder = apply { this.types.addAll(types) } + fun addDemographic(demographic: Demographic): Builder = apply { demographics.add(demographic) } + fun addDemographics(demographics: Collection): Builder = + apply { this.demographics.addAll(demographics) } + + fun year(year: Int): Builder = apply { this.year = year } + fun yearFrom(year: Int): Builder = apply { this.yearFrom = year } + fun yearTo(year: Int): Builder = apply { this.yearTo = year } + + fun build(): MangaListFilter = MangaListFilter( + query, tags, tagsExclude, locale, originalLocale, states, + contentRating, types, demographics, year, yearFrom, yearTo, + ) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt new file mode 100644 index 000000000..d0daa06d3 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterCapabilities.kt @@ -0,0 +1,56 @@ +package org.koitharu.kotatsu.parsers.model + +import org.koitharu.kotatsu.parsers.InternalParsersApi + +public data class MangaListFilterCapabilities @InternalParsersApi constructor( + + /** + * Whether parser supports filtering by more than one tag + * @see [MangaListFilter.tags] + * @see [MangaListFilterOptions.availableTags] + */ + val isMultipleTagsSupported: Boolean = false, + + /** + * Whether parser supports tagsExclude field in filter + * @see [MangaListFilter.tagsExclude] + * @see [MangaListFilterOptions.availableTags] + */ + val isTagsExclusionSupported: Boolean = false, + + /** + * Whether parser supports searching by string query + * @see [MangaListFilter.query] + */ + val isSearchSupported: Boolean = false, + + /** + * Whether parser supports searching by string query combined within other filters + */ + val isSearchWithFiltersSupported: Boolean = false, + + /** + * Whether parser supports searching/filtering by year + * @see [MangaListFilter.year] + */ + val isYearSupported: Boolean = false, + + /** + * Whether parser supports searching by year range + * @see [MangaListFilter.yearFrom] and [MangaListFilter.yearTo] + */ + val isYearRangeSupported: Boolean = false, + + /** + * Whether parser supports searching Original Languages + * @see [MangaListFilter.originalLocale] + * @see [MangaListFilterOptions.availableLocales] + */ + val isOriginalLocaleSupported: Boolean = false, + + /** + * Whether parser supports searching by author name + * @see [MangaListFilter.author] + */ + val isAuthorSearchSupported: Boolean = false, +) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt new file mode 100644 index 000000000..6dbc69ee4 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterOptions.kt @@ -0,0 +1,45 @@ +package org.koitharu.kotatsu.parsers.model + +import org.koitharu.kotatsu.parsers.InternalParsersApi +import java.util.* + +public data class MangaListFilterOptions @InternalParsersApi constructor( + + /** + * Available tags (genres) + */ + public val availableTags: Set = emptySet(), + + /** + * Supported [MangaState] variants for filtering. May be empty. + * + * For better performance use [EnumSet] for more than one item. + */ + public val availableStates: Set = emptySet(), + + /** + * Supported [ContentRating] variants for filtering. May be empty. + * + * For better performance use [EnumSet] for more than one item. + */ + public val availableContentRating: Set = emptySet(), + + /** + * Supported [ContentType] variants for filtering. May be empty. + * + * For better performance use [EnumSet] for more than one item. + */ + public val availableContentTypes: Set = emptySet(), + + /** + * Supported [Demographic] variants for filtering. May be empty. + * + * For better performance use [EnumSet] for more than one item. + */ + public val availableDemographics: Set = emptySet(), + + /** + * Supported content locales for multilingual sources + */ + public val availableLocales: Set = emptySet(), +) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt new file mode 100644 index 000000000..259541cef --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaPage.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.parsers.model + +import org.koitharu.kotatsu.parsers.MangaParser + +public data class MangaPage( + /** + * Unique identifier for page + */ + @JvmField public val id: Long, + /** + * Relative url to page (**without** a domain) or any other uri. + * Used principally in parsers. + * May contain link to image or html page. + * @see MangaParser.getPageUrl + */ + @JvmField public val url: String, + /** + * Absolute url of the small page image if exists, null otherwise + */ + @JvmField public val preview: String?, + @JvmField public val source: MangaSource, +) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt new file mode 100644 index 000000000..cbed3dc1a --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaSource.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.parsers.model + +public interface MangaSource { + + public val name: String +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt new file mode 100644 index 000000000..710c2b404 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaState.kt @@ -0,0 +1,5 @@ +package org.koitharu.kotatsu.parsers.model + +public enum class MangaState { + ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING, RESTRICTED +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt new file mode 100644 index 000000000..69e969778 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaTag.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.parsers.model + +import org.koitharu.kotatsu.parsers.MangaParser + +public data class MangaTag( + /** + * User-readable tag title, should be in Title case + */ + @JvmField public val title: String, + /** + * Identifier of a tag, must be unique among the source. + * @see MangaParser.getList + */ + @JvmField public val key: String, + @JvmField public val source: MangaSource, +) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt new file mode 100644 index 000000000..6754405f5 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/SortOrder.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.parsers.model + +public enum class SortOrder { + UPDATED, + UPDATED_ASC, + POPULARITY, + POPULARITY_ASC, + RATING, + RATING_ASC, + NEWEST, + NEWEST_ASC, + ALPHABETICAL, + ALPHABETICAL_DESC, + ADDED, + ADDED_ASC, + RELEVANCE, + POPULARITY_HOUR, + POPULARITY_TODAY, + POPULARITY_WEEK, + POPULARITY_MONTH, + POPULARITY_YEAR, +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt new file mode 100644 index 000000000..4f5c7cd75 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/WordSet.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.parsers.model + +import org.koitharu.kotatsu.parsers.InternalParsersApi + +@InternalParsersApi +public class WordSet(private vararg val words: String) { + + public fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) } + public fun startsWith(dateString: String): Boolean = words.any { dateString.startsWith(it, ignoreCase = true) } + public fun endsWith(dateString: String): Boolean = words.any { dateString.endsWith(it, ignoreCase = true) } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt new file mode 100644 index 000000000..9b3ef638f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt @@ -0,0 +1,92 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.model.search + +import androidx.collection.ArrayMap +import androidx.collection.ArraySet +import org.koitharu.kotatsu.parsers.model.SortOrder + +/** + * Represents a search query for filtering and sorting manga search results. + * This class is immutable and must be constructed using the [Builder]. + * + * @property criteria The set of search criteria applied to the query. + * @property order The sorting order for the results (optional). + * @property offset The offset number for paginated search results (optional). + */ + +@Deprecated("Too complex. Use MangaListFilter instead") +@ConsistentCopyVisibility +public data class MangaSearchQuery private constructor( + @JvmField public val criteria: Set>, + @JvmField public val order: SortOrder?, + @JvmField public val offset: Int, + @JvmField public val skipValidation: Boolean, +) { + + public fun newBuilder(): Builder = Builder(this) + + public class Builder { + + private val criteria = ArraySet>() + private var order: SortOrder? = null + private var offset: Int = 0 + private var skipValidation: Boolean = false + + public constructor() + + public constructor(query: MangaSearchQuery) : this() { + criteria.addAll(query.criteria) + order = query.order + offset = query.offset + } + + public fun criterion(criterion: QueryCriteria<*>): Builder = apply { criteria.add(criterion) } + + public fun order(order: SortOrder?): Builder = apply { this.order = order } + + public fun offset(offset: Int): Builder = apply { this.offset = offset } + + public fun skipValidation(skip: Boolean): Builder = apply { this.skipValidation = skip } + + @Throws(IllegalArgumentException::class) + public fun build(): MangaSearchQuery { + return MangaSearchQuery(deduplicateCriteria(criteria), order, offset, skipValidation) + } + + private fun deduplicateCriteria(criteria: Set>): Set> { + val uniqueCriteria = + ArrayMap>>, QueryCriteria<*>>(criteria.size) + + for (criterion in criteria) { + val key = criterion.field to criterion::class.java + val existing = uniqueCriteria[key] + + when { + existing == null -> uniqueCriteria[key] = criterion + + existing is QueryCriteria.Include<*> && criterion is QueryCriteria.Include<*> -> { + uniqueCriteria[key] = + QueryCriteria.Include(criterion.field, existing.values union criterion.values) + } + + existing is QueryCriteria.Exclude<*> && criterion is QueryCriteria.Exclude<*> -> { + uniqueCriteria[key] = + QueryCriteria.Exclude(criterion.field, existing.values union criterion.values) + } + + else -> throw IllegalArgumentException( + "Match and Range have only one criterion per type, but found duplicates for: ${criterion.field} in ${criterion::class.simpleName}", + ) + } + } + + return uniqueCriteria.values.toSet() + } + } + + public companion object { + + public val EMPTY: MangaSearchQuery = MangaSearchQuery(emptySet(), null, 0, false) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt new file mode 100644 index 000000000..d2ba414f7 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt @@ -0,0 +1,50 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.model.search + +import androidx.collection.ArraySet +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.util.mapToSet + +@Deprecated("Too complex. Use MangaListFilterCapabilities instead") +@ExposedCopyVisibility +public data class MangaSearchQueryCapabilities internal constructor( + public val capabilities: Set, +) { + + public constructor(vararg capabilities: SearchCapability) : this(ArraySet(capabilities)) + + internal fun validate(query: MangaSearchQuery) { + val strictFields = capabilities.filter { it.isExclusive }.mapToSet { it.field } + val usedStrictFields = query.criteria.mapToSet { it.field }.intersect(strictFields) + + require(usedStrictFields.isEmpty() || query.criteria.size <= 1) { + "Query contains multiple criteria, but at least one field (${usedStrictFields.joinToString()}) does not support multiple criteria." + } + for (criterion in query.criteria) { + val capability = requireNotNull(capabilities.find { it.field == criterion.field }) { + "Unsupported search field: ${criterion.field}" + } + + require(criterion::class in capability.criteriaTypes) { + "Unsupported search criterion: ${criterion::class.simpleName} for field ${criterion.field}" + } + + // Ensure single value per criterion if supportMultiValue is false + if (!capability.isMultiple) { + when (criterion) { + is Include<*> -> require(criterion.values.size <= 1) { + "Multiple values are not allowed for field ${criterion.field}" + } + + is Exclude<*> -> require(criterion.values.size <= 1) { + "Multiple values are not allowed for field ${criterion.field}" + } + + is Range<*> -> Unit // Range is always valid (from, to) + is Match<*> -> Unit // Match always has a single value + } + } + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt new file mode 100644 index 000000000..342d93b73 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt @@ -0,0 +1,108 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.model.search + +/** + * Represents a generic search criterion used for filtering manga search results. + * Each criterion applies a specific condition to a [SearchableField] and operates on values of type [T]. + * + * @param T The type of value associated with the search criterion. + * @property field The field to which this search criterion applies. + */ +@Deprecated("Too complex") +public sealed interface QueryCriteria { + + public val field: SearchableField + + override fun equals(other: Any?): Boolean + + override fun hashCode(): Int + + /** + * Represents an inclusion criterion that allows search results based on a set of allowed values. + * + * @param T The type of value being included in the search. + * @property values The set of values that should be included in the search results. + * + * ### Example Usage: + * ```kotlin + * val genreFilter = QueryCriteria.Include(SearchableField.STATE, setOf(MangaState.ONGOING, MangaState.FINISHED)) + * ``` + */ + public data class Include( + public override val field: SearchableField, + @JvmField public val values: Set, + ) : QueryCriteria { + + init { + check(values.all { x -> field.type.isInstance(x) }) + } + } + + /** + * Represents an exclusion criterion that exclude results containing certain values. + * + * @param T The type of value being excluded from the search. + * @property values The set of values that should be excluded from the search results. + * + * ### Example Usage: + * ```kotlin + * val excludeTag = QueryCriteria.Exclude(SearchableField.TAG, setOf(MangaTag(key, title, source))) + * ``` + */ + public data class Exclude( + public override val field: SearchableField, + @JvmField public val values: Set, + ) : QueryCriteria { + + init { + check(values.all { x -> field.type.isInstance(x) }) + } + } + + /** + * Represents a range criterion that allows search based on a range of values. + * + * @param T The type of value used in the range (must be comparable). + * @property from The starting value of the range (inclusive). + * @property to The ending value of the range (inclusive). + * + * ### Example Usage: + * ```kotlin + * val yearRange = QueryCriteria.Range(SearchableField.PUBLICATION_YEAR, 2000, 2020) + * ``` + */ + public data class Range>( + public override val field: SearchableField, + @JvmField public val from: T, + @JvmField public val to: T, + ) : QueryCriteria { + + init { + check(field.type.isInstance(from)) + check(field.type.isInstance(to)) + } + } + + + /** + * Represents a match criterion that search results based on an exact match of a value. + * + * @param T The type of value being matched. + * @property value The exact value that must be matched. + * + * ### Example Usage: + * ```kotlin + * val titleMatch = QueryCriteria.Match(SearchableField.TITLE, "manga title") + * ``` + */ + public data class Match( + public override val field: SearchableField, + @JvmField public val value: T, + ) : QueryCriteria { + + init { + check(field.type.isInstance(value)) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt new file mode 100644 index 000000000..df697ab81 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchCapability.kt @@ -0,0 +1,36 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.model.search + +import kotlin.reflect.KClass + +/** + * Defines the search capabilities of a given field in the manga search query. + * + * @property field The searchable field that this capability applies to. + * Example values: + * - `SearchableField.TITLE_NAME` for searching by title. + * - `SearchableField.AUTHOR` for searching by author names. + * - `SearchableField.TAG` for filtering by tags. + * @property criteriaTypes The set of supported criteria types for the field. + * Example values: + * - `setOf(Include::class, Exclude::class)` selected field supports inclusion/exclusion criteria. + * - `setOf(Range::class)` selected field support numerical range criteria. + * @property isMultiValue Indicates whether the field supports multiple values. + * - `true` if multiple values can be provided (e.g., multiple tags or authors). + * - `false` if only a single value is allowed (e.g., only one tag or author). + * @property isExclusive Specifies whether the field can be used alongside other criteria. + * - `true` if this field can be used with other search criteria. + * - `false` if using this field requires it to be the only criterion in query. + */ +@Deprecated("Too complex") +public data class SearchCapability( + /** The searchable field that this capability applies to. */ + @JvmField public val field: SearchableField, + /** The set of supported criteria types for this field. */ + @JvmField public val criteriaTypes: Set>>, + /** Indicates whether the field supports multiple values. */ + @JvmField public val isMultiple: Boolean, + /** Specifies whether the field can be used alongside other criteria. */ + @JvmField public val isExclusive: Boolean = false, +) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt new file mode 100644 index 000000000..f29df1d4d --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.parsers.model.search + +import org.koitharu.kotatsu.parsers.model.* +import java.util.* + +/** + * Represents the various fields that can be used for searching manga. + * Each field is associated with a specific data type that defines its expected values. + * + * @property type The Java class representing the expected type of values for this field. + */ +@Deprecated("Too complex") +public enum class SearchableField(public val type: Class<*>) { + TITLE_NAME(String::class.java), + TAG(MangaTag::class.java), + AUTHOR(MangaTag::class.java), + LANGUAGE(Locale::class.java), + ORIGINAL_LANGUAGE(Locale::class.java), + STATE(MangaState::class.java), + CONTENT_TYPE(ContentType::class.java), + CONTENT_RATING(ContentRating::class.java), + DEMOGRAPHIC(Demographic::class.java), + PUBLICATION_YEAR(Int::class.javaObjectType); +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt new file mode 100644 index 000000000..123735977 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CloudFlareHelper.kt @@ -0,0 +1,58 @@ +package org.koitharu.kotatsu.parsers.network + +import okhttp3.CookieJar +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Response +import org.jsoup.Jsoup +import java.net.HttpURLConnection.HTTP_FORBIDDEN +import java.net.HttpURLConnection.HTTP_UNAVAILABLE + +public object CloudFlareHelper { + + public const val PROTECTION_NOT_DETECTED: Int = 0 + public const val PROTECTION_CAPTCHA: Int = 1 + public const val PROTECTION_BLOCKED: Int = 2 + + private const val CF_CLEARANCE = "cf_clearance" + + public fun checkResponseForProtection(response: Response): Int { + if (response.code != HTTP_FORBIDDEN && response.code != HTTP_UNAVAILABLE) { + return PROTECTION_NOT_DETECTED + } + val content = try { + response.peekBody(Long.MAX_VALUE).use { + Jsoup.parse(it.byteStream(), Charsets.UTF_8.name(), response.request.url.toString()) + } + } catch (_: IllegalStateException) { + return PROTECTION_NOT_DETECTED + } + return when { + content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null -> PROTECTION_BLOCKED + content.getElementById("challenge-error-title") != null || + content.getElementById("challenge-error-text") != null || + content.selectFirst("form#challenge-form") != null || + content.selectFirst("iframe[src*='challenges.cloudflare.com']") != null || + content.selectFirst("div#turnstile-wrapper") != null || + content.selectFirst("div.cf-turnstile") != null || + content.getElementById("cf-wrapper") != null || + content.title() == "Just a moment..." || + content.title() == "Attention Required! | Cloudflare" || + content.body().text().let { text -> + text.contains("Verify you are human") || + text.contains("needs to review the security of your connection") + } -> PROTECTION_CAPTCHA + else -> PROTECTION_NOT_DETECTED + } + } + + public fun getClearanceCookie(cookieJar: CookieJar, url: String): String? { + return cookieJar.loadForRequest(url.toHttpUrl()).find { it.name == CF_CLEARANCE }?.value + } + + public fun isCloudFlareCookie(name: String): Boolean { + return name.startsWith("cf_") + || name.startsWith("_cf") + || name.startsWith("__cf") + || name == "csrftoken" + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt new file mode 100644 index 000000000..66d17a2ce --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/CommonHeaders.kt @@ -0,0 +1,77 @@ +package org.koitharu.kotatsu.parsers.network + +internal object CommonHeaders { + + const val REFERER: String = "Referer" + const val USER_AGENT: String = "User-Agent" + const val ACCEPT: String = "Accept" + const val ACCEPT_LANGUAGE: String = "Accept-Language" + const val ACCEPT_CHARSET: String = "Accept-Charset" + const val ACCEPT_ENCODING: String = "Accept-Encoding" + + const val CONTENT_TYPE: String = "Content-Type" + const val CONTENT_LENGTH: String = "Content-Length" + const val CONTENT_DISPOSITION: String = "Content-Disposition" + const val CONTENT_ENCODING: String = "Content-Encoding" + const val CONTENT_LANGUAGE: String = "Content-Language" + + const val AUTHORIZATION: String = "Authorization" + const val PROXY_AUTHORIZATION: String = "Proxy-Authorization" + + const val COOKIE: String = "Cookie" + const val SET_COOKIE: String = "Set-Cookie" + + const val CACHE_CONTROL: String = "Cache-Control" + const val PRAGMA: String = "Pragma" + const val EXPIRES: String = "Expires" + + const val LAST_MODIFIED: String = "Last-Modified" + const val IF_MODIFIED_SINCE: String = "If-Modified-Since" + const val ETAG: String = "ETag" + const val IF_NONE_MATCH: String = "If-None-Match" + + const val LOCATION: String = "Location" + const val HOST: String = "Host" + const val ORIGIN: String = "Origin" + const val CONNECTION: String = "Connection" + + const val RANGE: String = "Range" + const val CONTENT_RANGE: String = "Content-Range" + + const val RETRY_AFTER: String = "Retry-After" + const val SERVER: String = "Server" + const val DATE: String = "Date" + + const val X_FORWARDED_FOR: String = "X-Forwarded-For" + const val X_REAL_IP: String = "X-Real-IP" + const val X_REQUESTED_WITH: String = "X-Requested-With" + + /* Fetch metadata headers */ + const val SEC_FETCH_DEST: String = "Sec-Fetch-Dest" + const val SEC_FETCH_MODE: String = "Sec-Fetch-Mode" + const val SEC_FETCH_SITE: String = "Sec-Fetch-Site" + const val SEC_FETCH_USER: String = "Sec-Fetch-User" + + /* Client hints headers */ + const val SEC_CH_UA: String = "Sec-Ch-Ua" + const val SEC_CH_UA_MOBILE: String = "Sec-Ch-Ua-Mobile" + const val SEC_CH_UA_PLATFORM: String = "Sec-Ch-Ua-Platform" + + /* Client API headers */ + const val X_CLIENT_TS: String = "x-client-ts" + const val X_CLIENT_SIG: String = "x-client-sig" + + /* API specific headers */ + const val X_APP_KEY: String = "x-app-key" + const val X_APP_ORIGIN: String = "x-app-origin" + const val X_WM_REQUEST_TIME: String = "x-wm-request-time" + const val X_WM_ACCESS_KEY: String = "x-wm-accses-key" + const val X_WM_REQUEST_SIGNATURE: String = "x-wm-request-signature" + + /* Other browser headers */ + const val UPGRADE_INSECURE_REQUESTS: String = "Upgrade-Insecure-Requests" + + /* Some custom headers */ + const val MANGA_SOURCE: String = "X-Manga-Source" + const val TOKEN: String = "Token" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt new file mode 100644 index 000000000..c641b2477 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/OkHttpWebClient.kt @@ -0,0 +1,133 @@ +package org.koitharu.kotatsu.parsers.network + +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import org.jsoup.HttpStatusException +import org.koitharu.kotatsu.parsers.exception.AuthRequiredException +import org.koitharu.kotatsu.parsers.exception.GraphQLException +import org.koitharu.kotatsu.parsers.exception.NotFoundException +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.await +import org.koitharu.kotatsu.parsers.util.parseJson +import java.net.HttpURLConnection + +public class OkHttpWebClient( + private val httpClient: OkHttpClient, + private val mangaSource: MangaSource, +) : WebClient { + + override suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response { + val request = Request.Builder() + .get() + .url(url) + .addTags() + .addExtraHeaders(extraHeaders) + return httpClient.newCall(request.build()).await().ensureSuccess() + } + + override suspend fun httpHead(url: HttpUrl): Response { + val request = Request.Builder() + .head() + .url(url) + .addTags() + return httpClient.newCall(request.build()).await().ensureSuccess() + } + + override suspend fun httpPost(url: HttpUrl, form: Map, extraHeaders: Headers?): Response { + val body = FormBody.Builder() + form.forEach { (k, v) -> + body.addEncoded(k, v) + } + val request = Request.Builder() + .post(body.build()) + .url(url) + .addTags() + .addExtraHeaders(extraHeaders) + return httpClient.newCall(request.build()).await().ensureSuccess() + } + + override suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response { + val body = FormBody.Builder() + payload.split('&').forEach { + val pos = it.indexOf('=') + if (pos != -1) { + val k = it.substring(0, pos) + val v = it.substring(pos + 1) + body.addEncoded(k, v) + } + } + val request = Request.Builder() + .post(body.build()) + .url(url) + .addTags() + .addExtraHeaders(extraHeaders) + return httpClient.newCall(request.build()).await().ensureSuccess() + } + + override suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response { + val mediaType = "application/json; charset=utf-8".toMediaType() + val requestBody = body.toString().toRequestBody(mediaType) + val request = Request.Builder() + .post(requestBody) + .url(url) + .addTags() + .addExtraHeaders(extraHeaders) + return httpClient.newCall(request.build()).await().ensureSuccess() + } + + override suspend fun graphQLQuery(endpoint: String, query: String): JSONObject { + val body = JSONObject() + body.put("operationName", null as Any?) + body.put("variables", JSONObject()) + body.put("query", "{$query}") + + val mediaType = "application/json; charset=utf-8".toMediaType() + val requestBody = body.toString().toRequestBody(mediaType) + val request = Request.Builder() + .post(requestBody) + .url(endpoint) + .addTags() + val json = httpClient.newCall(request.build()).await().parseJson() + json.optJSONArray("errors")?.let { + if (it.length() != 0) { + throw GraphQLException(it) + } + } + return json + } + + private fun Request.Builder.addTags(): Request.Builder { + tag(MangaSource::class.java, mangaSource) + return this + } + + private fun Request.Builder.addExtraHeaders(headers: Headers?): Request.Builder { + if (headers != null) { + headers(headers) + } + return this + } + + private fun Response.ensureSuccess(): Response { + val exception: Exception? = when (code) { // Catch some error codes, not all + HttpURLConnection.HTTP_NOT_FOUND -> NotFoundException(message, request.url.toString()) + HttpURLConnection.HTTP_UNAUTHORIZED -> request.tag(MangaSource::class.java)?.let { + AuthRequiredException(it) + } ?: HttpStatusException(message, code, request.url.toString()) + + in 400..599 -> HttpStatusException(message, code, request.url.toString()) + else -> null + } + if (exception != null) { + runCatching { + close() + }.onFailure { + exception.addSuppressed(it) + } + throw exception + } + return this + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt new file mode 100644 index 000000000..a5c2ed54e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitHelper.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.parsers.network + +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.parsers.network.utils.RateLimiter +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration + +public class RateLimitHelper( + private val permits: Int, + private val period: Duration, + private val shouldLimit: (HttpUrl) -> Boolean +) : Interceptor { + + private val limiters = ConcurrentHashMap() + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + if (!shouldLimit(request.url)) { + return chain.proceed(request) + } + + val host = request.url.host + val limiter = limiters.computeIfAbsent(host) { + RateLimiter(permits, period.inWholeMilliseconds) + } + + val timestamp = limiter.acquire( + call = chain.call(), + url = request.url.toString(), + ) + val response = chain.proceed(request) + + if (response.networkResponse == null) { + limiter.release(timestamp) + } + + return response + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt new file mode 100644 index 000000000..58cfa1da2 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/UserAgents.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.parsers.network + +public object UserAgents { + + public const val CHROME_MOBILE: String = + "Mozilla/5.0 (Linux; Android 16) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36" + + public const val FIREFOX_MOBILE: String = + "Mozilla/5.0 (Android 16; Mobile; LG-M255; rv:146.0) Gecko/146.0 Firefox/146.0" + + /* Fallback: Set Linux as default for "DESKTOP" User-Agent */ + + public const val CHROME_DESKTOP: String = + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.59 Safari/537.36" + + public const val FIREFOX_DESKTOP: String = "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0" + + public const val CHROME_WINDOWS: String = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" + + public const val FIREFOX_WINDOWS: String = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0" + + public const val KOTATSU: String = "Kotatsu/9.0 (Android 16;;; en)" + + public const val LEGACY_KOTATSU: String = "Kotatsu/6.8 (Android 13;;; en)" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt new file mode 100644 index 000000000..8b243714e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/WebClient.kt @@ -0,0 +1,117 @@ +package org.koitharu.kotatsu.parsers.network + +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Response +import org.json.JSONObject + +public interface WebClient { + + /** + * Do a GET http request to specific url + * @param url + */ + public suspend fun httpGet(url: String): Response = httpGet(url.toHttpUrl()) + + public suspend fun httpGet(url: String, extraHeaders: Headers?): Response = httpGet(url.toHttpUrl(), extraHeaders) + + /** + * Do a GET http request to specific url + * @param url + */ + public suspend fun httpGet(url: HttpUrl): Response = httpGet(url, null) + + /** + * Do a GET http request to specific url + * @param url + * @param extraHeaders additional HTTP headers for request + */ + public suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response + + /** + * Do a HEAD http request to specific url + * @param url + */ + public suspend fun httpHead(url: String): Response = httpHead(url.toHttpUrl()) + + /** + * Do a HEAD http request to specific url + * @param url + */ + public suspend fun httpHead(url: HttpUrl): Response + + /** + * Do a POST http request to specific url with `multipart/form-data` payload + * @param url + * @param form payload as key=>value map + */ + public suspend fun httpPost(url: String, form: Map): Response = + httpPost(url.toHttpUrl(), form, null) + + /** + * Do a POST http request to specific url with `multipart/form-data` payload + * @param url + * @param form payload as key=>value map + */ + public suspend fun httpPost(url: HttpUrl, form: Map): Response = httpPost(url, form, null) + + /** + * Do a POST http request to specific url with `multipart/form-data` payload + * @param url + * @param form payload as key=>value map + * @param extraHeaders additional HTTP headers for request + */ + public suspend fun httpPost(url: HttpUrl, form: Map, extraHeaders: Headers?): Response + + /** + * Do a POST http request to specific url with `multipart/form-data` payload + * @param url + * @param payload payload as `key=value` string with `&` separator + */ + public suspend fun httpPost(url: String, payload: String): Response = httpPost(url.toHttpUrl(), payload, null) + + /** + * Do a POST http request to specific url with `multipart/form-data` payload + * @param url + * @param payload payload as `key=value` string with `&` separator + */ + public suspend fun httpPost(url: HttpUrl, payload: String): Response = httpPost(url, payload, null) + + /** + * Do a POST http request to specific url with `multipart/form-data` payload + * @param url + * @param payload payload as `key=value` string with `&` separator + * @param extraHeaders additional HTTP headers for request + */ + public suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response + + /** + * Do a POST http request to specific url with json payload + * @param url + * @param body + */ + public suspend fun httpPost(url: String, body: JSONObject): Response = httpPost(url.toHttpUrl(), body, null) + + /** + * Do a POST http request to specific url with json payload + * @param url + * @param body + */ + public suspend fun httpPost(url: HttpUrl, body: JSONObject): Response = httpPost(url, body, null) + + /** + * Do a POST http request to specific url with json payload + * @param url + * @param body + * @param extraHeaders additional HTTP headers for request + */ + public suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response + + /** + * Do a GraphQL request to specific url + * @param endpoint an url + * @param query GraphQL request payload + */ + public suspend fun graphQLQuery(endpoint: String, query: String): JSONObject +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt new file mode 100644 index 000000000..0399df703 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/utils/RateLimiter.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.parsers.network.utils + +import okhttp3.Call +import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions +import java.io.IOException +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +public class RateLimiter( + private val permits: Int, + private val periodMs: Long +) { + private val timestamps = ArrayDeque(permits) + private val lock = ReentrantLock(true) // fair lock + + public fun acquire(call: Call, url: String = ""): Long = lock.withLock { + val now = System.currentTimeMillis() + + while (timestamps.isNotEmpty() && now - timestamps.first() >= periodMs) { + timestamps.removeFirst() + } + + if (timestamps.size >= permits) { + if (call.isCanceled()) throw IOException("Canceled") + val oldestRequest = timestamps.first() + val waitTime = periodMs - (now - oldestRequest) + + if (waitTime > 0) { + throw TooManyRequestExceptions(url, waitTime) + } + + val current = System.currentTimeMillis() + while (timestamps.isNotEmpty() && current - timestamps.first() >= periodMs) { + timestamps.removeFirst() + } + } + + val timestamp = System.currentTimeMillis() + timestamps.addLast(timestamp) + return timestamp + } + + public fun release(timestamp: Long): Boolean = lock.withLock { + timestamps.remove(timestamp) + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt index 923057ac7..92b07651d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComicKLive.kt @@ -76,15 +76,14 @@ internal class ComicKLive(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder() .host(domain) .addPathSegment("api") .addPathSegment("search") .addQueryParameter("type", "comic") - query?.let { query -> - url.addQueryParameter("q", query) + filter.query?.let { + url.addQueryParameter("q", filter.query) } filter.tags.forEach { @@ -155,7 +154,7 @@ internal class ComicKLive(context: MangaLoaderContext) : } // no "page" parameter, use "cursor" to prevent empty list when searching with keyword - if (query.isNullOrEmpty() && page > 1) { + if (filter.query.isNullOrEmpty() && page > 1) { url.addQueryParameter("cursor", pageCursor) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt index a91e9f84e..b8a143681 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt @@ -301,8 +301,7 @@ internal class ExHentaiParser( override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) - val contentLength = response.headersContentLength() - if (contentLength in 0L..BANNED_RESPONSE_LENGTH) { + if (response.headersContentLength(BANNED_RESPONSE_LENGTH) <= BANNED_RESPONSE_LENGTH) { val text = response.peekBody(BANNED_RESPONSE_LENGTH).use { it.string() } if (text.contains("IP address has been temporarily banned", ignoreCase = true)) { val hours = Regex("([0-9]+) hours?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0 @@ -433,7 +432,6 @@ internal class ExHentaiParser( return null } val joiner = StringUtil.StringJoiner(" ") - val query = query if (!query.isNullOrEmpty()) { joiner.add(query) } @@ -458,7 +456,6 @@ internal class ExHentaiParser( joiner.append(lc.toLanguagePath()) joiner.append("\"$") } - val author = author if (!author.isNullOrEmpty()) { joiner.add("artist:\"") joiner.append(author) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt index 7f336f6ef..5807f9508 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt @@ -125,10 +125,8 @@ internal class HitomiLaParser(context: MangaLoaderContext) : AbstractMangaParser private var cachedSearchIds: List = emptyList() - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - return when { - query.isNullOrEmpty() -> { + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List = when { + filter.query.isNullOrEmpty() -> { if (filter.tags.isEmpty()) { when (order) { @@ -187,12 +185,11 @@ internal class HitomiLaParser(context: MangaLoaderContext) : AbstractMangaParser else -> { if (offset == 0) { - cachedSearchIds = hitomiSearch(query, order).toList() + cachedSearchIds = hitomiSearch(filter.query, order).toList() } cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size)) } }.toMangaList() - } private fun Int.nextOffsetRange(): LongRange { val bytes = this * 4L diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt index ffe658eb3..df890bdb8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ImHentai.kt @@ -59,16 +59,15 @@ internal class ImHentai(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/search/?page=") append(page.toString()) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("&key=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt index 73e50fdcd..8c8ad3368 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Koharu.kt @@ -80,8 +80,6 @@ internal class Koharu(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val baseUrl = "https://$apiSuffix/books" val url = buildString { append(baseUrl) @@ -90,8 +88,8 @@ internal class Koharu(context: MangaLoaderContext) : val includedTags: MutableList = mutableListOf() val excludedTags: MutableList = mutableListOf() - if (!query.isNullOrEmpty() && query.startsWith("id:")) { - val ipk = query.removePrefix("id:") + if (!filter.query.isNullOrEmpty() && filter.query.startsWith("id:")) { + val ipk = filter.query.removePrefix("id:") val response = webClient.httpGet("$baseUrl/detail/$ipk").parseJson() return listOf(parseMangaDetail(response)) } @@ -107,18 +105,18 @@ internal class Koharu(context: MangaLoaderContext) : } append("?sort=").append(sortValue) - if (!query.isNullOrEmpty()) { - terms.add("title:\"${query.urlEncoded()}\"") + if (!filter.query.isNullOrEmpty()) { + terms.add("title:\"${filter.query.urlEncoded()}\"") } - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { val authors = authorsIds.getOrDefault(emptyMap()) - val authorId = authors[author.lowercase()] + val authorId = authors[filter.author.lowercase()] if (authorId != null) { includedTags.add(authorId) } else { - terms.add("artist:\"${author.urlEncoded()}\"") + terms.add("artist:\"${filter.author.urlEncoded()}\"") } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt index 10a2944d9..2922d7594 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/LineWebtoonsParser.kt @@ -145,10 +145,9 @@ internal abstract class LineWebtoonsParser( } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val manga = when { - !query.isNullOrEmpty() -> { - makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20") + !filter.query.isNullOrEmpty() -> { + makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20") .getJSONObject("challengeSearch") .getJSONArray("titleList") .mapJSON { jo -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt index f23706bd8..03e93b6ab 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt @@ -176,7 +176,6 @@ internal abstract class MangaFireParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("/filter") @@ -189,12 +188,12 @@ internal abstract class MangaFireParser( append(siteLang) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { // Keyword + vrf append("&keyword=") - append(encodeKeyword(query)) + append(encodeKeyword(filter.query)) append("&vrf=") - append(VrfGenerator.generate(query)) + append(VrfGenerator.generate(filter.query)) append("&sort=") append(when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt index 1d85350b0..d4abb2175 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPark.kt @@ -110,15 +110,14 @@ internal class MangaPark(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/search?page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&word=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } append("&genres=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt index 60b9aa26b..407440a2b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt @@ -62,9 +62,8 @@ internal abstract class MangaPlusParser( private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString()) override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - query.isNullOrEmpty() -> { + filter.query.isNullOrEmpty() -> { when (order) { SortOrder.POPULARITY -> getPopularList() SortOrder.UPDATED -> getLatestList() @@ -72,7 +71,7 @@ internal abstract class MangaPlusParser( } } - else -> getAllTitleList(query) + else -> getAllTitleList(filter.query) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt index da0900a81..c906953bd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt @@ -88,12 +88,11 @@ internal class MangaReaderToParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = "https://$domain".toHttpUrl().newBuilder().apply { when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { addPathSegment("search") - addQueryParameter("keyword", query) + addQueryParameter("keyword", filter.query) addQueryParameter("page", page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt index df7f9c1d5..780077e04 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Manhwa210.kt @@ -37,17 +37,16 @@ internal class Manhwa210(context: MangaLoaderContext) : PagedMangaParser(context ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search") append("?filter[name]=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=") @@ -94,7 +93,7 @@ internal class Manhwa210(context: MangaLoaderContext) : PagedMangaParser(context } } - if (query.isNullOrEmpty()) { + if (filter.query.isNullOrEmpty()) { append("&sort=") when (order) { SortOrder.POPULARITY -> append("-views") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt index 5175b7a7f..3ff234ba0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Misskon.kt @@ -29,15 +29,14 @@ internal class Misskon(context: MangaLoaderContext) : PagedMangaParser(context, override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/page/$page/") append("?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } order == SortOrder.POPULARITY -> { append("/top3/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt index 11f708a2a..a252137fb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/Multporn.kt @@ -52,14 +52,13 @@ internal class Multporn(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?search_api_views_fulltext=") - val encodedQuery = query.splitByWhitespace().joinToString(separator = "+") { part -> + val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part -> part.urlEncoded() } append(encodedQuery) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt index 163f5aee6..2ea1bd94f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MyReadingManga.kt @@ -151,7 +151,6 @@ internal class MyReadingManga(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -172,8 +171,8 @@ internal class MyReadingManga(context: MangaLoaderContext) : // fix order append("&s=") - if (!query.isNullOrEmpty()) { - append(query.splitByWhitespace().joinToString(separator = "+")) + if (!filter.query.isNullOrEmpty()) { + append(filter.query.splitByWhitespace().joinToString(separator = "+")) } val langSlug = getLanguageSlug(filter.locale) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt index 9804ce7b7..98016c77c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineMangaParser.kt @@ -68,19 +68,18 @@ internal abstract class NineMangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) - if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty() || !query.isNullOrEmpty()) { + if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty() || !filter.query.isNullOrEmpty()) { append("/search/") append("?page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&name_sel=contain&wd=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } append("&category_id=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt index 2f92feeb2..4a846af76 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/NineNineNineHentaiParser.kt @@ -113,15 +113,14 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - return if (query.isNullOrEmpty()) { + return if (filter.query.isNullOrEmpty()) { if (filter.tags.isEmpty() && order == SortOrder.POPULARITY) { getPopularList(page, filter.locale) } else { getSearchList(page, null, filter.locale, filter.tags, order) } } else { - getSearchList(page, query, null, null, order) + getSearchList(page, filter.query, null, null, order) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt index cc4b7e8cd..a8cee699a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt @@ -181,10 +181,9 @@ internal abstract class WebtoonsParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val document = when { - !query.isNullOrEmpty() -> { - val searchUrl = "https://$domain/$languageCode/search?keyword=${query.urlEncoded()}" + !filter.query.isNullOrEmpty() -> { + val searchUrl = "https://$domain/$languageCode/search?keyword=${filter.query.urlEncoded()}" webClient.httpGet(searchUrl).parseHtml() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt index 108ecbc46..f6f19736e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WeebDex.kt @@ -157,8 +157,6 @@ internal class WeebDex(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = urlBuilder() .host("api.$domain") .addPathSegment("manga") @@ -197,8 +195,8 @@ internal class WeebDex(context: MangaLoaderContext) : } // Keyword - if (!query.isNullOrEmpty()) { - url.addQueryParameter("title", query.urlEncoded()) + if (!filter.query.isNullOrEmpty()) { + url.addQueryParameter("title", filter.query.urlEncoded()) } // Content rating @@ -286,8 +284,8 @@ internal class WeebDex(context: MangaLoaderContext) : } // Author + Artist search - if (!author.isNullOrBlank()) { - url.addQueryParameter("authorOrArtist", author.substringAfter("-").trim()) + if (!filter.author.isNullOrBlank()) { + url.addQueryParameter("authorOrArtist", filter.author.substringAfter("-").trim()) } val js = webClient.httpGet(url.build()).parseJson() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt index 390bddaf9..7a3bed07d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/AnimeBootstrapParser.kt @@ -56,7 +56,6 @@ internal abstract class AnimeBootstrapParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -65,9 +64,9 @@ internal abstract class AnimeBootstrapParser( append(page.toString()) append("&type=all") - query?.let { query -> + filter.query?.let { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt index 5eccd6a81..9496dab80 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/animebootstrap/fr/PapScan.kt @@ -29,7 +29,6 @@ internal class PapScan(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -37,9 +36,9 @@ internal class PapScan(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("&alpha=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt index 13603b03c..491443b6c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/AzoraMoon.kt @@ -55,12 +55,11 @@ internal class AzoraMoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val all = allPostsCache.get() var filtered: List = all - if (!query.isNullOrEmpty()) { - val q = query.lowercase(sourceLocale) + if (!filter.query.isNullOrEmpty()) { + val q = filter.query.lowercase(sourceLocale) filtered = filtered.filter { p -> p.getStringOrNull("postTitle")?.lowercase(sourceLocale)?.contains(q) == true || p.getStringOrNull("alternativeTitles")?.lowercase(sourceLocale)?.contains(q) == true || diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt index 612752790..68b0a16f1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/FlixScans.kt @@ -48,15 +48,14 @@ internal class FlixScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val json = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } val url = "https://api.$domain/api/v1/search/serie" val body = JSONObject() - body.put("title", query.urlEncoded()) + body.put("title", filter.query.urlEncoded()) webClient.httpPost(url, body).parseJson().getJSONArray("data") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt index 80d82a598..df6711751 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/HentaMan.kt @@ -77,12 +77,11 @@ internal class HentaMan(context: MangaLoaderContext) : PagedMangaParser( private val baseUrl get() = "https://$domain" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val searchUrl = "$baseUrl/manga/search".toHttpUrl().newBuilder() - .addQueryParameter("query", query) + .addQueryParameter("query", filter.query) .build() val response = webClient.httpGet(searchUrl).parseJson() val results = response.optJSONArray("results") ?: return emptyList() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt index 9167055fb..9548e36d9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/MangaStorm.kt @@ -33,16 +33,15 @@ internal class MangaStorm(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/mangas?page=") append(page) append("&query=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt index 9eb2b7585..ff672dedd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ar/TeamXNovel.kt @@ -48,14 +48,13 @@ internal class TeamXNovel(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt index b3bc9d354..dc214df01 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt @@ -43,11 +43,10 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val filters = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { return if (offset == 0) { - search(query) + search(filter.query) } else { emptyList() } @@ -90,7 +89,7 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c id = generateUid(mediaId), title = title.getString("be"), coverUrl = jo.getString("poster").removePrefix("/cdn") - .toAbsoluteUrl(getDomain(this, "cdn")) + "?width=200&height=280", + .toAbsoluteUrl(getDomain("cdn")) + "?width=200&height=280", altTitles = setOfNotNull(title.optJSONArray("alt")?.optString(0)?.nullIfEmpty()), authors = emptySet(), contentRating = null, @@ -129,7 +128,7 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c """.trimIndent(), ).getJSONObject("media") val title = details.getJSONObject("title") - val poster = details.getString("poster").removePrefix("/cdn").toAbsoluteUrl(getDomain(this, "cdn")) + val poster = details.getString("poster").removePrefix("/cdn").toAbsoluteUrl(getDomain("cdn")) val chapters = apiCall( """ chapters(mediaId: "${details.getString("mediaId")}", offset: 0, limit: 99999) { @@ -237,7 +236,7 @@ internal class AnibelParser(context: MangaLoaderContext) : AbstractMangaParser(c id = generateUid(mediaId), title = title.getString("be"), coverUrl = jo.getString("poster").removePrefix("/cdn") - .toAbsoluteUrl(getDomain(this, "cdn")) + "?width=200&height=280", + .toAbsoluteUrl(getDomain("cdn")) + "?width=200&height=280", altTitles = setOfNotNull(title.getString("en").nullIfEmpty()), authors = emptySet(), contentRating = null, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt index 0d7c1a7b0..6e6040877 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/cupfox/CupFoxParser.kt @@ -38,14 +38,13 @@ internal abstract class CupFoxParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append('/') append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt index 973cae657..14541e235 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/AsuraScansParser.kt @@ -64,17 +64,15 @@ internal class AsuraScansParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://") append(domain) append("/browse?page=") append(page) - query?.let { query -> + filter.query?.let { append("&q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (filter.tags.isNotEmpty()) { @@ -107,9 +105,9 @@ internal class AsuraScansParser(context: MangaLoaderContext) : ) } - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { append("&author=") - append(author.urlEncoded()) + append(filter.author.urlEncoded()) } append( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt index bed1e5094..888a11f3e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Atsumaru.kt @@ -30,10 +30,9 @@ internal class Atsumaru(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query // Case 1: Search (Uses a different API endpoint) - if (!query.isNullOrEmpty()) { - return getSearchPage(page, query) + if (!filter.query.isNullOrEmpty()) { + return getSearchPage(page, filter.query) } // Case 2: Popular / Latest (Uses the infinite API) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt index 00eb0231b..72bc1dc38 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BatCave.kt @@ -47,11 +47,10 @@ internal class BatCave(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val urlBuilder = StringBuilder() when { - !query.isNullOrEmpty() -> { - val encodedQuery = query.splitByWhitespace().joinToString(separator = "%20") { part -> + !filter.query.isNullOrEmpty() -> { + val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "%20") { part -> part.urlEncoded() } urlBuilder.append("/search/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt index 81727b6ee..79e2b07d8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/BeeToon.kt @@ -32,17 +32,16 @@ internal class BeeToon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt index b824d454f..8efda3622 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt @@ -39,15 +39,14 @@ internal class ComicExtra(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/") when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("search?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=") append(page.toString()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt index 060082bc5..8a9ca468c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DemonicScans.kt @@ -35,13 +35,12 @@ internal class DemonicScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val hasQuery = !query.isNullOrEmpty() + val hasQuery = !filter.query.isNullOrEmpty() val isNewest = order == SortOrder.NEWEST && !hasQuery && filter.tags.isEmpty() val isAlpha = order == SortOrder.ALPHABETICAL && !hasQuery && filter.tags.isEmpty() val isAlphaDesc = order == SortOrder.ALPHABETICAL_DESC && !hasQuery && filter.tags.isEmpty() val url = when { - hasQuery -> "https://$domain/search.php?manga=" + (query ?: "") + hasQuery -> "https://$domain/search.php?manga=" + (filter.query ?: "") isNewest -> "https://$domain/lastupdates.php?list=$page" isAlpha -> buildString { append("https://$domain/advanced.php") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt index ff800e901..d9cad6813 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/DynastyScans.kt @@ -42,14 +42,13 @@ internal class DynastyScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&") append("classes[]=Series&page=") append(page.toString()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt index 44f5a0ff8..2aaa3ed42 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FanFoxParser.kt @@ -44,14 +44,13 @@ internal class FanFoxParser(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?name=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt index 5907bdacd..b59c7e495 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlameComics.kt @@ -37,7 +37,6 @@ internal class FlameComics(context: MangaLoaderContext) : override val configKeyDomain = ConfigKey.Domain("flamecomics.xyz") override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder() .addPathSegment("_next") .addPathSegment("data") @@ -52,8 +51,8 @@ internal class FlameComics(context: MangaLoaderContext) : } // Filter by search if provided - val filteredManga = if (!query.isNullOrEmpty()) { - val normalizedQuery = removeSpecialCharsRegex.replace(query.lowercase(), "") + val filteredManga = if (!filter.query.isNullOrEmpty()) { + val normalizedQuery = removeSpecialCharsRegex.replace(filter.query.lowercase(), "") allManga.filter { manga -> val titles = mutableListOf(manga.title) titles.addAll(manga.altTitles) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt index 06f8598e0..ebaa729d8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/FlixScansOrg.kt @@ -48,9 +48,8 @@ internal class FlixScansOrg(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val json = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt index 5b2c2bb74..752232913 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Hentalk.kt @@ -60,19 +60,17 @@ internal class Hentalk(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() // not found any URLs for it override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://") append(domain) append("/__data.json?x-sveltekit-trailing-slash=1&x-sveltekit-invalidated=001") when { - !query.isNullOrEmpty() || filter.tags.isNotEmpty() || !author.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || !filter.author.isNullOrEmpty() -> { append("&q=") - if (!author.isNullOrEmpty()) { - append("artist:\"${space2plus(author)}\"") + if (!filter.author.isNullOrEmpty()) { + append("artist:\"${space2plus(filter.author)}\"") append('+') } @@ -83,8 +81,8 @@ internal class Hentalk(context: MangaLoaderContext) : } } - if (!query.isNullOrEmpty()) { - append(space2plus(query)) + if (!filter.query.isNullOrEmpty()) { + append(space2plus(filter.query)) } else { append('+') } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt index 0edfad20c..0f85c2a30 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/HeyToonParser.kt @@ -65,12 +65,11 @@ internal class HeyToonParser(context: MangaLoaderContext) : private val headers = Headers.headersOf("X-Requested-With", "XMLHttpRequest") override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) return emptyList() - val url = "https://$domain/api/complete-search?keyword=${query.urlEncoded()}" + val url = "https://$domain/api/complete-search?keyword=${filter.query.urlEncoded()}" val response = webClient.httpGet(url, headers).parseJsonArray() response.mapJSON { comic -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt index 242cd82cf..21896c626 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/KdtScans.kt @@ -76,12 +76,11 @@ internal class KdtScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://$domain/manga/?page=${page}") - query?.let { q -> - append("&s=${q.urlEncoded()}") + filter.query?.let { + append("&s=${it.urlEncoded()}") } val sortValue = when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt index fff414093..7c48e05a2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaGeko.kt @@ -37,8 +37,7 @@ internal class MangaGeko(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val isSearch = !query.isNullOrEmpty() + val isSearch = !filter.query.isNullOrEmpty() val url = buildString { append("https://") append(domain) @@ -48,7 +47,7 @@ internal class MangaGeko(context: MangaLoaderContext) : return emptyList() } append("/search/?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt index 16eff95aa..dd2f28693 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaHomeParser.kt @@ -41,14 +41,13 @@ internal class MangaHomeParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?name=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt index e0a66404a..ef517b99b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKatana.kt @@ -41,14 +41,13 @@ internal class MangaKatana(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = when { - !query.isNullOrEmpty() -> buildString { + !filter.query.isNullOrEmpty() -> buildString { append("https://") append(domain) if (page > 1) append("/page/").append(page) append("/?search=1&q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> buildString { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt index 0d7c01858..38b694f9c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaKawaiiEn.kt @@ -39,14 +39,13 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?query=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&search_type=manga&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt index 301dffd5c..74f978881 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaPill.kt @@ -41,9 +41,8 @@ internal class MangaPill(context: MangaLoaderContext) : PagedMangaParser(context ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query // bruh - val url = if (filter.types.isNotEmpty() || filter.states.isNotEmpty() || !query.isNullOrEmpty() || filter.tags.isNotEmpty()) { + val url = if (filter.types.isNotEmpty() || filter.states.isNotEmpty() || !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty()) { buildString { append("/search") append("?type=") @@ -67,9 +66,9 @@ internal class MangaPill(context: MangaLoaderContext) : PagedMangaParser(context } } ?: "") - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt index 2b0aa9edd..4e82318dd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt @@ -42,14 +42,13 @@ internal class MangaTownParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?name=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } @@ -261,7 +260,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : } private suspend fun bypassLicensedChapters(manga: Manga): List { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain(this, "m"))).parseHtml() + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain("m"))).parseHtml() val list = doc.body().selectFirst("ul.detail-ch-list") ?: return emptyList() val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US) return list.select("li").asReversed().mapIndexedNotNull { i, li -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt index 2b95f5fe6..9d9ed76a4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangago.kt @@ -41,7 +41,6 @@ internal class MangaGo(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val sortParam = when (order) { SortOrder.UPDATED -> "s=1" SortOrder.POPULARITY -> "s=9" @@ -50,8 +49,8 @@ internal class MangaGo(context: MangaLoaderContext) : else -> "s=1" } - val url = if (!query.isNullOrEmpty()) { - "https://$domain/r/l_search/?name=${query.urlEncoded()}&page=$page" + val url = if (!filter.query.isNullOrEmpty()) { + "https://$domain/r/l_search/?name=${filter.query.urlEncoded()}&page=$page" } else { val genre = filter.tags.firstOrNull()?.key ?: "All" "https://$domain/genre/$genre/$page/?$sortParam" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt index 14cb37510..38c56a34a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Mangaowl.kt @@ -51,14 +51,13 @@ internal class Mangaowl(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/10-search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt index 91efcb146..41a3356f5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Com.kt @@ -57,16 +57,15 @@ internal class Manhwa18Com(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/tim-kiem?page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } append("&accept_genres=") @@ -109,10 +108,10 @@ internal class Manhwa18Com(context: MangaLoaderContext) : } // Support author - // author.let{ + // filter.author.let{ // the // append("&artist=") - // append(author) + // append(filter.author) // } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt index b32004d07..0e5c89f0c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manhwa18Parser.kt @@ -87,16 +87,15 @@ internal class Manhwa18Parser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/tim-kiem?page=") append(page.toString()) - query?.let { q -> + filter.query?.let { append("&q=") - append(q.urlEncoded()) + append(it.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt index 1442c8aa1..944f67120 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ManhwasMen.kt @@ -39,7 +39,6 @@ internal class ManhwasMen(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -47,9 +46,9 @@ internal class ManhwasMen(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt index a0829fc9c..cdd6c3ba9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MyComicList.kt @@ -37,14 +37,13 @@ internal class MyComicList(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/comic-search?key=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } filter.tags.isNotEmpty() -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt index 727c4e081..d39740dbb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Po2Scans.kt @@ -31,14 +31,13 @@ internal class Po2Scans(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/series") - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } } val doc = webClient.httpGet(url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt index ebccdfa08..6136d3c47 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Pururin.kt @@ -42,16 +42,15 @@ internal class Pururin(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/search?tag_condition=contains&page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt index b2a2a971c..5a481b9c0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt @@ -52,10 +52,9 @@ internal class RoliaScan(context: MangaLoaderContext) : PagedMangaParser(context ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val body = JSONObject().apply { put("page", page) - put("search", query.orEmpty()) + put("search", filter.query.orEmpty()) put("years", "[]") put("genres", buildJsonArrayString(filter.tags.map { it.key })) put( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt index 6895055b9..08922dd62 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VioletScans.kt @@ -36,9 +36,8 @@ internal class VioletScans(context: MangaLoaderContext) : override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - return if (!query.isNullOrEmpty()) { - scrapeSearchList(query, page) + return if (!filter.query.isNullOrEmpty()) { + scrapeSearchList(filter.query, page) } else { scrapeNonSearchList(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt index 6e80645f5..7307997dd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/VyManga.kt @@ -46,20 +46,19 @@ internal class VyManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/search?search_po=0&q=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(filter.query.urlEncoded()) } append("&author_po=0&author=") - // author?.let { author -> - // append(author.urlEncoded()) + // filter.author?.let { + // append(filter.author.urlEncoded()) // } append("&completed=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt index a18a2e8bd..e7b14db89 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt @@ -131,16 +131,15 @@ internal class WeebCentral(context: MangaLoaderContext) : AbstractMangaParser(co } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = "https://$domain/search/data".toHttpUrl().newBuilder().apply { addQueryParameter("limit", "32") addQueryParameter("offset", offset.toString()) - query?.let { q -> - val normalized = q + filter.query?.let { + val query = it .replace(Regex("""[^a-zA-Z0-9\s]"""), " ") .replace(Regex("""\s+"""), " ") .trim() - addQueryParameter("text", normalized) + addQueryParameter("text", query) } addQueryParameter( name = "sort", diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt index 641fbecc5..351d11171 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/comix/Comix.kt @@ -58,10 +58,9 @@ internal class Comix(context: MangaLoaderContext) : // List / Search // ------------------------- override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val builder = "$apiBaseUrl/manga".toHttpUrl().newBuilder().apply { - if (!query.isNullOrBlank()) { - addQueryParameter("keyword", query) + if (!filter.query.isNullOrBlank()) { + addQueryParameter("keyword", filter.query) } val (param, dir) = when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt index c25397e8d..595ed3812 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/HouseMangasParser.kt @@ -50,7 +50,6 @@ internal class HouseMangasParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt index ee32dde90..967e8697e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/InMangaParser.kt @@ -105,9 +105,8 @@ internal class InMangaParser(context: MangaLoaderContext) : PagedMangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val genres = filter.tags.map { it.key }.toSet() - val formData = buildFormData(page, order, query ?: "", genres) + val formData = buildFormData(page, order, filter.query ?: "", genres) val response = webClient.httpPost( "https://$domain/manga/getMangasConsultResult".toHttpUrl(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt index dea224550..a26c3f7fc 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt @@ -69,16 +69,15 @@ internal class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaPars ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/library?_pg=1&page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&title=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } append("&order_item=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt index ffd5e5aae..05bd57148 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/ZonaTmoParser.kt @@ -82,7 +82,6 @@ internal class ZonaTmoParser(context: MangaLoaderContext) : PagedMangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append(apiUrl) append("/listing/manga?page=").append(page) @@ -99,7 +98,7 @@ internal class ZonaTmoParser(context: MangaLoaderContext) : PagedMangaParser( ) append("&order=").append(if (order == SortOrder.ALPHABETICAL) "asc" else "desc") - query?.takeIf { it.isNotEmpty() }?.let { + filter.query?.takeIf { it.isNotEmpty() }?.let { append("&search=").append(it.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt index 3165ff82f..4fcad1433 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fmreader/FmreaderParser.kt @@ -82,8 +82,6 @@ internal abstract class FmreaderParser( protected open val tagPrefix = "manga-list-genre-" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://") append(domain) @@ -91,14 +89,14 @@ internal abstract class FmreaderParser( append("?page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&name=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - // author?.let { author -> + // filter.author?.let { // append("&author=") - // append(author.urlEncoded()) + // append(filter.author.urlEncoded()) // } append("&genre=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt index 91ccbc05d..54827a864 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/FoolSlideParser.kt @@ -45,9 +45,8 @@ internal abstract class FoolSlideParser( override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val doc = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } @@ -59,7 +58,7 @@ internal abstract class FoolSlideParser( append(searchUrl) } - webClient.httpPost(url, "search=${query.urlEncoded()}").parseHtml() + webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml() } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt index bdee74582..e4bf51430 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AnimeSama.kt @@ -65,11 +65,10 @@ internal class AnimeSama(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = "$baseUrl/catalogue".toHttpUrl().newBuilder() // keyword - val encodedQuery = query?.splitByWhitespace()?.joinToString(separator = "+") { part -> + val encodedQuery = filter.query?.splitByWhitespace()?.joinToString(separator = "+") { part -> part.urlEncoded() }.orEmpty() url.addQueryParameter("search", encodedQuery) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt index ac4165e9b..b6324bf07 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/AstralManga.kt @@ -138,8 +138,6 @@ internal class AstralManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val all = getFilteredAndSortedList(order, filter) val from = ((page - 1) * pageSize).coerceAtLeast(0) if (from >= all.size) return emptyList() @@ -224,10 +222,9 @@ internal class AstralManga(context: MangaLoaderContext) : } continue } - val coverUrl = manga.coverUrl - if (isCoverUrlFresh(coverUrl, now)) { - if (slug.isNotBlank() && !coverUrl.isNullOrBlank()) { - putCoverCache(slug, coverUrl, now) + if (isCoverUrlFresh(manga.coverUrl, now)) { + if (slug.isNotBlank() && !manga.coverUrl.isNullOrBlank()) { + putCoverCache(slug, manga.coverUrl, now) } continue } @@ -633,15 +630,11 @@ internal class AstralManga(context: MangaLoaderContext) : tags = (first.manga.tags + second.manga.tags).toSet(), state = first.manga.state ?: second.manga.state, authors = (first.manga.authors + second.manga.authors).toSet(), - description = run { - val d1 = first.manga.description - val d2 = second.manga.description - when { - d1.isNullOrBlank() -> d2 - d2.isNullOrBlank() -> d1 - d1.length >= d2.length -> d1 - else -> d2 - } + description = when { + first.manga.description.isNullOrBlank() -> second.manga.description + second.manga.description.isNullOrBlank() -> first.manga.description + first.manga.description.length >= second.manga.description.length -> first.manga.description + else -> second.manga.description }, ) return MangaCache( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt index 6bcfd2cd0..5d99b2bf2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BentomangaParser.kt @@ -56,14 +56,13 @@ internal class BentomangaParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder() .host(domain) .addPathSegment("manga_list") .addQueryParameter("limit", page.toString()) - query?.let { query -> - url.addQueryParameter("search", query) + filter.query?.let { + url.addQueryParameter("search", filter.query) } when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt index f949bf861..89082290b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/BigSolo.kt @@ -77,18 +77,16 @@ internal class BigSolo(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val catalog = getCatalog() - val normalizedQuery = normalizeSearchText(query) + val query = normalizeSearchText(filter.query) val includeTags = filter.tags.mapTo(hashSetOf()) { normalizeTagKey(it.key) } val excludeTags = filter.tagsExclude.mapTo(hashSetOf()) { normalizeTagKey(it.key) } val stateFilter = filter.states val typesFilter = filter.types val demographicFilter = filter.demographics - val authorFilter = normalizeSearchText(author) + val authorFilter = normalizeSearchText(filter.author) val filtered = catalog.entries.filter { entry -> - if (normalizedQuery.isNotEmpty() && normalizedQuery !in entry.searchText) { + if (query.isNotEmpty() && query !in entry.searchText) { return@filter false } if (authorFilter.isNotEmpty() && authorFilter !in entry.authorsSearchText) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt index d5a456faa..9296d7cc2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/FuryoSociety.kt @@ -44,12 +44,11 @@ internal class FuryoSociety(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt index 408739f8c..6df551b18 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/InovaScanManga.kt @@ -80,10 +80,9 @@ internal class InovaScanManga(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildApiUrl( - search = query.orEmpty(), + search = filter.query.orEmpty(), sort = when (order) { SortOrder.POPULARITY -> "popular" SortOrder.NEWEST -> "new" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt index c7e4a637c..d79b47609 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LegacyScansParser.kt @@ -45,18 +45,17 @@ internal class LegacyScansParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val end = page * pageSize val start = end - (pageSize - 1) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } val url = buildString { append("https://api.$domain/misc/home/search?title=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } return parseMangaListQuery(webClient.httpGet(url).parseJson()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt index d5b21d574..68dda205a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LireScan.kt @@ -33,13 +33,12 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context, } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val doc = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } - val q = query.urlEncoded().replace("%20", "+") + val q = filter.query.urlEncoded().replace("%20", "+") val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q" webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt index 89420199b..42f41a23d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt @@ -61,9 +61,8 @@ internal class LugnicaScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt index 13398e676..0db768097 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaKawaii.kt @@ -39,14 +39,13 @@ internal class MangaKawaii(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?query=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&search_type=manga&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt index 209457bb4..b79a3638f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMana.kt @@ -55,17 +55,16 @@ internal class MangaMana(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val postData = buildString { append("page=") append(page) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } val domainCdn = "cdn" + domain.removePrefix("www") - val url = "https://$domain/search-live?q=${query.urlEncoded()}" + val url = "https://$domain/search-live?q=${filter.query.urlEncoded()}" val json = webClient.httpGet(url).parseJsonArray() return json.mapJSON { jo -> val slug = jo.getString("slug") ?: throw ParseException("Missing Slug", url) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt index 320679044..01f1eb02c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt @@ -63,7 +63,6 @@ internal class MangaMoins(context: MangaLoaderContext) : override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = HttpUrl.Builder() .scheme("https") .host(domain) @@ -71,7 +70,7 @@ internal class MangaMoins(context: MangaLoaderContext) : .addQueryParameter("page", page.toString()) .addQueryParameter("limit", API_LIMIT.toString()) .apply { - query?.trim()?.takeIf { it.isNotEmpty() }?.let { q -> + filter.query?.trim()?.takeIf { it.isNotEmpty() }?.let { q -> addQueryParameter("q", q) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt index d8111c2c7..b5862cfaa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/OrtegaScans.kt @@ -149,7 +149,6 @@ internal class OrtegaScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val all = getFilteredAndSortedList(order, filter) val from = ((page - 1) * pageSize).coerceAtLeast(0) if (from >= all.size) return emptyList() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt index a869bdbd3..c76bf76e4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PhenixscansParser.kt @@ -74,19 +74,18 @@ internal class PhenixscansParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/api/front/manga") when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } append("/search?query=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt index 94b8e7b78..7b3c0828e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PoseidonScans.kt @@ -143,8 +143,6 @@ internal class PoseidonScans(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val all = getFilteredAndSortedList(order, filter) val fromIndex = ((page - 1) * pageSize).coerceAtLeast(0) if (fromIndex >= all.size) { @@ -538,10 +536,9 @@ internal class PoseidonScans(context: MangaLoaderContext) : } override suspend fun getDetails(manga: Manga): Manga { - val chapters = manga.chapters - if (!chapters.isNullOrEmpty()) { + if (!manga.chapters.isNullOrEmpty()) { return manga.copy( - chapters = normalizeChapterOrder(chapters), + chapters = normalizeChapterOrder(manga.chapters), ) } @@ -899,7 +896,7 @@ internal class PoseidonScans(context: MangaLoaderContext) : if (dateString.isNullOrBlank()) return 0L val cleanedDateString = - dateString.removePrefix("\"").removeSuffix("\"").removePrefix("D").trim() + dateString.removePrefix("\"").removeSuffix("\"").removePrefix($$"$D").removePrefix("D").trim() return isoDateFormat.parseSafe(cleanedDateString) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt index b19d777e8..f2df36b3c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/PunkRecordz.kt @@ -103,10 +103,8 @@ internal class PunkRecordz(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val filtered = filterManga(getCatalogManga(), filter) - val sorted = sortManga(filtered, order, query) + val sorted = sortManga(filtered, order, filter.query) val fromIndex = ((page - 1) * pageSize).coerceAtLeast(0) if (fromIndex >= sorted.size) { return emptyList() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt index c1d376e50..148146060 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScansMangasMe.kt @@ -44,14 +44,13 @@ internal class ScansMangasMe(context: MangaLoaderContext) : } override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt index 7ddda9aee..d7a46d2ed 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/ScantradUnion.kt @@ -39,16 +39,15 @@ internal class ScantradUnion(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt index 7cb1d6ce5..5d0853367 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt @@ -91,7 +91,6 @@ internal abstract class FuzzyDoodleParser( protected open val comicsValue = "bande-dessinee" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -111,9 +110,9 @@ internal abstract class FuzzyDoodleParser( ) } - query?.let { query -> + filter.query?.let { append("&title=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } append("&status=") filter.states.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt index 82244f167..a55fb9ce7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gallery/GalleryParser.kt @@ -33,10 +33,9 @@ internal abstract class GalleryParser( override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder().apply { when { - !query.isNullOrEmpty() -> addQueryParameter("search", query) + !filter.query.isNullOrEmpty() -> addQueryParameter("search", filter.query) filter.tags.isNotEmpty() -> addPathSegments(filter.tags.first().key) order == SortOrder.POPULARITY -> addPathSegment("hot") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt index 53f80ad69..e83d997a7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/GalleryAdultsParser.kt @@ -60,14 +60,13 @@ internal abstract class GalleryAdultsParser( order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&") } @@ -81,9 +80,9 @@ internal abstract class GalleryAdultsParser( append("/tag/") append(tag.key) append("/?") - } else if (lang != null) { + } else if (filter.locale != null) { append("/language/") - append(lang.toLanguagePath()) + append(filter.locale.toLanguagePath()) append("/?") } else { append("/?") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt index a2bcdb3d4..4a5254cef 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/Hentai3.kt @@ -44,40 +44,37 @@ internal class Hentai3(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } else -> { - val tags = filter.tags - val lang = filter.locale - if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { + if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { append("/search?q=") - append(buildQuery(tags, lang)) + append(buildQuery(filter.tags, filter.locale)) if (order == SortOrder.POPULARITY) { append("&sort=popular") } append("&page=") append(page.toString()) - } else if (lang != null) { + } else if (filter.locale != null) { append("/language/") - append(lang.toLanguagePath()) + append(filter.locale.toLanguagePath()) append("/") append(page.toString()) if (order == SortOrder.POPULARITY) { append("?sort=popular") } - } else if (tags.isNotEmpty()) { - tags.oneOrThrowIfMany()?.let { + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { append("/tags/") append(it.key) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt index 16d5940e1..c45bb4a0e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEnvy.kt @@ -38,25 +38,22 @@ internal class HentaiEnvy(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/?s_key=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&") } else -> { - val tags = filter.tags - val lang = filter.locale - if (tags.isNotEmpty()) { - if (lang != null) { + if (filter.tags.isNotEmpty()) { + if (filter.locale != null) { throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED) } - tags.oneOrThrowIfMany()?.let { + filter.tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) if (order == SortOrder.POPULARITY) { @@ -64,9 +61,9 @@ internal class HentaiEnvy(context: MangaLoaderContext) : } append("/?") } - } else if (lang != null) { + } else if (filter.locale != null) { append("/language/") - append(lang.toLanguagePath()) + append(filter.locale.toLanguagePath()) append("/?") } else { append("/?") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt index 52c28c81f..1f503034c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiEra.kt @@ -46,34 +46,31 @@ internal class HentaiEra(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/?key=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&") } else -> { - val tags = filter.tags - val lang = filter.locale - if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { + if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { append("/search/?key=") if (order == SortOrder.POPULARITY) { append( - buildQuery(tags, lang) + buildQuery(filter.tags, filter.locale) .replace("<=1&dl=0&pp=0&tr=0", "<=0&dl=0&pp=1&tr=0"), ) } else { - append(buildQuery(tags, lang)) + append(buildQuery(filter.tags, filter.locale)) } append("&") - } else if (tags.isNotEmpty()) { - tags.oneOrThrowIfMany()?.let { + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) } @@ -83,9 +80,9 @@ internal class HentaiEra(context: MangaLoaderContext) : append("popular/") } append("?") - } else if (lang != null) { + } else if (filter.locale != null) { append("/language/") - append(lang.toLanguagePath()) + append(filter.locale.toLanguagePath()) append("/") if (order == SortOrder.POPULARITY) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt index ad65267b1..c48a65848 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiForce.kt @@ -54,29 +54,26 @@ internal class HentaiForce(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") } else -> { - val tags = filter.tags - val lang = filter.locale - if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { + if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { append("/search?q=") - append(buildQuery(tags, lang)) + append(buildQuery(filter.tags, filter.locale)) if (order == SortOrder.POPULARITY) { append("&sort=popular") } append("&page=") - } else if (tags.isNotEmpty()) { - tags.oneOrThrowIfMany()?.let { + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) } @@ -86,9 +83,9 @@ internal class HentaiForce(context: MangaLoaderContext) : append("popular/") } append("?") - } else if (lang != null) { + } else if (filter.locale != null) { append("/language/") - append(lang.toLanguagePath()) + append(filter.locale.toLanguagePath()) append("/") if (order == SortOrder.POPULARITY) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt index 5b47d5fa1..60d5a62ac 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/HentaiFox.kt @@ -26,14 +26,13 @@ internal class HentaiFox(context: MangaLoaderContext) : override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=") append(page.toString()) @@ -41,11 +40,9 @@ internal class HentaiFox(context: MangaLoaderContext) : } else -> { - val tags = filter.tags - val lang = filter.locale - if (tags.size > 1 || (tags.isNotEmpty() && lang != null)) { + if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { append("/search/?q=") - append(buildQuery(tags, lang)) + append(buildQuery(filter.tags, filter.locale)) if (page > 1) { append("&page=") append(page.toString()) @@ -54,8 +51,8 @@ internal class HentaiFox(context: MangaLoaderContext) : if (order == SortOrder.POPULARITY) { append("&sort=popular") } - } else if (tags.isNotEmpty()) { - tags.oneOrThrowIfMany()?.let { + } else if (filter.tags.isNotEmpty()) { + filter.tags.oneOrThrowIfMany()?.let { append("/tag/") append(it.key) } @@ -69,9 +66,9 @@ internal class HentaiFox(context: MangaLoaderContext) : append(page.toString()) append("/") } - } else if (lang != null) { + } else if (filter.locale != null) { append("/language/") - append(lang.toLanguagePath()) + append(filter.locale.toLanguagePath()) append("/") if (order == SortOrder.POPULARITY) { append("popular/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt index de06e2c08..387b472f1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiParser.kt @@ -44,10 +44,9 @@ internal class NHentaiParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder().addPathSegments(apiSuffix) val isDefaultHome = order == SortOrder.UPDATED - && query.isNullOrEmpty() + && filter.query.isNullOrEmpty() && filter.tags.isEmpty() && filter.locale == null @@ -56,7 +55,7 @@ internal class NHentaiParser(context: MangaLoaderContext) : url.addQueryParameter("page", page.toString()) } else { val query = buildString { - query?.trim()?.takeIf { it.isNotEmpty() }?.let { + filter.query?.trim()?.takeIf { it.isNotEmpty() }?.let { append(it) append(" ") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt index 9dd58364f..4514a4392 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiToParser.kt @@ -47,7 +47,6 @@ internal class NHentaiToParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -56,9 +55,9 @@ internal class NHentaiToParser(context: MangaLoaderContext) : append("/go/?") } - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&") } @@ -72,7 +71,7 @@ internal class NHentaiToParser(context: MangaLoaderContext) : append("/tag/") append(tag.key) append("/?") - } else if (lang != null) { + } else if (filter.locale != null) { append("/language/") append(lang.toLanguagePath()) append("/?") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt index 6cd2c9fb2..f63fddf55 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/all/NHentaiXxxParser.kt @@ -44,7 +44,6 @@ internal class NHentaiXxxParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -70,8 +69,8 @@ internal class NHentaiXxxParser(context: MangaLoaderContext) : joiner.add(tag.title) } - if (!query.isNullOrEmpty()) { - joiner.add(query.urlEncoded()) + if (!filter.query.isNullOrEmpty()) { + joiner.add(filter.query.urlEncoded()) } append(joiner.complete()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt index 45364a8b1..9329471bf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiNexus.kt @@ -85,21 +85,19 @@ internal class HentaiNexus(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://$domain/page/$page") when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { val queries = mutableListOf() - if (!author.isNullOrEmpty()) { - queries.add("artist:${author}") + if (!filter.author.isNullOrEmpty()) { + queries.add("artist:${filter.author}") } filter.tags.map { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt index 4c1ca4d69..c992273ef 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/galleryadults/en/HentaiRead.kt @@ -159,8 +159,6 @@ internal class HentaiRead(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query - val author = filter.author // Query structure: // ?s=& // title-type=contains& /* ?s= query types: contain, start-with, end-with */ @@ -180,12 +178,12 @@ internal class HentaiRead(context: MangaLoaderContext) : append("https://$domain") // ANY filter (including single tag) uses search format - val isSearch = query != null || + val isSearch = filter.query != null || filter.tags.isNotEmpty() || filter.types.isNotEmpty() || filter.year != YEAR_UNKNOWN || filter.tagsExclude.isNotEmpty() || - !author.isNullOrEmpty() + !filter.author.isNullOrEmpty() when { // Search with any filters @@ -198,7 +196,7 @@ internal class HentaiRead(context: MangaLoaderContext) : val queries = mutableListOf() // Search query (can be empty) - queries.add("s=${query?.trim()?.urlEncoded() ?: ""}") + queries.add("s=${filter.query?.trim()?.urlEncoded() ?: ""}") queries.add("title-type=contains") queries.add("search-mode=AND") queries.add("release-type=in") @@ -224,8 +222,8 @@ internal class HentaiRead(context: MangaLoaderContext) : } // Author (using artist ID for search) - if (!author.isNullOrEmpty()) { - val authorId = getAuthorId(author) + if (!filter.author.isNullOrEmpty()) { + val authorId = getAuthorId(filter.author) if (authorId != null) { queries.add("artists[]=$authorId") } @@ -401,15 +399,14 @@ internal class HentaiRead(context: MangaLoaderContext) : } override suspend fun getPageUrl(page: MangaPage): String { - val preview = page.preview - if (preview.isNullOrEmpty()) { + if (page.preview.isNullOrEmpty()) { throw Exception("It should be not null. Something wrong!") } // preview page url: https://hencover.xyz/preview/${mangaId}/${chapterId}/hr_${index.padLeft(4)}.jpg // page url: https://henread.xyz/${mangaId}/${chapterId}/hr_${index.padLeft(4)}.jpg val index = page.url.split("/").last() - val t = preview.split("/") + val t = page.preview.split("/") val mangaId = t[4] val chapterId = t[5] diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt index 69b27fbce..9f5cedefa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/gattsu/GattsuParser.kt @@ -37,17 +37,16 @@ internal abstract class GattsuParser( protected open val tagPrefix = "tag" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt index ae26659ee..57b7b736f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/guya/GuyaParser.kt @@ -34,14 +34,13 @@ internal abstract class GuyaParser( override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/api/get_all_series/") } - return if (!query.isNullOrEmpty()) { - parseMangaList(webClient.httpGet(url).parseJson(), query) + return if (!filter.query.isNullOrEmpty()) { + parseMangaList(webClient.httpGet(url).parseJson(), filter.query) } else { parseMangaList(webClient.httpGet(url).parseJson(), "") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt index cd811ecb0..47b848830 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt @@ -50,19 +50,18 @@ internal abstract class HeanCms( protected open val pathManga = "series" protected open val apiPath - get() = getDomain(this, "api") + get() = getDomain("api") protected open val paramsUpdated = "latest" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(apiPath) append("/query?query_string=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(filter.query.urlEncoded()) } append("&series_type=Comic&perPage=$pageSize") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt index e244513b6..172d9d576 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/TempleScan.kt @@ -56,9 +56,8 @@ internal class TempleScan(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - return if (!query.isNullOrEmpty()) { - getSearchList(query, page) + return if (!filter.query.isNullOrEmpty()) { + getSearchList(filter.query, page) } else { getBrowseList(page, order) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt index 35af4cc45..76a04ca0b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/fr/PerfScan.kt @@ -61,11 +61,10 @@ internal class PerfScan(context: MangaLoaderContext) : override val cdn = "https://$apiPath/cdn/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://$apiPath/series?page=$page&take=$pageSize&type=COMIC") - query?.let { query -> append("&title=").append(query.urlEncoded()) } + filter.query?.let { append("&title=").append(it.urlEncoded()) } if (filter.states.isNotEmpty()) { val statusMapping = fetchStatusMap() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt index c012bbdd0..3394796e1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancmsalt/HeanCmsAlt.kt @@ -45,12 +45,11 @@ internal abstract class HeanCmsAlt( override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append(listUrl) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt index 4b05e04b8..4dc8c588d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt @@ -52,7 +52,6 @@ internal abstract class HotComicsParser( protected open val onePage = false override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query if (onePage && page > 1) { return emptyList() } @@ -62,9 +61,9 @@ internal abstract class HotComicsParser( append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt index 56c88fb7b..0e994f814 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/DoujinDesuParser.kt @@ -51,16 +51,15 @@ internal class DoujinDesuParser(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val author = filter.author val url = urlBuilder().apply { if (filter.tags.isNotEmpty()) { addPathSegment("genre") filter.tags.oneOrThrowIfMany()?.key?.let { it -> addPathSegment(it.lowercase().splitByWhitespace().joinToString("-") { it }) } - } else if (!author.isNullOrEmpty()) { + } else if (!filter.author.isNullOrEmpty()) { addPathSegment("author") - addPathSegment(author.splitByWhitespace().joinToString("-") { it.lowercase() }) + addPathSegment(filter.author.splitByWhitespace().joinToString("-") { it.lowercase() }) } else { addPathSegment("manga") } @@ -71,7 +70,7 @@ internal class DoujinDesuParser(context: MangaLoaderContext) : } if (!filter.states.isEmpty() && - author.isNullOrEmpty() && + filter.author.isNullOrEmpty() && filter.tags.isEmpty() ) { filter.states.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt index 4bb297fa3..2ec8f5634 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt @@ -33,16 +33,15 @@ internal class HentaiCrot(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/page/") append(page) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt index ef400ba9d..099262e9d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Komikcast.kt @@ -86,13 +86,12 @@ internal class Komikcast(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder().host(apiUrl).apply { addPathSegment("series") // Query with keyword, testing... - if (!query.isNullOrEmpty()) { - val keyword = query.encodeKeyword() + if (!filter.query.isNullOrEmpty()) { + val keyword = filter.query.encodeKeyword() val filterValue = "title=like=\"$keyword\",nativeTitle=like=\"$keyword\"" addEncodedQueryParameter("filter", filterValue) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt index 0f0f25456..cb4429076 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Kumapage.kt @@ -42,15 +42,14 @@ internal class Kumapage(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val genre = if (filter.tags.isNotEmpty()) filter.tags.first().key else "all" val url = "https://$domain/search-process/" val headers = Headers.Builder() .add(CommonHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") .build() - val response = webClient.httpPost(url.toHttpUrl(), payload = "view=1&keyword=${query}&genre=$genre", headers).parseHtml() + val response = webClient.httpPost(url.toHttpUrl(), payload = "view=1&keyword=${filter.query}&genre=$genre", headers).parseHtml() parseSearchList(response) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt index 01b91c23f..8d3bb33b9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt @@ -33,16 +33,15 @@ internal class PixHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/page/") append(page) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt index 62e86edaa..dea872bd9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt @@ -64,8 +64,6 @@ internal class Shinigami(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://") append(apiSuffix) @@ -130,14 +128,14 @@ internal class Shinigami(context: MangaLoaderContext) : append("&genre_exclude_mode=and") } - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { append("&author=") - append(author.lowercase().replace(' ', '-').urlEncoded()) + append(filter.author.lowercase().replace(' ', '-').urlEncoded()) } - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&q=") - val encodedQuery = query.splitByWhitespace() + val encodedQuery = filter.query.splitByWhitespace() .joinToString(separator = "+") { part -> part.urlEncoded() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt index 7f849651f..68e81a2c1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/ThemangaParser.kt @@ -82,9 +82,8 @@ internal class TheManga(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> searchManga(query) + !filter.query.isNullOrEmpty() -> searchManga(filter.query) filter.tags.isNotEmpty() -> getMangaListByGenre(filter.tags.first().key, page, filter.tags) else -> getAllManga(page, order) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt index bb6ec69a4..306fe2319 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/WestmangaParser.kt @@ -44,19 +44,18 @@ internal class WestmangaParser(context: MangaLoaderContext) : ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val page = (offset / PAGE_SIZE) + 1 val urlBuilder = "$apiUrl/api/contents".toHttpUrl().newBuilder() - if (!query.isNullOrBlank()) { - urlBuilder.addQueryParameter("q", query) + if (!filter.query.isNullOrBlank()) { + urlBuilder.addQueryParameter("q", filter.query) } urlBuilder.addQueryParameter("page", page.toString()) urlBuilder.addQueryParameter("per_page", PAGE_SIZE.toString()) - if (query.isNullOrBlank()) { + if (filter.query.isNullOrBlank()) { val orderBy = when (order) { SortOrder.POPULARITY -> "Popular" SortOrder.UPDATED -> "Update" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt index f8c154afd..a68e9103a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt @@ -56,7 +56,6 @@ internal abstract class IkenParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(defaultDomain) @@ -64,8 +63,8 @@ internal abstract class IkenParser( append(page) append("&perPage=18&searchTerm=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(filter.query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt index a5c2041f2..95e098bd1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/Klz9.kt @@ -66,7 +66,6 @@ internal class Klz9(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val builder = urlBuilder() .addPathSegment("api").addPathSegment("manga") .addPathSegment("list") @@ -74,8 +73,8 @@ internal class Klz9(context: MangaLoaderContext) : builder.addQueryParameter("page", page.toString()) builder.addQueryParameter("limit", pageSize.toString()) - if (!query.isNullOrEmpty()) { - builder.addQueryParameter("search", query) + if (!filter.query.isNullOrEmpty()) { + builder.addQueryParameter("search", filter.query) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt index d0c537386..aebb7dede 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt @@ -43,24 +43,23 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : ) override val authUrl: String - get() = "https://${getDomain(this, "account")}/login?site=seiga" + get() = "https://${getDomain("account")}/login?site=seiga" - override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(getDomain(this, "seiga")).any { + override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(getDomain("seiga")).any { it.name == "user_session" } override suspend fun getUsername(): String { - val body = webClient.httpGet("https://${getDomain(this, "app")}/my/apps").parseHtml().body() + val body = webClient.httpGet("https://${getDomain("app")}/my/apps").parseHtml().body() return body.selectFirst("#userinfo > div > div > strong")?.text() ?: throw AuthRequiredException(source) } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val page = (offset / 20f).toIntUp().inc() - val domain = getDomain(this, "seiga") + val domain = getDomain("seiga") val url = when { - !query.isNullOrEmpty() -> { - return if (offset == 0) getSearchList(query, page) else emptyList() + !filter.query.isNullOrEmpty() -> { + return if (offset == 0) getSearchList(filter.query, page) else emptyList() } else -> { @@ -106,14 +105,14 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : STATUS_FINISHED -> MangaState.FINISHED else -> null }, - publicUrl = href.toAbsoluteUrl(item.host ?: getDomain(this, "seiga")), + publicUrl = href.toAbsoluteUrl(item.host ?: getDomain("seiga")), source = source, ) } } override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain(this, "seiga"))).parseHtml() + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(getDomain("seiga"))).parseHtml() val contents = doc.body().requireElementById("contents") val statusText = contents .select("div.mg_work_detail > div > div:nth-child(2) > div.tip.content_status.status_series > span") @@ -151,7 +150,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : } override suspend fun getPages(chapter: MangaChapter): List { - val fullUrl = chapter.url.toAbsoluteUrl(getDomain(this, "seiga")) + val fullUrl = chapter.url.toAbsoluteUrl(getDomain("seiga")) val doc = webClient.httpGet(fullUrl).parseHtml() if (!doc.select("#login_manga").isEmpty()) throw AuthRequiredException(source) @@ -168,7 +167,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : } private suspend fun fetchAvailableTags(): Set { - val doc = webClient.httpGet("https://${getDomain(this, "seiga")}/manga/list").parseHtml() + val doc = webClient.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml() val root = doc.body().selectOrThrow("#mg_category_list > ul > li").drop(1) return root.mapToSet { li -> val a = li.selectFirstOrThrow("a") @@ -181,7 +180,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : } private suspend fun getSearchList(query: String, page: Int): List { - val domain = getDomain(this, "seiga") + val domain = getDomain("seiga") val doc = webClient.httpGet("https://$domain/manga/search/?q=$query&page=$page&sort=score").parseHtml() val root = doc.body().select(".search_result__item") return root.mapNotNull { item -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt index 1b08e0f19..e2f1d681e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/keyoapp/KeyoappParser.kt @@ -66,10 +66,15 @@ internal abstract class KeyoappParser( ) override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query.orEmpty() + var query = "" var tag = "" val url = urlBuilder().apply { + + filter.query?.let { + query = filter.query + } + filter.tags.oneOrThrowIfMany()?.let { tag = it.title } @@ -79,6 +84,7 @@ internal abstract class KeyoappParser( SortOrder.NEWEST -> addPathSegment("series") else -> addPathSegment("series") } + }.build() return parseMangaList(webClient.httpGet(url).parseHtml(), tag, query) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt index 143bb9536..672c9b561 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/likemanga/LikeMangaParser.kt @@ -52,17 +52,16 @@ internal abstract class LikeMangaParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/?act=search") - query?.let { query -> + filter.query?.let { append("&f") append("[keyword]".urlEncoded()) append("=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } append("&f") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt index 8bb7afb0e..73795abb8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/LilianaParser.kt @@ -57,16 +57,15 @@ internal abstract class LilianaParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/") append(page) append("/?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt index 4466ed9b5..290a2a43f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt @@ -250,8 +250,6 @@ internal abstract class MadaraParser( protected open val listUrl = "manga/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author if (withoutAjax) { val pages = page + 1 @@ -265,8 +263,8 @@ internal abstract class MadaraParser( } append("/?s=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(filter.query.urlEncoded()) } append("&post_type=wp-manga") @@ -307,8 +305,8 @@ internal abstract class MadaraParser( append(filter.year.toString()) } - if (!author.isNullOrEmpty()) { - author.let { + if (!filter.author.isNullOrEmpty()) { + filter.author.let { append("&author=") // should be like "minamida-usuke" append(it.lowercase().replace(" ", "-")) @@ -349,8 +347,8 @@ internal abstract class MadaraParser( payload["page"] = page.toString() - query?.let { query -> - payload["vars[s]"] = query.urlEncoded() + filter.query?.let { + payload["vars[s]"] = filter.query.urlEncoded() } if (filter.tags.isNotEmpty()) { @@ -377,11 +375,11 @@ internal abstract class MadaraParser( payload["vars[tax_query][2][terms][]"] = filter.year.toString() } - if (!author.isNullOrEmpty()) { - author.let { + if (!filter.author.isNullOrEmpty()) { + filter.author.let { payload["vars[tax_query][3][taxonomy]"] = "wp-manga-author" payload["vars[tax_query][3][field]"] = "name" - payload["vars[tax_query][3][terms][0]"] = author + payload["vars[tax_query][3][terms][0]"] = filter.author payload["vars[tax_query][3][operator]"] = "IN" } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt index d2dd96a92..9bca29160 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manga18Fx.kt @@ -31,14 +31,13 @@ internal class Manga18Fx(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt index 99e385178..a1a084563 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/all/Manhwa18Cc.kt @@ -31,15 +31,14 @@ internal class Manhwa18Cc(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt index 49f920c7a..e1293c93a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/ArabsHentai.kt @@ -84,11 +84,10 @@ internal class ArabsHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val pages = page + 1 val url = when { - !query.isNullOrEmpty() || filter.tags.isNotEmpty() -> { + !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() -> { buildString { append("https://") append(domain) @@ -97,7 +96,7 @@ internal class ArabsHentai(context: MangaLoaderContext) : append(pages) } append("/?s=") - append(query?.urlEncoded() ?: "") + append(filter.query?.urlEncoded() ?: "") filter.tags.forEach { tag -> append("&genre%5B%5D=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt index 0ccfe6878..3cfb44328 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/RocksManga.kt @@ -72,7 +72,6 @@ internal class RocksManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -85,8 +84,8 @@ internal class RocksManga(context: MangaLoaderContext) : append("/?post_type=wp-manga") append("&s=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(it.urlEncoded()) } filter.types.forEach { contentType -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt index 7c70f6c05..396393901 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/AdultWebtoon.kt @@ -31,20 +31,19 @@ internal class AdultWebtoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt index 8cfc7a2da..76d4ee69e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Hentai4Free.kt @@ -23,16 +23,15 @@ internal class Hentai4Free(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt index 845648f35..9621e348a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiManga.kt @@ -25,20 +25,19 @@ internal class HentaiManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt index 413f6d882..2c68a9cc3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/HentaiWebtoon.kt @@ -25,20 +25,19 @@ internal class HentaiWebtoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt index d0be0d11a..f6c368064 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScan.kt @@ -32,14 +32,13 @@ internal class IsekaiScan(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt index 5ec2b780d..40d36c18d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/IsekaiScanEuParser.kt @@ -21,20 +21,19 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { append("/page/") append(page.toString()) } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt index a1eff32ce..e8a4e46a3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDass.kt @@ -30,14 +30,13 @@ internal class MangaDass(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt index 4ae595ec1..cc9b80205 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaDna.kt @@ -25,14 +25,13 @@ internal class MangaDna(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt index 3000302d6..c1393bb10 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt @@ -36,14 +36,13 @@ internal class MangaPure(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt index cc4a8f879..df31a917e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhwaz.kt @@ -26,14 +26,13 @@ internal class Manhwaz(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt index e002b2066..223786876 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/ManyToon.kt @@ -28,20 +28,19 @@ internal class ManyToon(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val pages = page + 1 val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (pages > 1) { append("/page/") append(pages.toString()) } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&post_type=wp-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt index 37bb7d35b..abff4c3ae 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/DoujinHentaiNet.kt @@ -42,15 +42,14 @@ internal class DoujinHentaiNet(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val pageNum = page + 1 val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?query=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (pageNum > 1) { append("&page=") append(pageNum) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt index f9098ca24..c91758121 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/es/TmoManga.kt @@ -29,15 +29,14 @@ internal class TmoManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/$listUrl") append("?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=") append(page) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt index 1ee5956cc..c07178896 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/fr/RaijinScans.kt @@ -85,13 +85,12 @@ internal class RaijinScans(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://$domain") if (page > 0) append("/page/${page + 1}") append("?post_type=wp-manga&s=") - query?.let { query -> append(query.urlEncoded()) } + filter.query?.let { append(it.urlEncoded()) } if (filter.year != YEAR_UNKNOWN) append("&release[]=${filter.year}") if (!filter.tags.isEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt index e7fb109f4..5a222ed0a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/id/ManhwaHub.kt @@ -30,14 +30,13 @@ internal class ManhwaHub(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt index 6a62ca5ff..3b520f721 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt @@ -31,17 +31,15 @@ internal class HentaiCube(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val pages = page + 1 val url = buildString { - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tacgia/") - append(author.lowercase().replace(" ", "-")) + append(filter.author.lowercase().replace(" ", "-")) if (pages > 1) { append("/page/") @@ -71,8 +69,8 @@ internal class HentaiCube(context: MangaLoaderContext) : append("/?s=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(filter.query.urlEncoded()) } append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt index 41932bdc5..abdae5403 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiVnPlus.kt @@ -23,17 +23,15 @@ internal class HentaiVnPlus(context: MangaLoaderContext) : override val authorSearchSupported = true override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val pages = page + 1 val url = buildString { - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tac-gia/") - append(author.lowercase().replace(" ", "-")) + append(filter.author.lowercase().replace(" ", "-")) if (pages > 1) { append("/page/") @@ -63,8 +61,8 @@ internal class HentaiVnPlus(context: MangaLoaderContext) : append("/?s=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(filter.query.urlEncoded()) } append("&post_type=wp-manga") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt index 7d0fc9d9a..b91703139 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt @@ -29,14 +29,13 @@ internal class Saytruyenhay(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt index 5bf2efedc..e3a0aef2a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/MadthemeParser.kt @@ -68,7 +68,6 @@ internal abstract class MadthemeParser( protected open val datePattern = "MMM dd, yyyy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -78,9 +77,9 @@ internal abstract class MadthemeParser( append("?page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } append("&sort=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt index 5685b7c4c..c640a9737 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madtheme/en/ManhuaScan.kt @@ -55,8 +55,6 @@ internal class ManhuaScan(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://") append(domain) @@ -65,9 +63,9 @@ internal class ManhuaScan(context: MangaLoaderContext) : append("?page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&q=") - append(query.urlEncoded()) + append(it.urlEncoded()) } append("&sort=") @@ -108,7 +106,7 @@ internal class ManhuaScan(context: MangaLoaderContext) : ) } ?: append("&status=all") - author?.takeIf { it.isNotBlank() }?.let { author -> + filter.author?.takeIf { it.isNotBlank() }?.let { author -> append("&author=") append(author.urlEncoded()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt index de29c3300..c96f920c6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt @@ -62,13 +62,12 @@ internal abstract class Manga18Parser( protected open val datePattern = "dd-MM-yyyy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append('/') - if (filter.tags.isNotEmpty() && query != null) { + if (filter.tags.isNotEmpty() && filter.query != null) { throw IllegalArgumentException("Search is not supported with tags") } @@ -81,11 +80,11 @@ internal abstract class Manga18Parser( } } - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append(listUrl) append(page.toString()) append("?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&order_by=latest") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt index 12c1a22e5..1c87f670d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt @@ -89,14 +89,13 @@ internal abstract class MangaboxParser( protected open val itemSelector = "div.search-story-item, a.list-story-item, .story_item" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder() - if (!query.isNullOrEmpty()) { - val filteredQuery = query.filterQuery() + if (!filter.query.isNullOrEmpty()) { + val query = filter.query.filterQuery() url.addPathSegment("search") url.addPathSegment("story") - url.addPathSegment(filteredQuery) + url.addPathSegment(query) } else { url.addPathSegment("genre") if (filter.tags.isEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt index 1f188a9b7..f02d37abf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangadventure/MangAdventureParser.kt @@ -61,13 +61,12 @@ internal abstract class MangAdventureParser( order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query val url = apiUrl.addEncodedPathSegment("series") .addEncodedQueryParameter("limit", pageSize.toString()) .addEncodedQueryParameter("page", page.toString()) - query?.let { query -> - url.addQueryParameter("title", query) + filter.query?.let { + url.addQueryParameter("title", filter.query) } url.addQueryParameter( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt index 2cba9fcf6..3e9ccf4c6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/MangaReaderParser.kt @@ -70,18 +70,17 @@ internal abstract class MangaReaderParser( protected val mutex = Mutex() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/page/") append(page.toString()) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt index e3013a104..cbef2d8a7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/ar/Normoyun.kt @@ -23,15 +23,14 @@ internal class Normoyun(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt index 5b0085fbf..02395442d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RizzComic.kt @@ -58,17 +58,16 @@ internal class RizzComic(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query if (page > 1) { return emptyList() } var url = "https://$domain$filterUrl" val payload = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { url = "https://$domain$searchUrl" - if (query != "") { - mapOf("search_value" to query.trim()) + if (filter.query != "") { + mapOf("search_value" to filter.query.trim()) } else { null } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt index a08104b32..dc794832e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/Zahard.kt @@ -31,7 +31,6 @@ internal class Zahard(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -39,9 +38,9 @@ internal class Zahard(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt index 0e70add01..451cdfa97 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/CerberuSeries.kt @@ -29,17 +29,16 @@ internal class CerberuSeries(context: MangaLoaderContext) : // Site is served at /wp/ subpath, so search URLs need that prefix too. override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/wp/page/") append(page.toString()) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt index e319bd09e..f4eda7727 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/HentaiReader.kt @@ -18,17 +18,16 @@ internal class HentaiReader(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append(listUrl) append("?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt index ee909c610..0356b6fc7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/LectorHentai.kt @@ -17,17 +17,16 @@ internal class LectorHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append(listUrl) append("?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt index 900cd2724..3077b5668 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/MangaTv.kt @@ -18,16 +18,15 @@ internal class MangaTv(context: MangaLoaderContext) : override val datePattern = "yyyy-MM-dd" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/lista?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt index 273912d5a..4c14dd5d5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TenkaiScan.kt @@ -46,10 +46,9 @@ internal class TenkaiScan(context: MangaLoaderContext) : private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.ROOT) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query if (page > 1) return emptyList() // site does not paginate; all comics are returned on page 1 val url = "https://$domain/comics".toHttpUrl().newBuilder().apply { - if (!query.isNullOrBlank()) addQueryParameter("search", query) + if (!filter.query.isNullOrBlank()) addQueryParameter("search", filter.query) }.build() val doc = webClient.httpGet(url).parseHtml() return doc.select(".card.blade-project-index").mapNotNull { card -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt index 806f4ba9d..f31eaaf85 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/es/TuManhwas.kt @@ -30,7 +30,6 @@ internal class TuManhwas(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -38,9 +37,9 @@ internal class TuManhwas(context: MangaLoaderContext) : append("?page=") append(page.toString()) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt index 456cc8428..3d0a02e5a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/fr/RevolutionScantrad.kt @@ -30,11 +30,10 @@ internal class RevolutionScantrad(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query if (page > 1) { return emptyList() } - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } val url = buildString { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt index 7b0052b9f..0841c0ba3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikIndo.kt @@ -46,7 +46,6 @@ internal class KomikIndo(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -112,9 +111,9 @@ internal class KomikIndo(context: MangaLoaderContext) : }, ) - query?.let { query -> + filter.query?.let { append("&title=") - append(query.urlEncoded()) + append(it.urlEncoded()) } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt index b5743c23b..ea024c7d1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/id/KomikSan.kt @@ -23,15 +23,14 @@ internal class KomikSan(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt index 21546b489..f9e4745f1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorldParser.kt @@ -57,10 +57,8 @@ internal abstract class MangaWorldParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author // Handle UPDATED sort without filters - use homepage - val noQuery = query.isNullOrBlank() + val noQuery = filter.query.isNullOrBlank() && filter.tags.isEmpty() && filter.states.isEmpty() && filter.types.isEmpty() @@ -77,9 +75,9 @@ internal abstract class MangaWorldParser( append("/archive?&page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } filter.tags.forEach { @@ -127,7 +125,7 @@ internal abstract class MangaWorldParser( } // author ( not query but same to tags ) - // author.forEach { + // filter.author.forEach { // append("&author=") // append(it.key) // } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt index d882a44b0..02bc867f7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manhwaz/ManhwaZ.kt @@ -73,13 +73,12 @@ internal abstract class ManhwaZ( protected open val searchPath = "/search" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("$searchPath?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } else { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt index 4b3da68fd..72424b3f5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt @@ -74,10 +74,9 @@ internal abstract class MmrcmsParser( protected open val datePattern = "dd MMM. yyyy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query if (order == SortOrder.UPDATED) { - if (query != null || filter.tags.isNotEmpty()) { + if (filter.query != null || filter.tags.isNotEmpty()) { throw IllegalArgumentException("Sorting by update with filters is not supported by this source.") } @@ -99,8 +98,8 @@ internal abstract class MmrcmsParser( append(page.toString()) append("&author=&tag=&alpha=") - query?.let { query -> - append(query.urlEncoded()) + filter.query?.let { + append(filter.query.urlEncoded()) } append("&cat=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt index 055d145af..498478157 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/natsu/NatsuParser.kt @@ -111,8 +111,6 @@ internal abstract class NatsuParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = "https://${domain}/wp-admin/admin-ajax.php?action=advanced_search" val formParts = mutableMapOf() @@ -132,8 +130,8 @@ internal abstract class NatsuParser( formParts["page"] = page.toString() - if (!author.isNullOrEmpty()) { - val authorArray = JSONArray(author) + if (!filter.author.isNullOrEmpty()) { + val authorArray = JSONArray(filter.author) formParts["author"] = authorArray.toString() } else formParts["author"] = "[]" @@ -181,8 +179,8 @@ internal abstract class NatsuParser( else -> "popular" } - if (!query.isNullOrEmpty()) { - formParts["query"] = query + if (!filter.query.isNullOrEmpty()) { + formParts["query"] = filter.query } val html = httpPost(url, formParts) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt index f5f92cb95..48aac05a1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/nepnep/NepnepParser.kt @@ -63,7 +63,6 @@ internal abstract class NepnepParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val doc = searchDoc.get() val json = JSONArray( doc.selectFirstOrThrow("script:containsData(MainFunction)").data() @@ -84,10 +83,10 @@ internal abstract class NepnepParser( //val viewMonth = m.getString("vm") when { - !query.isNullOrEmpty() -> { - if (m.getString("s").contains(query, ignoreCase = true) || (m.getJSONArray("al") + !filter.query.isNullOrEmpty() -> { + if (m.getString("s").contains(filter.query, ignoreCase = true) || (m.getJSONArray("al") .length() > 0 && m.getJSONArray("al").getString(0) - .contains(query, ignoreCase = true)) + .contains(filter.query, ignoreCase = true)) ) { mangaWithLastUpdateList.add( MangaWithLastUpdate(addManga(href, imgUrl, m), lastUpdate, views), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt index f4877713e..319ea41c8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/otakusanctuary/OtakuSanctuaryParser.kt @@ -67,9 +67,8 @@ internal abstract class OtakuSanctuaryParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val doc = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } @@ -77,7 +76,7 @@ internal abstract class OtakuSanctuaryParser( append("https://") append(domain) append("/Home/Search?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } webClient.httpGet(url).parseHtml().requireElementById("collection-manga") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt index ee2f0901e..279a7869c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/PizzaReaderParser.kt @@ -105,7 +105,6 @@ internal abstract class PizzaReaderParser( protected open val abandonedFilter = "droppato" override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val manga = ArrayList() val selectedState = filter.states.oneOrThrowIfMany() val selectedRating = filter.contentRating.oneOrThrowIfMany() @@ -113,8 +112,8 @@ internal abstract class PizzaReaderParser( val excludeTags = filter.tagsExclude.map { it.key.lowercase(Locale.ROOT) } when { - !query.isNullOrEmpty() -> { - val jsonManga = webClient.httpGet("https://$domain/api/search/${query.urlEncoded()}").parseJson() + !filter.query.isNullOrEmpty() -> { + val jsonManga = webClient.httpGet("https://$domain/api/search/${filter.query.urlEncoded()}").parseJson() .getJSONArray("comics") for (i in 0 until jsonManga.length()) { val j = jsonManga.getJSONObject(i) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt index 671d5a34b..87b157b63 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/AnimeXNovel.kt @@ -55,8 +55,7 @@ internal class AnimeXNovel(context: MangaLoaderContext) : private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val all = fetchAllSeries(query) + val all = fetchAllSeries(filter.query) val sorted = when (order) { SortOrder.POPULARITY -> all.sortedByDescending { it.postCount } SortOrder.ALPHABETICAL -> all.sortedBy { it.name.lowercase(Locale.ROOT) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt index 969abf520..a3a281cbb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt @@ -35,18 +35,17 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { append("/page/$page/") } append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { @@ -78,7 +77,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, val item = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { doc.select("div.listagem div.item") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt index 0ee53c872..98b4f1607 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/InstaHentai.kt @@ -47,15 +47,14 @@ internal class InstaHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("/page/") append(page) append("/?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else { val tagSlug = filter.tags.oneOrThrowIfMany()?.key if (tagSlug != null) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt index b6e1b1fc2..552d51de2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerManga.kt @@ -42,7 +42,6 @@ internal class LerManga(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -55,7 +54,7 @@ internal class LerManga(context: MangaLoaderContext) : PagedMangaParser(context, when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt index 94c8222b7..573557b7a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LerMangaOnline.kt @@ -35,20 +35,19 @@ internal class LerMangaOnline(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { append("page/") append(page.toString()) append("/") } append("?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt index 41bfde54a..7063378bf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/LuratoonScansParser.kt @@ -37,8 +37,7 @@ internal class LuratoonScansParser(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - require(query.isNullOrEmpty()) { ErrorMessages.SEARCH_NOT_SUPPORTED } + require(filter.query.isNullOrEmpty()) { ErrorMessages.SEARCH_NOT_SUPPORTED } val url = urlBuilder() val tag = filter.tags.oneOrThrowIfMany() if (tag == null) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt index d7e9a6d05..64abdba39 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MangaOnline.kt @@ -33,15 +33,14 @@ internal class MangaOnline(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search/") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt index ea5fd2eef..5bd97465c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/Manhastro.kt @@ -49,11 +49,10 @@ internal class Manhastro(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val all = allMangasCache.get() var filtered: List = all - if (!query.isNullOrEmpty()) { - val q = query.lowercase(sourceLocale) + if (!filter.query.isNullOrEmpty()) { + val q = filter.query.lowercase(sourceLocale) filtered = filtered.filter { m -> (m.getStringOrNull("titulo")?.lowercase(sourceLocale)?.contains(q) == true) || (m.getStringOrNull("titulo_brasil")?.lowercase(sourceLocale)?.contains(q) == true) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt index 52fb35547..802875c28 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MediocreToons.kt @@ -75,9 +75,8 @@ internal class MediocreToons(context: MangaLoaderContext) : PagedMangaParser( private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", sourceLocale) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = when { - !query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.types.isNotEmpty() -> buildSearchUrl( + !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.types.isNotEmpty() -> buildSearchUrl( page, filter, ) @@ -97,13 +96,12 @@ internal class MediocreToons(context: MangaLoaderContext) : PagedMangaParser( } private fun buildSearchUrl(page: Int, filter: MangaListFilter): HttpUrl { - val query = filter.query val builder = "$apiUrl/obras".toHttpUrl().newBuilder().addQueryParameter("limite", pageSize.toString()) .addQueryParameter("pagina", page.toString()) // Add search query - if (!query.isNullOrEmpty()) { - builder.addQueryParameter("string", query) + if (!filter.query.isNullOrEmpty()) { + builder.addQueryParameter("string", filter.query) } // Add tags diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt index bb04a6ca4..a846a14fd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/MuitoHentai.kt @@ -31,16 +31,15 @@ internal class MuitoHentai(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) return emptyList() append("/buscar-manga/?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt index a628ba99e..70ec164af 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SssScanlator.kt @@ -51,7 +51,6 @@ internal class SssScanlator(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -68,9 +67,9 @@ internal class SssScanlator(context: MangaLoaderContext) : else -> "recent" }, ) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } filter.tags.firstOrNull()?.let { tag -> append("&genre=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt index a84fe81d8..3e45ad084 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/SussyScan.kt @@ -86,14 +86,13 @@ internal class SussyScan(context: MangaLoaderContext) : PagedMangaParser( private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", sourceLocale) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val genId = when { filter.types.oneOrThrowIfMany() == ContentType.HENTAI -> "5" else -> "1" } val url = when { - !query.isNullOrEmpty() || filter.tags.isNotEmpty() || + !filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty() -> buildSearchUrl(page, filter) // Popularity rankings @@ -132,9 +131,8 @@ internal class SussyScan(context: MangaLoaderContext) : PagedMangaParser( } private fun buildSearchUrl(page: Int, filter: MangaListFilter): HttpUrl { - val query = filter.query val builder = "$apiUrl/obras".toHttpUrl().newBuilder() - .addQueryParameter("obr_nome", query ?: "") + .addQueryParameter("obr_nome", filter.query ?: "") .addQueryParameter("limite", "15") .addQueryParameter("pagina", page.toString()) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt index a9561c457..e9b1cdf94 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/TaiyoParser.kt @@ -111,7 +111,7 @@ internal class TaiyoParser(context: MangaLoaderContext) : .addHeader(CommonHeaders.ORIGIN, "https://$domain") .addHeader(CommonHeaders.REFERER, "https://$domain/") .addHeader(CommonHeaders.ACCEPT, "application/json") - .tag(source) + .tag(MangaParserSource::class.java, source) .build() val client = context.httpClient.newBuilder() .apply { interceptors().clear() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt index 8b14132d0..429269e8d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/YugenMangas.kt @@ -34,16 +34,15 @@ internal class YugenMangas(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val json = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://api.") append(domain) append("/api/series/?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } webClient.httpGet(url).parseJsonArray() } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt index 8bda4474c..1e2a3bc9b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt @@ -48,17 +48,16 @@ internal class AComics(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 0) { return emptyList() } append("/search?keyword=") - append(query) + append(filter.query) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt index 17202d936..fc82d67f1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ComXParser.kt @@ -48,11 +48,10 @@ internal class ComXParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val urlBuilder = StringBuilder() when { - !query.isNullOrEmpty() -> { - val encodedQuery = query.splitByWhitespace().joinToString(separator = "%20") { part -> + !filter.query.isNullOrEmpty() -> { + val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "%20") { part -> part.urlEncoded() } urlBuilder.append("/search/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt index c6cf24f35..4636ffea6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt @@ -54,8 +54,7 @@ internal class DesuMeParser(context: MangaLoaderContext) : private val tagsCache = suspendLazy(initializer = ::fetchTags) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - if (!query.isNullOrEmpty() && page != searchPaginator.firstPage) { + if (!filter.query.isNullOrEmpty() && page != searchPaginator.firstPage) { return emptyList() } val domain = domain @@ -70,9 +69,9 @@ internal class DesuMeParser(context: MangaLoaderContext) : append("&genres=") filter.tags.joinTo(this, ",") { it.key } } - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&search=") - append(query) + append(filter.query) } } val json = webClient.httpGet(url).parseJson().getJSONArray("response") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt index 4dd4253ae..f2c524843 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/MangaWtfParser.kt @@ -55,7 +55,6 @@ internal class MangaWtfParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder("api") .addPathSegment("v2") @@ -64,7 +63,7 @@ internal class MangaWtfParser( .addQueryParameter("size", pageSize.toString()) .addQueryParameter("type", "COMIC") when { - query.isNullOrEmpty() -> { + filter.query.isNullOrEmpty() -> { url.addQueryParameter( "sort", when (order) { @@ -111,7 +110,7 @@ internal class MangaWtfParser( } else -> { - url.addQueryParameter("search", query) + url.addQueryParameter("search", filter.query) } } val ja = webClient.httpGet(url.build()).parseJsonArray() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt index 1b52dd204..571ba6573 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt @@ -55,15 +55,14 @@ internal class NudeMoonParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val domain = domain val url = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (!isAuthorized()) { throw AuthRequiredException(source) } - "https://$domain/search?stext=${query.urlEncoded()}&rowstart=$offset" + "https://$domain/search?stext=${filter.query.urlEncoded()}&rowstart=$offset" } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt index da6995c6a..9605d3ad9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt @@ -82,7 +82,6 @@ internal class RemangaParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query copyCookies() val urlBuilder = urlBuilder(subdomain = "api") .addPathSegment("api") @@ -91,8 +90,8 @@ internal class RemangaParser( .addQueryParameter("page", page.toString()) .addQueryParameter("count", PAGE_SIZE.toString()) .addQueryParameter("ordering", getSortKey(order)) - if (!query.isNullOrEmpty()) { - urlBuilder.addQueryParameter("query", query) + if (!filter.query.isNullOrEmpty()) { + urlBuilder.addQueryParameter("query", filter.query) } else { urlBuilder.addPathSegment("catalog") } @@ -169,7 +168,7 @@ internal class RemangaParser( } override suspend fun getPages(chapter: MangaChapter): List { - val content = webClient.httpGet(chapter.url.toAbsoluteUrl(org.koitharu.kotatsu.parsers.util.getDomain(this, "api"))) + val content = webClient.httpGet(chapter.url.toAbsoluteUrl(getDomain("api"))) .parseJson() val pages = content.optJSONArray("pages") if (pages == null) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt index f9c1e4b2a..2707a7a17 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/ZenMangaParser.kt @@ -51,10 +51,8 @@ internal class ZenMangaParser(context: MangaLoaderContext) : private val apiDomain = if (domain.startsWith("v1.")) domain.replace("v1.", "api.") else "api.$domain" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author - if (!author.isNullOrBlank()) { - return getListPageByAuthor(author, page) + if (!filter.author.isNullOrBlank()) { + return getListPageByAuthor(filter.author, page) } val urlBuilder = HttpUrl.Builder() @@ -68,8 +66,8 @@ internal class ZenMangaParser(context: MangaLoaderContext) : urlBuilder.addQueryParameter("sort", getSortParameter(order)) - if (!query.isNullOrBlank()) { - urlBuilder.addQueryParameter("search", query) + if (!filter.query.isNullOrBlank()) { + urlBuilder.addQueryParameter("search", filter.query) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt index e3c591a08..f7f002552 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt @@ -91,7 +91,6 @@ internal abstract class GroupleParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val domain = domain val root = if (filter.isEmpty()) { webClient.httpGet( @@ -357,12 +356,11 @@ internal abstract class GroupleParser( } private suspend fun advancedSearch(offset: Int, order: SortOrder, filter: MangaListFilter): Response { - val query = filter.query val tagsMap = tagsIndex.get() val url = urlBuilder() .addPathSegment("search") .addPathSegment("advancedResults") - url.addQueryParameter("q", query) + url.addQueryParameter("q", filter.query) url.addQueryParameter("offset", offset.toString()) filter.tags.forEach { tag -> val tagId = requireNotNull(tagsMap[tag.title.lowercase()]) { "Tag ${tag.title} not found" } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt index 85e31dcb0..dbe16c35b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt @@ -42,7 +42,6 @@ internal abstract class ChanParser( ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val domain = domain val doc = webClient.httpGet(buildUrl(offset, order, filter)).parseHtml() val root = doc.body().selectFirst("div.main_fon")?.getElementById("content") @@ -194,18 +193,17 @@ internal abstract class ChanParser( order: SortOrder, filter: MangaListFilter, ): HttpUrl { - val query = filter.query val builder = urlBuilder() builder.addQueryParameter("offset", offset.toString()) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { builder.addQueryParameter("do", "search") builder.addQueryParameter("subaction", "search") builder.addQueryParameter("search_start", ((offset / 40) + 1).toString()) builder.addQueryParameter("full_search", "0") builder.addQueryParameter("result_from", (offset + 1).toString()) builder.addQueryParameter("result_num", "40") - builder.addQueryParameter("story", query) + builder.addQueryParameter("story", filter.query) builder.addQueryParameter("need_sort_date", "false") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt index a01688c13..87fb31478 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt @@ -59,37 +59,30 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context, ) } - override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - return super.getList(offset, order, filter) - } - - override fun buildUrl(offset: Int, order: SortOrder, filter: MangaListFilter): HttpUrl { - val query = filter.query - return when { - query.isNullOrEmpty() && filter.tags.isEmpty() && filter.tagsExclude.isEmpty() -> { - val builder = urlBuilder().addQueryParameter("offset", offset.toString()) - when (order) { - SortOrder.POPULARITY -> { - builder.addPathSegment("mostviews") - builder.addQueryParameter("sort", "manga") - } + override fun buildUrl(offset: Int, order: SortOrder, filter: MangaListFilter): HttpUrl = when { + filter.query.isNullOrEmpty() && filter.tags.isEmpty() && filter.tagsExclude.isEmpty() -> { + val builder = urlBuilder().addQueryParameter("offset", offset.toString()) + when (order) { + SortOrder.POPULARITY -> { + builder.addPathSegment("mostviews") + builder.addQueryParameter("sort", "manga") + } - SortOrder.RATING -> { - builder.addPathSegment("mostfavorites") - builder.addQueryParameter("sort", "manga") - } + SortOrder.RATING -> { + builder.addPathSegment("mostfavorites") + builder.addQueryParameter("sort", "manga") + } - else -> { // SortOrder.NEWEST - builder.addPathSegment("manga") - builder.addPathSegment("newest") - } + else -> { // SortOrder.NEWEST + builder.addPathSegment("manga") + builder.addPathSegment("newest") } - builder.build() } + builder.build() + } - else -> { - super.buildUrl(offset, order, filter) - } + else -> { + super.buildUrl(offset, order, filter) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt index a2cebe3b6..2acddbf36 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt @@ -108,7 +108,6 @@ internal abstract class LibSocialParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val urlBuilder = HttpUrl.Builder() .scheme(SCHEME_HTTPS) .host(apiHost) @@ -127,8 +126,8 @@ internal abstract class LibSocialParser( for (tag in filter.tagsExclude) { urlBuilder.addQueryParameter("${tag.typeKey()}_exclude[]", tag.key.drop(1)) } - if (!query.isNullOrEmpty()) { - urlBuilder.addQueryParameter("q", query) + if (!filter.query.isNullOrEmpty()) { + urlBuilder.addQueryParameter("q", filter.query) } urlBuilder.addQueryParameter( "sort_by", diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt index 496e814ae..a35821686 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/scan/ScanParser.kt @@ -43,17 +43,16 @@ internal abstract class ScanParser( protected open val listUrl = "/manga" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val q = filter.query - var isSearch = false + var query = false val url = buildString { append("https://") append(domain) when { - !q.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search?q=") - append(q.urlEncoded()) - isSearch = true + append(filter.query.urlEncoded()) + query = true } else -> { @@ -81,7 +80,7 @@ internal abstract class ScanParser( } } - val doc = if (isSearch) { + val doc = if (query) { val raw = webClient.httpGet(url).parseRaw() Jsoup.parseBodyFragment( raw.unescapeJson(), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt index 2e033b9ea..6a514676d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt @@ -57,16 +57,15 @@ internal abstract class SinmhParser( protected open val listUrl = "list/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append(searchUrl) append("?keywords=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt index c78d71ffb..417dded6b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt @@ -50,7 +50,6 @@ internal class ElderManga(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -58,9 +57,9 @@ internal class ElderManga(context: MangaLoaderContext): append("?page=") append(page.toString()) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt index 0b189a58e..80c083db0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/MangaAy.kt @@ -35,16 +35,15 @@ internal class MangaAy(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } return parseMangaListQueryOrTags( webClient.httpPost( "https://$domain/arama", - mapOf("title" to query.urlEncoded(), "genres" to ""), + mapOf("title" to filter.query.urlEncoded(), "genres" to ""), ).parseHtml(), ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt index 80947aa4a..98d4d3d0b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/SadScans.kt @@ -31,14 +31,13 @@ internal class SadScans(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/series") - query?.let { query -> + filter.query?.let { append("?search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt index f382c175a..c5224d8a4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt @@ -50,7 +50,6 @@ internal class TenshiManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -58,9 +57,9 @@ internal class TenshiManga(context: MangaLoaderContext) : append("?page=") append(page.toString()) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt index 9424bea73..bb05c6823 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TrWebtoon.kt @@ -43,9 +43,8 @@ internal class TrWebtoon(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query if (order == SortOrder.UPDATED) { - if (filter.tags.isNotEmpty() || filter.states.isNotEmpty() || query != null) { + if (filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.query != null) { throw IllegalArgumentException("Sorting by update with filters is not supported by this source.") } val url = buildString { @@ -62,9 +61,9 @@ internal class TrWebtoon(context: MangaLoaderContext) : append("/webtoon-listesi?page=") append(page.toString()) - query?.let { query -> + filter.query?.let { append("&q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } filter.tags.oneOrThrowIfMany()?.let { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt index a66143f20..32bbca473 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt @@ -49,7 +49,6 @@ internal class UzayManga(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -57,9 +56,9 @@ internal class UzayManga(context: MangaLoaderContext) : append("?page=") append(page.toString()) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt index b6ead2d89..1af345784 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HentaiUkrParser.kt @@ -87,17 +87,16 @@ internal class HentaiUkrParser(context: MangaLoaderContext) : AbstractMangaParse } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query // Get all manga val json = allManga.get().toMutableList() - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { json.retainAll { item -> - item.getString("name").contains(query, ignoreCase = true) || - item.getStringOrNull("eng_name")?.contains(query, ignoreCase = true) == true || - item.getStringOrNull("orig_name")?.contains(query, ignoreCase = true) == true || - item.getStringOrNull("author")?.contains(query, ignoreCase = true) == true || - item.getStringOrNull("team")?.contains(query, ignoreCase = true) == true + item.getString("name").contains(filter.query, ignoreCase = true) || + item.getStringOrNull("eng_name")?.contains(filter.query, ignoreCase = true) == true || + item.getStringOrNull("orig_name")?.contains(filter.query, ignoreCase = true) == true || + item.getStringOrNull("author")?.contains(filter.query, ignoreCase = true) == true || + item.getStringOrNull("team")?.contains(filter.query, ignoreCase = true) == true } } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt index 2f8e1b943..3c8459ce2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/HoneyMangaParser.kt @@ -89,7 +89,6 @@ internal class HoneyMangaParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val body = JSONObject() body.put("page", page) body.put("pageSize", PAGE_SIZE) @@ -116,15 +115,15 @@ internal class HoneyMangaParser(context: MangaLoaderContext) : } - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { // Search when { - query.length < 3 -> throw IllegalArgumentException( + filter.query.length < 3 -> throw IllegalArgumentException( "The query must contain at least 3 characters (Запит має містити щонайменше 3 символи)", ) page == searchPaginator.firstPage -> webClient - .httpGet(searchApi + query.urlEncoded()) + .httpGet(searchApi + filter.query.urlEncoded()) .parseJsonArray() else -> JSONArray() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt index 2d385e68f..591b55991 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/uk/MangaInUaParser.kt @@ -42,14 +42,13 @@ internal class MangaInUaParser(context: MangaLoaderContext) : PagedMangaParser( ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { - append("/index.php?do=search&subaction=search&search_start=$page&full_search=1&story=${query}&titleonly=3") + !filter.query.isNullOrEmpty() -> { + append("/index.php?do=search&subaction=search&search_start=$page&full_search=1&story=${filter.query}&titleonly=3") } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt index f626aa3a6..3204e4596 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BFANGTeam.kt @@ -55,12 +55,10 @@ internal class BFANGTeam (context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = urlBuilder().addPathSegment("manga") - if (!query.isNullOrEmpty()) { - url.addEncodedQueryParameter("q", query) + if (!filter.query.isNullOrEmpty()) { + url.addEncodedQueryParameter("q", filter.query) } // todo: need to use data-genre (num) instead of genre name @@ -77,9 +75,9 @@ internal class BFANGTeam (context: MangaLoaderContext) : } } - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { url.addQueryParameter("q", - author.splitByWhitespace().joinToString(separator = "+") { it } + filter.author.splitByWhitespace().joinToString(separator = "+") { it } ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt index 3069bcf1c..7fbbb3337 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenParser.kt @@ -56,10 +56,9 @@ internal class BlogTruyenParser(context: MangaLoaderContext) : private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> { - val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page" + !filter.query.isNullOrEmpty() -> { + val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${filter.query.urlEncoded()}&p=$page" val searchContent = webClient.httpGet(searchUrl).parseHtml() .selectFirst("section.list-manga-bycate > div.list") parseMangaList(searchContent) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt index 4bc44cf20..794cedbef 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/BlogTruyenVN.kt @@ -60,10 +60,9 @@ internal class BlogTruyenVN(context: MangaLoaderContext) : private var cacheTags = suspendLazy(initializer = ::fetchTags) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> { - val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page" + !filter.query.isNullOrEmpty() -> { + val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${filter.query.urlEncoded()}&p=$page" val searchContent = webClient.httpGet(searchUrl).parseHtml() .selectFirst("section.list-manga-bycate > div.list") parseMangaList(searchContent) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt index c25f1c036..9d194182a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt @@ -107,9 +107,8 @@ internal class CMangaParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder().apply { - if (query.isNullOrEmpty() && (order == SortOrder.RELEVANCE || + if (filter.query.isNullOrEmpty() && (order == SortOrder.RELEVANCE || order == SortOrder.POPULARITY_TODAY || order == SortOrder.POPULARITY_WEEK || order == SortOrder.POPULARITY_MONTH @@ -122,7 +121,7 @@ internal class CMangaParser(context: MangaLoaderContext) : addQueryParameter("team", "0") addQueryParameter("sort", "update") addQueryParameter("tag", filter.tags.joinToString(separator = ",") { it.key }) - addQueryParameter("string", query.orEmpty()) + addQueryParameter("string", filter.query.orEmpty()) addQueryParameter( "status", when (filter.states.oneOrThrowIfMany()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt index be2a11839..5e8d4eb86 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt @@ -72,13 +72,12 @@ internal class CuuTruyenParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query // Move to urlBuilder val url = buildString { - if (!query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty()) { + if (!filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty()) { append("/mangas/search?q=") - if (!query.isNullOrEmpty()) { - append(query.urlEncoded()) + if (!filter.query.isNullOrEmpty()) { + append(filter.query.urlEncoded()) } // Bug from API: Select both ONGOING + FINISHED will return empty list for all case diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt index 5c677e239..e705951ac 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DamCoNuong.kt @@ -55,7 +55,6 @@ internal class DamCoNuong(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -91,9 +90,9 @@ internal class DamCoNuong(context: MangaLoaderContext) : append(filter.tags.joinTo(this, ",") { it.key }) } - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&filter[name]=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (filter.tagsExclude.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt index f27663525..4308edb7f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/DuaLeoTruyen.kt @@ -40,15 +40,14 @@ internal class DuaLeoTruyen(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/tim-kiem.html") append("?key=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } filter.tags.isNotEmpty() -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt index 2a4bc711c..fd00d70d8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranh.kt @@ -53,13 +53,12 @@ internal class GocTruyenTranh(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append("/baseapi/comics/filterComic") append("?keyword=") - append(query?.urlEncoded() ?: "") + append(filter.query?.urlEncoded() ?: "") if (filter.tags.isNotEmpty()) { append("&categories=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt index 809bd9c87..c83b4065c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/GocTruyenTranhVui.kt @@ -110,12 +110,11 @@ internal class GocTruyenTranhVui(context: MangaLoaderContext): } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append(apiUrl) append("/search?p=${page - 1}") - if (!query.isNullOrBlank()) { - append("&searchValue=${query.urlEncoded()}") + if (!filter.query.isNullOrBlank()) { + append("&searchValue=${filter.query.urlEncoded()}") } val sortValue = when (order) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt index 8f6daea4e..2db5cd81c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HangTruyen.kt @@ -45,7 +45,6 @@ internal class HangTruyen(context: MangaLoaderContext) : PagedMangaParser(contex ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("/tim-kiem?page=") append(page) @@ -80,9 +79,9 @@ internal class HangTruyen(context: MangaLoaderContext) : PagedMangaParser(contex else -> "view_desc" }) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&keyword=") - val encodedQuery = query.splitByWhitespace().joinToString(separator = "+") { part -> + val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part -> part.urlEncoded() } append(encodedQuery) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt index a3a2428e2..677ddc225 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt @@ -37,14 +37,13 @@ internal class Hentai18VN(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } - val keyword = query + val keyword = filter.query val url = "http://$domain/search/html/1" val headers = Headers.Builder().add("X-Requested-With", "XMLHttpRequest").build() val response = webClient.httpPost(url.toHttpUrl(), payload = "keyword=$keyword", headers).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt index a5b28623b..5cabdcb92 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVNParser.kt @@ -63,13 +63,12 @@ internal class HentaiVNParser(context: MangaLoaderContext) : AbstractMangaParser ) override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1 urlBuilder() val searchUrl = - "/tim-kiem-truyen.html?key=${query.urlEncoded()}&page=$page".toAbsoluteUrl(domain) + "/tim-kiem-truyen.html?key=${filter.query.urlEncoded()}&page=$page".toAbsoluteUrl(domain) val docs = webClient.httpGet(searchUrl).parseHtml() parseMainList(docs, page) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt index 599d64562..6d50d37e8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt @@ -35,7 +35,6 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -74,13 +73,13 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : append(filter.tagsExclude.joinToString(",") { it.key }) } - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tim-kiem?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) return@buildString // end of buildString } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt index b840b3857..9a0653fe3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnSU.kt @@ -71,13 +71,12 @@ internal class HentaiVnSU(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val apiUrl = urlBuilder().run { addPathSegments("api/library") when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { addPathSegments("search") - addQueryParameter("q", query) + addQueryParameter("q", filter.query) } filter.tags.isNotEmpty() -> { addPathSegments("advanced-search") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt index 9714db35c..28c297699 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/KuroNeko.kt @@ -55,16 +55,14 @@ internal class KuroNeko(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { clear() append("https://") append(domain) append("/tac-gia/") - append(author.lowercase().replace(" ", "-")) + append(filter.author.lowercase().replace(" ", "-")) append("?sort=") append( @@ -111,9 +109,9 @@ internal class KuroNeko(context: MangaLoaderContext): }, ) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt index 686a2dc1a..9535a3439 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LangGeekParser.kt @@ -58,14 +58,13 @@ internal class LangGeekParser(context: MangaLoaderContext): override suspend fun getFilterOptions() = MangaListFilterOptions() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query return when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) { return emptyList() } - val keyword = query.urlEncoded() + val keyword = filter.query.urlEncoded() val url = "https://$domain/wp-admin/admin-ajax.php?action=flatsome_ajax_search_products&query=${keyword}" val response = webClient.httpGet(url.toHttpUrl()).parseJson() parseMangaSearch(response) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt index b66862ae7..ecd085494 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/LxManga.kt @@ -50,16 +50,15 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/tim-kiem") append("?filter[name]=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=") @@ -106,7 +105,7 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context, } } - if (query.isNullOrEmpty()) { + if (filter.query.isNullOrEmpty()) { append("&sort=") when (order) { SortOrder.POPULARITY -> append("-views") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt index a2d5458a3..1f753bc54 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/MimiHentai.kt @@ -68,28 +68,26 @@ internal class MimiHentai(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions(availableTags = fetchTags()) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://") append("$domain/$apiSuffix") - if (!query.isNullOrEmpty() || - !author.isNullOrEmpty() || + if (!filter.query.isNullOrEmpty() || + !filter.author.isNullOrEmpty() || filter.tags.isNotEmpty() ) { append("/advance-search?page=") append(page) append("&max=18") // page size, avoid rate limit - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&name=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { append("&author=") - append(author.urlEncoded()) + append(filter.author.urlEncoded()) } if (filter.tags.isNotEmpty()) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt index 11589d28b..0c8faee3a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/NhentaiWorld.kt @@ -56,8 +56,6 @@ internal class NhentaiWorld(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val urlBuilder = urlBuilder() urlBuilder.host(apiDomain) urlBuilder.addPathSegment("comic") @@ -83,10 +81,10 @@ internal class NhentaiWorld(context: MangaLoaderContext) : } // Search - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { urlBuilder.addQueryParameter( "search", - query.splitByWhitespace().joinToString("+") { it } + filter.query.splitByWhitespace().joinToString("+") { it } ) } @@ -104,10 +102,10 @@ internal class NhentaiWorld(context: MangaLoaderContext) : } // Author - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { urlBuilder.addQueryParameter( "author", - author.splitByWhitespace().joinToString("+") { it } + filter.author.splitByWhitespace().joinToString("+") { it } ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt index ea67857dc..3d3e7346e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/OTruyenParser.kt @@ -74,14 +74,13 @@ internal class OTruyenParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/v1/api/tim-kiem?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt index 9980289cf..d900d7727 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenGG.kt @@ -43,15 +43,14 @@ internal class TruyenGG(context: MangaLoaderContext) : PagedMangaParser(context, ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { buildString { append("https://") append(domain) append("/tim-kiem/trang-$page.html") append("?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt index c3a7835c5..e7a15234a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentai18.kt @@ -42,7 +42,6 @@ internal class TruyenHentai18(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = when { filter.tags.isNotEmpty() -> { buildString { @@ -73,8 +72,8 @@ internal class TruyenHentai18(context: MangaLoaderContext): append("&page=") append(page) - if (!query.isNullOrEmpty()) { - append("&query=${query}") + if (!filter.query.isNullOrEmpty()) { + append("&query=${filter.query}") } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt index ccb70b877..55aea6d39 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt @@ -34,7 +34,6 @@ internal class TruyenHentaiVN(context: MangaLoaderContext) : override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -49,9 +48,9 @@ internal class TruyenHentaiVN(context: MangaLoaderContext) : } } - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/tim-kiem-truyen/?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=") append(page) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt index 6c9c986c8..ce52c8dc2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenQQ.kt @@ -48,15 +48,14 @@ internal class TruyenQQ(context: MangaLoaderContext): ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { buildString { append("https://") append(domain) append("/tim-kiem/trang-$page.html") append("?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt index 164de66ee..fea97c295 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenTranh3Q.kt @@ -45,7 +45,6 @@ internal class TruyenTranh3Q(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -59,9 +58,9 @@ internal class TruyenTranh3Q(context: MangaLoaderContext) : append("&") } - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt index 3e7fd19d9..beb652c94 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TuSachXinhXinh.kt @@ -40,13 +40,12 @@ internal class TuSachXinhXinh(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = urlBuilder() when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) return emptyList() // no paging url.addPathSegments("wp-admin/admin-ajax.php") - val payload = "action=searchtax&keyword=${query.urlEncoded()}" + val payload = "action=searchtax&keyword=${filter.query.urlEncoded()}" return webClient.httpPost(url.build(), payload) .parseJson().getJSONArray("data") .mapJSONNotNull(::parseSearchItem) @@ -59,7 +58,7 @@ internal class TuSachXinhXinh(context: MangaLoaderContext) : else -> url.addPathSegment("danh-sach-truyen") } - if (page > 1 && query.isNullOrEmpty()) { + if (page > 1 && filter.query.isNullOrEmpty()) { url.addPathSegments("page/$page") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt index 4a44392d8..8220b2623 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/VcomycsParser.kt @@ -53,11 +53,10 @@ internal class VcomycsParser(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { if (page > 1) return emptyList() - val payload = "action=searchtax&keyword=${query.urlEncoded()}" + val payload = "action=searchtax&keyword=${filter.query.urlEncoded()}" return webClient.httpPost("/wp-admin/admin-ajax.php".toAbsoluteUrl(domain), payload) .parseJson().getJSONArray("data") .mapJSONNotNull { jo -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt index 7c25a8444..fb6978f72 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YuriGardenParser.kt @@ -86,8 +86,6 @@ internal abstract class YuriGardenParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query - val author = filter.author val url = buildString { append("https://") append(apiSuffix) @@ -106,9 +104,9 @@ internal abstract class YuriGardenParser( else -> "newest" // default }) - if (!query.isNullOrEmpty()) { + if (!filter.query.isNullOrEmpty()) { append("&search=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } filter.states.oneOrThrowIfMany()?.let { state -> @@ -130,14 +128,14 @@ internal abstract class YuriGardenParser( append(filter.tags.joinToString(separator = ",") { it.key }) } - if (!author.isNullOrEmpty()) { + if (!filter.author.isNullOrEmpty()) { clear() append("https://") append(apiSuffix) append("/creators/authors/") append( - author.substringAfter("(").substringBefore(")") + filter.author.substringAfter("(").substringBefore(")") ) return@buildString // end of buildString diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt index 21bdc8874..72aead355 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/YurinekoParser.kt @@ -59,10 +59,9 @@ internal class YurinekoParser(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val listUrl = when { - !query.isNullOrEmpty() -> { - "/search?query=${query.urlEncoded()}&page=$page" + !filter.query.isNullOrEmpty() -> { + "/search?query=${filter.query.urlEncoded()}&page=$page" } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt index 26e67d7c8..20a97317a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vmp/VmpParser.kt @@ -42,19 +42,18 @@ internal abstract class VmpParser( protected open val geneUrl = "genero/" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) append('/') when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append(listUrl) append("/page/") append(page.toString()) append("?s=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt index 178518096..63dfdadd4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt @@ -81,16 +81,15 @@ internal abstract class WpComicsParser( protected open val datePattern = "dd/MM/yy" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append(listUrl) append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt index 045f7ac00..70cd7406e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt @@ -50,14 +50,13 @@ internal class XoxoComics(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append("/search-comic?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt index 498f31840..30320350c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/ja/MangaRaw.kt @@ -15,15 +15,14 @@ internal class MangaRaw(context: MangaLoaderContext) : override val listUrl = "/search/manga" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append(listUrl) append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) append("&page=") append(page.toString()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt index 867ec568a..3fd8e2701 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt @@ -35,7 +35,6 @@ internal class DocTruyen3Q(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -62,8 +61,8 @@ internal class DocTruyen3Q(context: MangaLoaderContext) : } } - if (query?.isNotEmpty() == true) { - params.add("keyword=${query.urlEncoded()}") + if (filter.query?.isNotEmpty() == true) { + params.add("keyword=${filter.query.urlEncoded()}") } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt index 15895d1f7..9e891b48b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/MeHentaiVN.kt @@ -35,17 +35,16 @@ internal class MeHentaiVN(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { // url template: https://www.mehentaivn.xyz/tim-truyen?keyword=${query} - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) append(listUrl) append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) if (page > 1) { append("&page=$page") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt index a47405e44..4f912a30f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen1975.kt @@ -40,10 +40,9 @@ internal class NetTruyen1975(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -52,7 +51,7 @@ internal class NetTruyen1975(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt index fffe42926..c590fa9f2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenFE.kt @@ -40,10 +40,9 @@ internal class NetTruyenFE(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -52,7 +51,7 @@ internal class NetTruyenFE(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt index 9c09b6234..bbdf27370 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenHE.kt @@ -36,10 +36,9 @@ internal class NetTruyenHE(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -48,7 +47,7 @@ internal class NetTruyenHE(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt index 41835faa7..f34569aff 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenLL.kt @@ -40,10 +40,9 @@ internal class NetTruyenLL(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -52,7 +51,7 @@ internal class NetTruyenLL(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt index 2841126c6..35dce2270 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenSSR.kt @@ -40,10 +40,9 @@ internal class NetTruyenSSR(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -52,7 +51,7 @@ internal class NetTruyenSSR(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt index 305c8558f..15f46d64c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenUU.kt @@ -40,10 +40,9 @@ internal class NetTruyenUU(context: MangaLoaderContext) : ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val response = when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { val url = buildString { append("https://") append(domain) @@ -52,7 +51,7 @@ internal class NetTruyenUU(context: MangaLoaderContext) : append(page.toString()) append('/') append("?keyword=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } val result = runCatchingCancellable { webClient.httpGet(url) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt index 5d41066b0..509b4a3e9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyen.kt @@ -35,7 +35,6 @@ internal class TopTruyen(context: MangaLoaderContext) : } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -62,8 +61,8 @@ internal class TopTruyen(context: MangaLoaderContext) : } } - if (query?.isNotEmpty() == true) { - params.add("keyword=${query.urlEncoded()}") + if (filter.query?.isNotEmpty() == true) { + params.add("keyword=${filter.query.urlEncoded()}") } if (page > 1) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt index f4381a5ba..168cece52 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zeistmanga/ZeistMangaParser.kt @@ -90,7 +90,6 @@ internal abstract class ZeistMangaParser( protected open val datePattern = "yyyy-MM-dd" override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val startIndex = maxMangaResults * (page - 1) + 1 val url = buildString { @@ -99,7 +98,7 @@ internal abstract class ZeistMangaParser( append("/feeds/posts/default/-/") when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { append(mangaCategory) append("?alt=json&orderby=published&max-results=") append((maxMangaResults + 1).toString()) @@ -108,7 +107,7 @@ internal abstract class ZeistMangaParser( append("&q=label:") append(mangaCategory) append("+") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } else -> { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt index 956ff117a..3be5286d6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/Baozimh.kt @@ -46,15 +46,14 @@ internal class Baozimh(context: MangaLoaderContext) : private val tagsMap = suspendLazy(initializer = ::parseTags) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query when { - !query.isNullOrEmpty() -> { + !filter.query.isNullOrEmpty() -> { if (page > 1) return emptyList() val url = buildString { append("https://") append(domain) append("/search?q=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } return parseMangaListSearch(webClient.httpGet(url).parseHtml()) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt index fb0276c23..e6e127723 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/KomiicParser.kt @@ -141,7 +141,6 @@ internal class KomiicParser(context: MangaLoaderContext) : .build() override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val offset = (page - paginator.firstPage) * pageSize val sexyLevel = parseSexyLevel(filter) // 严格使用原始查询:始终将 sexyLevel 传递给远端(若未选择则为 null) @@ -158,10 +157,10 @@ internal class KomiicParser(context: MangaLoaderContext) : else -> "DATE_UPDATED" } - val base = if (!query.isNullOrEmpty()) { - val list = search(query) + val base = if (!filter.query.isNullOrEmpty()) { + val list = search(filter.query) if (page == paginator.firstPage) { - searchFirstPageCacheQuery = query + searchFirstPageCacheQuery = filter.query searchFirstPageCache = list } list diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt index d04941994..4a6d65f6c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zh/ManhuaguiParser.kt @@ -162,7 +162,6 @@ internal class ManhuaguiParser(context: MangaLoaderContext) : order: SortOrder, filter: MangaListFilter, ): List { - val query = filter.query // Flag of whether there is title query param var flagHasTitleQuery = false @@ -171,7 +170,7 @@ internal class ManhuaguiParser(context: MangaLoaderContext) : var queryFull: String? var orderStr: String? var pageStr: String? - if (query == null) { + if (filter.query == null) { append(listUrl.toAbsoluteUrl(domain)) queryFull = listOfNotNull( filter.locale, @@ -190,7 +189,7 @@ internal class ManhuaguiParser(context: MangaLoaderContext) : } else { flagHasTitleQuery = true append(searchUrl.toAbsoluteUrl(domain)) - queryFull = query + queryFull = filter.query orderStr = when (order) { SortOrder.POPULARITY -> "_o1" SortOrder.NEWEST -> "_o2" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt index a97e62071..f6bb36b20 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/zmanga/ZMangaParser.kt @@ -78,7 +78,6 @@ internal abstract class ZMangaParser( // https://komikindo.info/advanced-search/?title=the&author=the&artist=the&yearx=2020&status=ongoing&type=Manga&order=update override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val query = filter.query val url = buildString { append("https://") append(domain) @@ -101,15 +100,15 @@ internal abstract class ZMangaParser( else -> append("update") } - query?.let { query -> + filter.query?.let { append("&title=") - append(query.urlEncoded()) + append(filter.query.urlEncoded()) } // author - // author?.let { author -> + // filter.author?.let { // append("&author=") - // append(author.urlEncoded()) + // append(filter.author.urlEncoded()) // } // artist diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt new file mode 100644 index 000000000..35212e304 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Assert.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.parsers.util + +internal fun T?.assertNotNull(name: String): T? { + assert(this != null) { + "Value $name is null" + } + return this +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt new file mode 100644 index 000000000..ecbf8cbf6 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CSSBackground.kt @@ -0,0 +1,52 @@ +package org.koitharu.kotatsu.core.parser + +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.util.attrOrNull +import org.koitharu.kotatsu.parsers.util.nullIfEmpty +import org.koitharu.kotatsu.parsers.util.splitByWhitespace + +/** + * Utility class for parsing the `background` property of css + */ +public class CSSBackground private constructor( + public val url: String, + public val left: Int, + public val top: Int, + public val width: Int, + public val height: Int, +) { + + public val right: Int + get() = left + width + + public val bottom: Int + get() = top + height + + internal companion object { + + fun parse(element: Element): CSSBackground? { + val style = element.attrOrNull("style") ?: return null + val attrs = style.split(';').associate { + val trimmed = it.trim() + trimmed.substringBefore(':') to trimmed.substringAfter(':', "") + } + val width = attrs["width"]?.toPx() ?: return null + val height = attrs["height"]?.toPx() ?: return null + val bg = attrs["background"]?.substringAfter("url")?.splitByWhitespace() ?: return null + val url = bg.firstOrNull()?.removeSurrounding("(", ")")?.nullIfEmpty() ?: return null + val x = bg.getOrNull(1)?.toPx() ?: 0 + val y = bg.getOrNull(2)?.toPx() ?: 0 + return CSSBackground( + url = url, + left = -x, + top = y, + width = width, + height = height, + ) + } + + private fun String.toPx(): Int? { + return removeSuffix("px").toIntOrNull() + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt new file mode 100644 index 000000000..afad7166d --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Chapters.kt @@ -0,0 +1,75 @@ +package org.koitharu.kotatsu.parsers.util + +import org.json.JSONArray +import org.json.JSONObject +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.json.asTypedList + +@InternalParsersApi +public inline fun List.mapChapters( + reversed: Boolean = false, + transform: (index: Int, T) -> MangaChapter?, +): List { + val builder = ChaptersListBuilder(collectionSize()) + var index = 0 + val elements = if (reversed) this.asReversed() else this + for (item in elements) { + if (builder.add(transform(index, item))) { + index++ + } + } + return builder.toList() +} + +@InternalParsersApi +public inline fun JSONArray.mapChapters( + reversed: Boolean = false, + transform: (index: Int, JSONObject) -> MangaChapter?, +): List = asTypedList().mapChapters(reversed, transform) + +@InternalParsersApi +public inline fun List.flatMapChapters( + reversed: Boolean = false, + transform: (T) -> Iterable, +): List { + val builder = ChaptersListBuilder(collectionSize()) + val elements = if (reversed) this.asReversed() else this + for (item in elements) { + builder.addAll(transform(item)) + } + return builder.toList() +} + +@PublishedApi +internal fun Iterable.collectionSize(): Int { + return if (this is Collection<*>) this.size else 10 +} + +@PublishedApi +internal class ChaptersListBuilder(initialSize: Int) { + + private val ids = HashSet(initialSize) + private val list = ArrayList(initialSize) + + fun add(chapter: MangaChapter?): Boolean { + return chapter != null && ids.add(chapter.id) && list.add(chapter) + } + + fun addAll(chapters: Iterable) { + if (chapters is Collection<*>) { + list.ensureCapacity(list.size + chapters.size) + } + chapters.forEach { add(it) } + } + + operator fun plusAssign(chapter: MangaChapter?) { + add(chapter) + } + + fun reverse() { + list.reverse() + } + + fun toList(): List = list +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt new file mode 100644 index 000000000..38d0dfa7b --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt @@ -0,0 +1,101 @@ +@file:JvmName("CollectionUtils") + +package org.koitharu.kotatsu.parsers.util + +import androidx.collection.ArrayMap +import androidx.collection.ArraySet +import java.util.* + +public fun Collection<*>?.sizeOrZero(): Int = this?.size ?: 0 + +public fun MutableCollection.replaceWith(subject: Iterable) { + clear() + addAll(subject) +} + +public fun > Iterable>.flattenTo(destination: C): C { + for (element in this) { + destination.addAll(element) + } + return destination +} + +public fun List.medianOrNull(): T? = when { + isEmpty() -> null + else -> get((size / 2).coerceIn(indices)) +} + +public inline fun Collection.mapToSet(transform: (T) -> R): Set { + return mapTo(ArraySet(size), transform) +} + +public inline fun Collection.mapNotNullToSet(transform: (T) -> R?): Set { + val destination = ArraySet(size) + for (item in this) { + destination.add(transform(item) ?: continue) + } + return destination +} + +public inline fun Array.mapToArray(transform: (T) -> R): Array = Array(size) { i -> + transform(get(i)) +} + +public fun List>.toMutableMap(): MutableMap = toMap(ArrayMap(size)) + +public fun MutableList.move(sourceIndex: Int, targetIndex: Int) { + if (sourceIndex <= targetIndex) { + Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) + } else { + Collections.rotate(subList(targetIndex, sourceIndex + 1), 1) + } +} + +public fun Iterator.nextOrNull(): T? = if (hasNext()) next() else null + +public inline fun Collection.associateGrouping(transform: (T) -> Pair): Map> { + val result = LinkedHashMap>(size) + for (item in this) { + val (k, v) = transform(item) + result.getOrPut(k) { LinkedHashSet() }.add(v) + } + return result +} + +public fun MutableMap.incrementAndGet(key: K): Int { + var value = get(key) ?: 0 + value++ + put(key, value) + return value +} + +public inline fun MutableSet(size: Int, init: (index: Int) -> T): MutableSet { + val set = ArraySet(size) + repeat(size) { index -> set.add(init(index)) } + return set +} + +public inline fun Set(size: Int, init: (index: Int) -> T): Set = when (size) { + 0 -> emptySet() + 1 -> Collections.singleton(init(0)) + else -> MutableSet(size, init) +} + +@Suppress("UNCHECKED_CAST") +public inline fun Collection.mapToArray(transform: (T) -> R): Array { + val result = arrayOfNulls(size) + forEachIndexed { index, t -> result[index] = transform(t) } + return result as Array +} + +public fun Array.toArraySet(): Set = when (size) { + 0 -> emptySet() + 1 -> setOf(first()) + else -> ArraySet(this) +} + +public fun Collection.toArraySet(): Set = when (size) { + 0 -> emptySet() + 1 -> setOf(first()) + else -> ArraySet(this) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt new file mode 100644 index 000000000..c24521449 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/ContinuationCallCallback.kt @@ -0,0 +1,34 @@ +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CompletionHandler +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import okhttp3.internal.closeQuietly +import java.io.IOException +import kotlin.coroutines.resumeWithException + +internal class ContinuationCallCallback( + private val call: Call, + private val continuation: CancellableContinuation, +) : Callback, CompletionHandler { + + override fun onResponse(call: Call, response: Response) { + continuation.resume(response) { _, value, _ -> + value.closeQuietly() + } + } + + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun invoke(cause: Throwable?) { + runCatching { + call.cancel() + }.onFailure { e -> + cause?.addSuppressed(e) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt new file mode 100644 index 000000000..b3534c880 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt @@ -0,0 +1,48 @@ +@file:JvmName("CookieJarUtils") + +package org.koitharu.kotatsu.parsers.util + +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +public fun CookieJar.insertCookies(domain: String, vararg cookies: String) { + val url = safeUrlOf(domain) ?: return + saveFromResponse( + url, + cookies.mapNotNull { + Cookie.parse(url, it) + }, + ) +} + +public fun CookieJar.insertCookie(domain: String, cookie: Cookie) { + val url = safeUrlOf(domain) ?: return + saveFromResponse(url, listOf(cookie)) +} + +public fun CookieJar.getCookies(domain: String): List { + val url = safeUrlOf(domain) ?: return emptyList() + return loadForRequest(url) +} + +public fun CookieJar.copyCookies(oldDomain: String, newDomain: String, names: Array? = null) { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(oldDomain) + var cookies = loadForRequest(url.build()) + if (names != null) { + cookies = cookies.filter { c -> c.name in names } + } + url.host(newDomain) + saveFromResponse(url.build(), cookies) +} + +private fun safeUrlOf(domain: String): HttpUrl? = try { + HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(domain) + .build() +} catch (_: IllegalArgumentException) { + null +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt new file mode 100644 index 000000000..0a902c1aa --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Coroutines.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +public fun Iterable.cancelAll(cause: CancellationException? = null) { + forEach { it.cancel(cause) } +} + +public suspend fun Iterable>.awaitFirst(): T { + return channelFlow { + for (deferred in this@awaitFirst) { + launch { + send(deferred.await()) + } + } + }.first().also { this@awaitFirst.cancelAll() } +} + +public suspend fun Collection>.awaitFirst(condition: (T) -> Boolean): T { + return channelFlow { + for (deferred in this@awaitFirst) { + launch { + val result = deferred.await() + if (condition(result)) { + send(result) + } + } + } + }.first().also { this@awaitFirst.cancelAll() } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt new file mode 100644 index 000000000..d615d14a8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt @@ -0,0 +1,127 @@ +package org.koitharu.kotatsu.parsers.util + +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING" +private const val AES = "AES" +private const val KDF_DIGEST = "MD5" + +/** + * Conforming with CryptoJS AES method + */ +@InternalParsersApi +public class CryptoAES( + private val context: MangaLoaderContext, +) { + + /** + * Decrypt using CryptoJS defaults compatible method. + * Uses KDF equivalent to OpenSSL's EVP_BytesToKey function + * + * http://stackoverflow.com/a/29152379/4405051 + * @param cipherText base64 encoded ciphertext + * @param password passphrase + */ + @Throws(Exception::class) + public fun decrypt(cipherText: String, password: String): String { + val ctBytes = context.decodeBase64(cipherText) + val saltBytes = ctBytes.copyOfRange(8, 16) + val cipherTextBytes = ctBytes.copyOfRange(16, ctBytes.size) + val md5: MessageDigest = MessageDigest.getInstance(KDF_DIGEST) + val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5) + return decryptAES( + cipherTextBytes, + keyAndIV.getOrNull(0) ?: ByteArray(32), + keyAndIV.getOrNull(1) ?: ByteArray(16), + ) + } + + /** + * Decrypt using CryptoJS defaults compatible method. + * + * @param cipherText base64 encoded ciphertext + * @param keyBytes key as a bytearray + * @param ivBytes iv as a bytearray + */ + @Throws(Exception::class) + public fun decrypt(cipherText: String, keyBytes: ByteArray, ivBytes: ByteArray): String { + val cipherTextBytes = context.decodeBase64(cipherText) + return decryptAES(cipherTextBytes, keyBytes, ivBytes) + } + + /** + * Decrypt using CryptoJS defaults compatible method. + * + * @param cipherTextBytes encrypted text as a bytearray + * @param keyBytes key as a bytearray + * @param ivBytes iv as a bytearray + */ + @Throws(Exception::class) + private fun decryptAES(cipherTextBytes: ByteArray, keyBytes: ByteArray, ivBytes: ByteArray): String { + val cipher = Cipher.getInstance(HASH_CIPHER) + val keyS = SecretKeySpec(keyBytes, AES) + cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(ivBytes)) + return cipher.doFinal(cipherTextBytes).toString(Charsets.UTF_8) + } + + /** + * Generates a key and an initialization vector (IV) with the given salt and password. + * + * https://stackoverflow.com/a/41434590 + * This method is equivalent to OpenSSL's EVP_BytesToKey function + * (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c). + * By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data. + * + * @param keyLength the length of the generated key (in bytes) + * @param ivLength the length of the generated IV (in bytes) + * @param iterations the number of digestion rounds + * @param salt the salt data (8 bytes of data or `null`) + * @param password the password data (optional) + * @param md the message digest algorithm to use + * @return an two-element array with the generated key and IV + */ + @Suppress("SameParameterValue") + @Throws(Exception::class) + private fun generateKeyAndIV( + keyLength: Int, + ivLength: Int, + iterations: Int, + salt: ByteArray?, + password: ByteArray, + md: MessageDigest, + ): Array { + val digestLength = md.digestLength + val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + md.reset() + + // Repeat process until sufficient data has been generated + while (generatedLength < keyLength + ivLength) { + + // Digest data (last digest if available, password data, salt if available) + if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength) + md.update(password) + if (salt != null) md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + // additional rounds + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + generatedLength += digestLength + } + + // Copy key and IV into separate byte arrays + val result = arrayOfNulls(2) + result[0] = generatedData.copyOfRange(0, keyLength) + if (ivLength > 0) result[1] = generatedData.copyOfRange(keyLength, keyLength + ivLength) + return result + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt new file mode 100644 index 000000000..2cb7cfb23 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Enum.kt @@ -0,0 +1,13 @@ +@file:JvmName("EnumUtils") + +package org.koitharu.kotatsu.parsers.util + +import kotlin.enums.EnumEntries + +public fun > EnumEntries.names(): Array = Array(size) { i -> + get(i).name +} + +public fun > EnumEntries.find(name: String): E? { + return find { x -> x.name == name } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt new file mode 100644 index 000000000..b01fc794d --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/FaviconParser.kt @@ -0,0 +1,92 @@ +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.model.Favicon +import org.koitharu.kotatsu.parsers.model.Favicons +import org.koitharu.kotatsu.parsers.network.WebClient +import org.koitharu.kotatsu.parsers.util.json.mapJSON + +public class FaviconParser( + private val webClient: WebClient, + private val domain: String, +) { + + public suspend fun parseFavicons(): Favicons = withContext(Dispatchers.Default) { + val url = "https://$domain" + val doc = webClient.httpGet(url).parseHtml() + val result = HashSet() + val manifestLink = doc.getElementsByAttributeValue("rel", "manifest").firstOrNull() + ?.attrAsAbsoluteUrlOrNull("href") + if (manifestLink != null) { + runCatchingCancellable { + parseManifest(manifestLink) + }.onSuccess { manifest -> + result += manifest + } + } + val links = doc.getElementsByAttributeValueContaining("rel", "icon") + links.mapNotNullTo(result) { link -> + parseLink(link) + } + val touchIcons = doc.getElementsByAttributeValue("rel", "apple-touch-icon") + touchIcons.mapNotNullTo(result) { link -> + parseLink(link) + } + if (result.isEmpty()) { + result.add(createFallback()) + } + Favicons(result, url) + } + + private fun parseLink(link: Element): Favicon? { + val href = link.attrAsAbsoluteUrlOrNull("href") + if (href == null || href.endsWith('/')) { + return null + } + val sizes = link.attr("sizes") + return Favicon( + url = href, + size = parseSize(sizes), + rel = link.attrOrNull("rel"), + ) + } + + private fun parseSize(sizes: String): Int { + if (sizes.isEmpty() || sizes == "any") { + return 0 + } + return sizes.substringBefore(' ') + .split('x', 'X', '*') + .firstNotNullOfOrNull { it.toIntOrNull() } + ?: 0 + } + + private suspend fun parseManifest(url: String): List { + val json = webClient.httpGet(url).parseJson() + val icons = json.optJSONArray("icons") ?: return emptyList() + return icons.mapJSON { jo -> + Favicon( + url = jo.getString("src").resolveLink(), + size = parseSize(jo.getString("sizes")), + rel = null, + ) + } + } + + private fun createFallback(): Favicon { + val href = "https://$domain/favicon.ico" + return Favicon( + url = href, + size = 0, + rel = null, + ) + } + + private fun String.resolveLink(): String = when { + startsWith("http:") || startsWith("https:") -> this + startsWith('/') -> "https://$domain$this" + else -> "https://$domain/$this" + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt new file mode 100644 index 000000000..b9bdb0286 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Jsoup.kt @@ -0,0 +1,225 @@ +@file:JvmName("JsoupUtils") + +package org.koitharu.kotatsu.parsers.util + +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.jsoup.nodes.Element +import org.jsoup.select.Elements +import org.jsoup.select.QueryParser +import org.jsoup.select.Selector +import org.koitharu.kotatsu.core.parser.CSSBackground +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.exception.ParseException +import kotlin.contracts.contract + +public val Element.host: String? + get() { + val uri = baseUri() + return if (uri.isEmpty()) { + null + } else { + uri.toHttpUrlOrNull()?.host + } + } + +/** + * Return an attribute value or null if it is missing or empty + * @see [Element.attr] which returns empty string instead of null + */ +public fun Element.attrOrNull(attributeKey: String): String? = attr(attributeKey).takeUnless { it.isBlank() }?.trim() + +/** + * Return an attribute value or throw an exception if it is missing + * @see [Element.attr] which returns empty string instead + */ +public fun Element.attrOrThrow(attributeKey: String): String = if (hasAttr(attributeKey)) { + attr(attributeKey) +} else { + throw ParseException("Attribute \"$attributeKey\" is missing at element \"$this\"", baseUri()) +} + +/** + * Return an attribute value as relative url or null if it is missing or empty + * @see attrAsRelativeUrl + * @see attrAsAbsoluteUrlOrNull + * @see attrAsAbsoluteUrl + */ +public fun Element.attrAsRelativeUrlOrNull(attributeKey: String): String? { + val attr = attrOrNull(attributeKey) ?: return null + if (attr.isEmpty() || attr.startsWith("data:")) { + return null + } + if (attr.startsWith('/')) { + return attr + } + val host = baseUri().toHttpUrlOrNull()?.host ?: return null + return attr.substringAfter(host) +} + +/** + * Return an attribute value as relative url or throw an exception if it is missing or empty + * @throws IllegalArgumentException if attribute value is missing or empty + * @see attrAsRelativeUrlOrNull + * @see attrAsAbsoluteUrlOrNull + * @see attrAsAbsoluteUrl + */ +public fun Element.attrAsRelativeUrl(attributeKey: String): String { + return requireNotNull(attrAsRelativeUrlOrNull(attributeKey)) { + "Cannot get relative url for $attributeKey: \"${attr(attributeKey)}\"" + } +} + +/** + * Return an attribute value as absolute url or null if it is missing or empty + * @see attrAsAbsoluteUrl + * @see attrAsRelativeUrl + * @see attrAsRelativeUrlOrNull + */ +public fun Element.attrAsAbsoluteUrlOrNull(attributeKey: String): String? { + val attr = attrOrNull(attributeKey) ?: return null + if (attr.isEmpty() || attr.startsWith("data:")) { + return null + } + return (baseUri().toHttpUrlOrNull()?.resolve(attr) ?: return null).toString() +} + +/** + * Return an attribute value as absolute url or throw an exception if it is missing or empty + * @throws IllegalArgumentException if attribute value is missing or empty + * @see attrAsAbsoluteUrlOrNull + * @see attrAsRelativeUrl + * @see attrAsRelativeUrlOrNull + */ +public fun Element.attrAsAbsoluteUrl(attributeKey: String): String { + return parseNotNull(attrAsAbsoluteUrlOrNull(attributeKey)) { + "Cannot get absolute url for $attributeKey: \"${attr(attributeKey)}\"" + } +} + +/** + * Return css value from `style` attribute or null if it is missing + */ +public fun Element.styleValueOrNull(property: String): String? { + val regex = Regex("${Regex.escape(property)}\\s*:\\s*[^;]+") + val css = attr("style").find(regex) ?: return null + return css.substringAfter(':').removeSuffix(';').trim() +} + +/** + * Like a `expectFirst` but with detailed error message + */ +public fun Element.selectFirstOrThrow(cssQuery: String): Element = parseNotNull(Selector.selectFirst(cssQuery, this)) { + "Cannot find \"$cssQuery\"" +} + +public fun Element.selectOrThrow(cssQuery: String): Elements { + return Selector.select(cssQuery, this).ifEmpty { + throw ParseException("Empty result for \"$cssQuery\"", baseUri()) + } +} + +public fun Element.requireElementById(id: String): Element = parseNotNull(getElementById(id)) { + "Cannot find \"#$id\"" +} + +public fun Element.selectLast(cssQuery: String): Element? = select(cssQuery).lastOrNull() + +public fun Element.selectLastOrThrow(cssQuery: String): Element = parseNotNull(selectLast(cssQuery)) { + "Cannot find \"$cssQuery\"" +} + +public fun Element.textOrNull(): String? = text().nullIfEmpty() + +public fun Elements.textOrNull(): String? = text().nullIfEmpty() + +public fun Element.ownTextOrNull(): String? = ownText().nullIfEmpty() + +public fun Element.selectFirstParent(query: String): Element? { + val selector = QueryParser.parse(query) + val parents = parents() + val root = parents.lastOrNull() ?: return null + return parents.firstOrNull { + selector.matches(root, it) + } +} + +public fun Element.selectFirstParentOrThrow(query: String): Element = parseNotNull(selectFirstParent(query)) { + "Cannot find parent \"$query\"" +} + +/** + * Return a first non-empty attribute value of [names] or null if it is missing or empty + */ +public fun Element.attrOrNull(vararg names: String): String? { + for (name in names) { + val value = attr(name) + if (value.isNotEmpty()) { + return value.trim() + } + } + return null +} + +@InternalParsersApi +@JvmOverloads +public fun Element.src( + names: Array = arrayOf( + "data-src", + "data-cfsrc", + "data-original", + "data-original-src", + "data-cdn", + "data-sizes", + "data-lazy-src", + "data-srcset", + "original-src", + "data-wpfc-original-src", + "src", + ), +): String? { + for (name in names) { + val value = attrAsAbsoluteUrlOrNull(name) + if (value != null) { + return value + } + } + return null +} + +@InternalParsersApi +public fun Element.requireSrc(): String = parseNotNull(src()) { + "Image src not found" +} + +public fun Element.backgroundOrNull(): CSSBackground? = CSSBackground.parse(this) + +public fun Element.metaValue(itemprop: String): String? = getElementsByAttributeValue("itemprop", itemprop) + .firstNotNullOfOrNull { element -> + element.attrOrNull("content") + } + +public fun String.cssUrl(): String? { + val fromIndex = indexOf("url(") + if (fromIndex == -1) { + return null + } + val toIndex = indexOf(')', startIndex = fromIndex) + return if (toIndex == -1) { + null + } else { + substring(fromIndex + 4, toIndex).trim() + } +} + +internal inline fun Element.parseNotNull(value: T?, lazyMessage: () -> String): T { + contract { + returns() implies (value != null) + } + + if (value == null) { + val message = lazyMessage() + throw ParseException(message, baseUri()) + } else { + return value + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt index f5c3a85ae..cb542f7d3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolver.kt @@ -20,9 +20,8 @@ public class LinkResolver internal constructor( public suspend fun getSource(): MangaParserSource? = source.get() public suspend fun getManga(): Manga? { - val parserSource = source.get() ?: return null - val parser = context.newParserInstance(parserSource) - return parser.resolveLink(link) + val parser = context.newParserInstance(source.get() ?: return null) + return parser.resolveLink(this, link) ?: resolveManga(parser) } private suspend fun resolveSource(): MangaParserSource? = runInterruptible(Dispatchers.Default) { @@ -38,7 +37,7 @@ public class LinkResolver internal constructor( null } - public suspend fun resolveManga( + internal suspend fun resolveManga( parser: MangaParser, url: String = link.toString().toRelativeUrl(link.host), id: Long = parser.generateUid(url), @@ -75,14 +74,14 @@ public class LinkResolver internal constructor( seed.authors.isNotEmpty() -> seed.authors.first() else -> return seed // unfortunately we do not know a real manga title so unable to find it } - val resolved = runCatching { + val resolved = runCatchingCancellable { val list = parser.getList(0, parser.bestSortOrder(), MangaListFilter(query = query)) list.singleOrNull { manga -> isSameUrl(manga.publicUrl) } }.getOrNull() if (resolved == null) { return seed } - return runCatching { + return runCatchingCancellable { parser.getDetails(resolved) }.getOrElse { resolved.copy( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt new file mode 100644 index 000000000..7f34cd8de --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt @@ -0,0 +1,96 @@ +package org.koitharu.kotatsu.parsers.util + +import okhttp3.HttpUrl +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.ErrorMessages +import org.koitharu.kotatsu.parsers.InternalParsersApi +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.core.AbstractMangaParser +import org.koitharu.kotatsu.parsers.exception.ParseException +import org.koitharu.kotatsu.parsers.model.* + + +/** + * Create a unique id for [Manga]/[MangaChapter]/[MangaPage]. + * @param url must be relative url, without a domain + * @see [Manga.id] + * @see [MangaChapter.id] + * @see [MangaPage.id] + */ +@InternalParsersApi +public fun MangaParser.generateUid(url: String): Long { + var h = LONG_HASH_SEED + source.name.forEach { c -> + h = 31 * h + c.code + } + url.forEach { c -> + h = 31 * h + c.code + } + return h +} + +/** + * Create a unique id for [Manga]/[MangaChapter]/[MangaPage]. + * @param id an internal identifier + * @see [Manga.id] + * @see [MangaChapter.id] + * @see [MangaPage.id] + */ +@InternalParsersApi +public fun MangaParser.generateUid(id: Long): Long { + var h = LONG_HASH_SEED + source.name.forEach { c -> + h = 31 * h + c.code + } + h = 31 * h + id + return h +} + +@InternalParsersApi +public fun Element.parseFailed(message: String? = null): Nothing { + throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null) +} + +@InternalParsersApi +public fun Set?.oneOrThrowIfMany(): MangaTag? = oneOrThrowIfMany( + ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED, +) + +@InternalParsersApi +public fun Set?.oneOrThrowIfMany(): MangaState? = oneOrThrowIfMany( + ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED, +) + +@InternalParsersApi +public fun Set?.oneOrThrowIfMany(): ContentType? = oneOrThrowIfMany( + ErrorMessages.FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED, +) + +@InternalParsersApi +public fun Set?.oneOrThrowIfMany(): Demographic? = oneOrThrowIfMany( + ErrorMessages.FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED, +) + +@InternalParsersApi +public fun Set?.oneOrThrowIfMany(): ContentRating? = oneOrThrowIfMany( + ErrorMessages.FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED, +) + +private fun Set?.oneOrThrowIfMany(msg: String): T? = when { + isNullOrEmpty() -> null + size == 1 -> first() + else -> throw IllegalArgumentException(msg) +} + +@InternalParsersApi +public fun AbstractMangaParser.getDomain(subdomain: String): String { + val domain = domain + return subdomain + "." + domain.removePrefix("www.") +} + +@InternalParsersApi +public fun MangaParser.urlBuilder(subdomain: String? = null): HttpUrl.Builder { + return HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(if (subdomain == null) domain else "$subdomain.$domain") +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt deleted file mode 100644 index e5e9798d8..000000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserExt.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.koitharu.kotatsu.parsers.util - -import org.koitharu.kotatsu.parsers.MangaParser -import org.koitharu.kotatsu.parsers.model.* - -/** - * Extension properties to help with smart-casting across modules. - * These return a local copy of the property value. - */ -public val MangaListFilter.queryOrNull: String? - get() = query - -public val MangaListFilter.authorOrNull: String? - get() = author - -public inline fun T?.ifNotNull(block: (T) -> Unit) { - if (this != null) block(this) -} - -public fun String?.nullIfEmpty(): String? = if (isNullOrEmpty()) null else this diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt new file mode 100644 index 000000000..3c63a6669 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParsersUtils.kt @@ -0,0 +1,18 @@ +@file:JvmName("MangaParsersUtils") + +package org.koitharu.kotatsu.parsers.util + +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import kotlin.contracts.contract + +public fun MangaListFilter?.isNullOrEmpty(): Boolean { + contract { + returns(false) implies (this@isNullOrEmpty != null) + } + return this == null || this.isEmpty() +} + +public fun Collection.findById(chapterId: Long): MangaChapter? = find { x -> + x.id == chapterId +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt new file mode 100644 index 000000000..53227fecc --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt @@ -0,0 +1,85 @@ +@file:JvmName("NumberUtils") + +package org.koitharu.kotatsu.parsers.util + +import java.text.DecimalFormat +import java.text.NumberFormat +import java.util.* +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.math.absoluteValue + +public fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String { + val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat + val symbols = formatter.decimalFormatSymbols + if (thousandsSep != null) { + symbols.groupingSeparator = thousandsSep + formatter.isGroupingUsed = true + } else { + formatter.isGroupingUsed = false + } + symbols.decimalSeparator = decPoint + formatter.decimalFormatSymbols = symbols + formatter.minimumFractionDigits = decimals + formatter.maximumFractionDigits = decimals + return when (this) { + is Float, + is Double, + -> formatter.format(this.toDouble()) + + else -> formatter.format(this.toLong()) + } +} + +public fun Float.toIntUp(): Int { + val intValue = toInt() + return if ((this - intValue.toFloat()).absoluteValue <= 0.00001) { + intValue + } else { + intValue + 1 + } +} + +public infix fun Int.upBy(step: Int): Int { + val mod = this % step + return if (mod == 0) { + this + } else { + this - mod + step + } +} + +public fun Number.formatSimple(): String { + val raw = toString() + return if (raw.endsWith(".0") || raw.endsWith(",0")) { + raw.dropLast(2) + } else { + raw + } +} + +public inline fun Int.ifZero(defaultValue: () -> Int): Int { + contract { + callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) + } + return if (this == 0) { + defaultValue() + } else { + this + } +} + +public inline fun Long.ifZero(defaultValue: () -> Long): Long { + contract { + callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) + } + return if (this == 0L) { + defaultValue() + } else { + this + } +} + +public fun longOf(a: Int, b: Int): Long { + return a.toLong() shl 32 or (b.toLong() and 0xffffffffL) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt new file mode 100644 index 000000000..85d5be4f1 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt @@ -0,0 +1,55 @@ +@file:JvmName("OkHttpUtils") + +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.* +import okhttp3.internal.toLongOrDefault +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +public suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation -> + val callback = ContinuationCallCallback(this, continuation) + continuation.invokeOnCancellation(callback) + enqueue(callback) +} + +public val Response.mimeType: String? + get() = header("content-type")?.substringBefore(';')?.trim()?.nullIfEmpty()?.lowercase() + +public val HttpUrl.isHttpOrHttps: Boolean + get() = scheme.equals("https", ignoreCase = true) || scheme.equals("http", ignoreCase = true) + +public fun Headers.Builder.mergeWith(other: Headers, replaceExisting: Boolean): Headers.Builder { + for ((name, value) in other) { + if (replaceExisting || this[name] == null) { + this[name] = value + } + } + return this +} + +public fun Response.copy(): Response = newBuilder() + .body(peekBody(Long.MAX_VALUE)) + .build() + +public fun Response.Builder.setHeader(name: String, value: String?): Response.Builder = if (value == null) { + removeHeader(name) +} else { + header(name, value) +} + +public inline fun Response.map(mapper: (ResponseBody) -> ResponseBody): Response { + contract { + callsInPlace(mapper, InvocationKind.AT_MOST_ONCE) + } + return body.use { responseBody -> + newBuilder() + .body(mapper(responseBody)) + .build() + } +} + +public fun Response.headersContentLength( + defaultValue: Long = -1, +): Long = headers["Content-Length"]?.toLongOrDefault(defaultValue) ?: defaultValue diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt new file mode 100644 index 000000000..45e15ca6f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Paginator.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.parsers.util + +import androidx.collection.SparseArrayCompat +import androidx.collection.set + +public class Paginator internal constructor(private val initialPageSize: Int) { + + public var firstPage: Int = 1 + private var pages = SparseArrayCompat() + + internal fun getPage(offset: Int): Int { + if (offset == 0) { // just an optimization + return firstPage + } + pages[offset]?.let { return it } + val pageSize = initialPageSize + val intPage = offset / pageSize + val tail = offset % pageSize + return intPage + firstPage + if (tail == 0) 0 else 1 + } + + internal fun onListReceived(offset: Int, page: Int, count: Int) { + pages[offset + count] = if (count > 0) page + 1 else page + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt new file mode 100644 index 000000000..9cc4517b9 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt @@ -0,0 +1,101 @@ +@file:JvmName("ParseUtils") + +package org.koitharu.kotatsu.parsers.util + +import okhttp3.Response +import okhttp3.ResponseBody +import org.json.JSONArray +import org.json.JSONObject +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.koitharu.kotatsu.parsers.InternalParsersApi +import java.text.DateFormat + +private val REGEX_SCHEME_PREFIX = Regex("^\\w{2,6}://", RegexOption.IGNORE_CASE) +internal const val SCHEME_HTTPS = "https" + +/** + * Parse [Response] body as html document using Jsoup + * @see [parseJson] + * @see [parseJsonArray] + */ +// TODO suspend +public fun Response.parseHtml(): Document = use { response -> + val body = response.body + Jsoup.parse(body.string(), response.request.url.toString()) +} + +/** + * Parse [Response] body as [JSONObject] + * @see [parseJsonArray] + * @see [parseHtml] + */ +public fun Response.parseJson(): JSONObject = use { response -> + JSONObject(response.body.string()) +} + +/** + * Parse [Response] body as [JSONArray] + * @see [parseJson] + * @see [parseHtml] + */ +public fun Response.parseJsonArray(): JSONArray = use { response -> + JSONArray(response.body.string()) +} + +public fun Response.parseRaw(): String = use { response -> + response.body.string() +} + +public fun Response.parseBytes(): ByteArray = use { response -> + response.body.bytes() +} + +/** + * Convert url to relative if it is on [domain] + * @return an url relative to the [domain] or absolute, if domain is mismatching + */ +public fun String.toRelativeUrl(domain: String): String { + if (isEmpty() || startsWith("/")) { + return this + } + return replace(Regex("^[^/]{2,6}://${Regex.escape(domain)}+/", RegexOption.IGNORE_CASE), "/") +} + +/** + * Convert url to absolute with specified domain + * @return an absolute url with [domain] if this is relative + */ +public fun String.toAbsoluteUrl(domain: String): String = when { + startsWith("//") -> "$SCHEME_HTTPS:$this" + startsWith('/') -> "$SCHEME_HTTPS://$domain$this" + REGEX_SCHEME_PREFIX.containsMatchIn(this) -> this + else -> "$SCHEME_HTTPS://$domain/$this" +} + +public fun concatUrl(host: String, path: String): String { + val hostWithSlash = host.endsWith('/') + val pathWithSlash = path.startsWith('/') + val hostWithScheme = if (host.startsWith("//")) "https:$host" else host + return when { + hostWithSlash && pathWithSlash -> hostWithScheme + path.drop(1) + !hostWithSlash && !pathWithSlash -> "$hostWithScheme/$path" + else -> hostWithScheme + path + } +} + +@InternalParsersApi +public fun DateFormat.parseSafe(str: String?): Long = if (str.isNullOrEmpty()) { + 0L +} else { + runCatching { + parse(str)?.time ?: 0L + }.onFailure { + if (javaClass.desiredAssertionStatus()) { + throw AssertionError("Cannot parse date $str", it) + } + }.getOrDefault(0L) +} + +@Deprecated("Useless since OkHttp 5.0", replaceWith = ReplaceWith("body")) +public fun Response.requireBody(): ResponseBody = body diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt new file mode 100644 index 000000000..1639eaa85 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RateLimit.kt @@ -0,0 +1,81 @@ +package org.koitharu.kotatsu.parsers.util + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import org.koitharu.kotatsu.parsers.network.RateLimitHelper +import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * An OkHttp interceptor that enforces rate limiting across all requests. + * + * Examples: + * - `permits = 5`, `period = 1.seconds` => 5 requests per second + * - `permits = 10`, `period = 2.minutes` => 10 requests per 2 minutes + * + * @param permits [Int] Number of requests allowed within a period of units. + * @param period [Duration] The limiting duration. Defaults to 1.seconds. + * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information + */ +public fun OkHttpClient.Builder.rateLimit( + permits: Int, + period: Duration = 1.seconds +): OkHttpClient.Builder = rateLimit(permits, period) { true } + +/** + * An OkHttp interceptor that handles given url host's rate limiting. + * + * Examples: + * - `url = "https://api.manga.example"`, `permits = 5`, `period = 1.seconds` => 5 requests per second to any url with host "api.manga.example" + * - `url = "https://cdn.manga.example/image"`, `permits = 10`, `period = 2.minutes` => 10 requests per 2 minutes to any url with host "cdn.manga.example" + * + * @param url [String] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host() + * @param permits [Int] Number of requests allowed within a period of units. + * @param period [Duration] The limiting duration. Defaults to 1.seconds. + * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information + */ +public fun OkHttpClient.Builder.rateLimit( + url: String, + permits: Int, + period: Duration = 1.seconds +): OkHttpClient.Builder = rateLimit(url.toHttpUrl(), permits, period) + +/** + * An OkHttp interceptor that handles given url host's rate limiting. + * + * Examples: + * - `httpUrl = "https://api.manga.example".toHttpUrlOrNull()`, `permits = 5`, `period = 1.seconds` => 5 requests per second to any url with host "api.manga.example" + * - `httpUrl = "https://cdn.manga.example/image".toHttpUrlOrNull()`, `permits = 10`, `period = 2.minutes` => 10 requests per 2 minutes to any url with host "cdn.manga.example + * + * @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host() + * @param permits [Int] Number of requests allowed within a period of units. + * @param period [Duration] The limiting duration. Defaults to 1.seconds. + * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information + */ +public fun OkHttpClient.Builder.rateLimit( + httpUrl: HttpUrl, + permits: Int, + period: Duration = 1.seconds +): OkHttpClient.Builder = rateLimit(permits, period) { it.host == httpUrl.host } + +/** + * An OkHttp interceptor that enforces conditional rate limiting based on a given condition. + * + * Examples: + * - `permits = 5`, `period = 1.seconds`, `shouldLimit = { it.host == "api.manga.example" }` => 5 requests per second to any url with host "api.manga.example". + * - `permits = 10`, `period = 2.minutes`, `shouldLimit = { it.encodedPath.startsWith("/images/") }` => 10 requests per 2 minutes to paths starting with "/images/". + * + * @param permits [Int] Number of requests allowed within a period of units. + * @param period [Duration] The limiting duration. Defaults to 1.seconds. + * @param shouldLimit A predicate to determine whether the rate limit should apply to a given request. + * @throws TooManyRequestExceptions when rate limit is exceeded with retry delay information + */ +public fun OkHttpClient.Builder.rateLimit( + permits: Int, + period: Duration = 1.seconds, + shouldLimit: (HttpUrl) -> Boolean +): OkHttpClient.Builder { + return addInterceptor(RateLimitHelper(permits, period, shouldLimit)) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt new file mode 100644 index 000000000..73e89b42c --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt @@ -0,0 +1,57 @@ +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.* +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.SortOrder + +public class RelatedMangaFinder( + private val parsers: Collection, +) { + + public suspend operator fun invoke(seed: Manga): List = withContext(Dispatchers.Default) { + coroutineScope { + parsers.singleOrNull()?.let { parser -> + findRelatedImpl(this, parser, seed) + } ?: parsers.map { parser -> + async { + findRelatedImpl(this, parser, seed) + } + }.awaitAll().flatten() + } + } + + private suspend fun findRelatedImpl(scope: CoroutineScope, parser: MangaParser, seed: Manga): List { + val words = HashSet() + words += seed.title.splitByWhitespace() + seed.altTitles.forEach { + words += it.splitByWhitespace() + } + if (words.isEmpty()) { + return emptyList() + } + val results = words.map { keyword -> + scope.async { + val result = parser.getList( + 0, + if (SortOrder.RELEVANCE in parser.availableSortOrders) { + SortOrder.RELEVANCE + } else { + parser.availableSortOrders.first() + }, + MangaListFilter( + query = keyword, + ), + ) + result.filter { it.id != seed.id && it.containKeyword(keyword) } + } + }.awaitAll() + return results.minBy { if (it.isEmpty()) Int.MAX_VALUE else it.size } + } + + private fun Manga.containKeyword(keyword: String): Boolean { + return title.contains(keyword, ignoreCase = true) + || altTitles.any { it.contains(keyword, ignoreCase = true) } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt new file mode 100644 index 000000000..3b4bc91aa --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Result.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.CancellationException +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@Suppress("WRONG_INVOCATION_KIND") // https://youtrack.jetbrains.com/issue/KT-70714 +public inline fun T.runCatchingCancellable(block: T.() -> R): Result { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return try { + Result.success(block()) + } catch (e: InterruptedException) { + throw e + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + Result.failure(e) + } +} + +public inline fun Result.recoverCatchingCancellable(transform: (exception: Throwable) -> R): Result { + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + } + return when (val exception = exceptionOrNull()) { + null -> this + else -> runCatchingCancellable { transform(exception) } + } +} + +public inline fun Result.recoverNotNull(transform: (exception: Throwable) -> R?): Result { + contract { + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + } + return when (val exception = exceptionOrNull()) { + null -> this + else -> transform(exception)?.let(Result.Companion::success) ?: this + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt new file mode 100644 index 000000000..1f02cdee0 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt @@ -0,0 +1,250 @@ +@file:Suppress("DEPRECATION") + +package org.koitharu.kotatsu.parsers.util + +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.model.search.SearchCapability +import org.koitharu.kotatsu.parsers.model.search.SearchableField.* + +/** + * Converts a [MangaListFilter] into a [MangaSearchQuery]. + * + * This function iterates through the filter attributes in [MangaListFilter] and creates corresponding + * search criteria in a [MangaSearchQuery.Builder]. + * + * @param filter The [MangaListFilter] to convert. + * @return A [MangaSearchQuery] constructed based on the given [filter]. + */ +internal fun convertToMangaSearchQuery(offset: Int, sortOrder: SortOrder, filter: MangaListFilter): MangaSearchQuery { + return MangaSearchQuery.Builder().apply { + offset(offset) + order(sortOrder) + if (filter.tags.isNotEmpty()) criterion(Include(TAG, filter.tags)) + if (filter.tagsExclude.isNotEmpty()) criterion(Exclude(TAG, filter.tagsExclude)) + if (filter.states.isNotEmpty()) criterion(Include(STATE, filter.states)) + if (filter.types.isNotEmpty()) criterion(Include(CONTENT_TYPE, filter.types)) + if (filter.contentRating.isNotEmpty()) criterion(Include(CONTENT_RATING, filter.contentRating)) + if (filter.demographics.isNotEmpty()) criterion(Include(DEMOGRAPHIC, filter.demographics)) + if (validateYear(filter.yearFrom) || validateYear(filter.yearTo)) { + criterion(QueryCriteria.Range(PUBLICATION_YEAR, filter.yearFrom, filter.yearTo)) + } + if (validateYear(filter.year)) { + criterion(Match(PUBLICATION_YEAR, filter.year)) + } + filter.locale?.let { + criterion(Include(LANGUAGE, setOf(it))) + } + filter.originalLocale?.let { + criterion(Include(ORIGINAL_LANGUAGE, setOf(it))) + } + filter.query?.takeIf { it.isNotBlank() }?.let { + criterion(Match(TITLE_NAME, it)) + } + }.build() +} + +/** + * Converts a {@link MangaSearchQuery} into a {@link MangaListFilter}. + *

+ * This method iterates through the search criteria defined in the provided {@code searchQuery} + * and applies them to a {@link MangaListFilter.Builder}. The criteria are processed based on + * their types, such as inclusion, exclusion, equality checks, range filtering, and pattern matching. + *

+ *

+ * Supported criteria: + *

    + *
  • {@link QueryCriteria.Include} - Adds tags, states, content types, content ratings, demographics, and languages.
  • + *
  • {@link QueryCriteria.Exclude} - Excludes tags.
  • + *
  • {@link QueryCriteria.Equals} - Sets specific values like publication year.
  • + *
  • {@link QueryCriteria.Between} - Sets a range of values like publication year range.
  • + *
  • {@link QueryCriteria.Match} - Adds a search pattern for the title name.
  • + *
+ *

+ *

+ * If an unsupported field is encountered, an {@link UnsupportedOperationException} is thrown. + *

+ * + * @param searchQuery The {@link MangaSearchQuery} to convert. + * @return A {@link MangaListFilter} constructed based on the given {@code searchQuery}. + * @throws UnsupportedOperationException If the search criteria contain unsupported fields. + */ +internal fun convertToMangaListFilter(searchQuery: MangaSearchQuery): MangaListFilter { + return MangaListFilter.Builder().apply { + for (criterion in searchQuery.criteria) { + when (criterion) { + is Include<*> -> handleInclude(this, criterion) + is Exclude<*> -> handleExclude(this, criterion) + is Range<*> -> handleBetween(this, criterion) + is Match<*> -> handleMatch(this, criterion) + } + } + }.build() +} + +internal fun MangaSearchQueryCapabilities.toMangaListFilterCapabilities() = MangaListFilterCapabilities( + isMultipleTagsSupported = capabilities.any { x -> x.field == TAG && x.isMultiple }, + isTagsExclusionSupported = capabilities.any { x -> x.field == TAG && x.criteriaTypes.contains(Exclude::class) }, + isSearchSupported = capabilities.any { x -> x.field == TITLE_NAME }, + isSearchWithFiltersSupported = capabilities.any { x -> x.field == TITLE_NAME && !x.isExclusive }, + isYearSupported = capabilities.any { x -> x.field == PUBLICATION_YEAR && x.criteriaTypes.contains(Match::class) }, + isYearRangeSupported = capabilities.any { x -> x.field == PUBLICATION_YEAR && x.criteriaTypes.contains(Range::class) }, + isOriginalLocaleSupported = capabilities.any { x -> x.field == ORIGINAL_LANGUAGE }, + isAuthorSearchSupported = capabilities.any { x -> x.field == AUTHOR }, +) + +internal fun MangaListFilterCapabilities.toMangaSearchQueryCapabilities(): MangaSearchQueryCapabilities = + MangaSearchQueryCapabilities( + capabilities = setOfNotNull( + isMultipleTagsSupported.takeIf { it }?.let { + SearchCapability( + field = TAG, + criteriaTypes = setOf(Include::class), + isMultiple = true, + ) + }, + isTagsExclusionSupported.takeIf { it }?.let { + SearchCapability( + field = TAG, + criteriaTypes = setOf(Exclude::class), + isMultiple = true, + ) + }, + isSearchSupported.takeIf { it }?.let { + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + isMultiple = false, + isExclusive = true, + ) + }, + isSearchWithFiltersSupported.takeIf { it }?.let { + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + isMultiple = false, + ) + }, + isYearSupported.takeIf { it }?.let { + SearchCapability( + field = PUBLICATION_YEAR, + criteriaTypes = setOf(Match::class), + isMultiple = false, + ) + }, + isYearRangeSupported.takeIf { it }?.let { + SearchCapability( + field = PUBLICATION_YEAR, + criteriaTypes = setOf(Range::class), + isMultiple = false, + ) + }, + isOriginalLocaleSupported.takeIf { it }?.let { + SearchCapability( + field = ORIGINAL_LANGUAGE, + criteriaTypes = setOf(Include::class), + isMultiple = true, + ) + }, + SearchCapability( + field = LANGUAGE, + criteriaTypes = setOf(Include::class), + isMultiple = true, + ), + SearchCapability( + field = STATE, criteriaTypes = setOf(Include::class), isMultiple = true, + ), + SearchCapability( + field = CONTENT_TYPE, + criteriaTypes = setOf(Include::class), + isMultiple = true, + ), + SearchCapability( + field = CONTENT_RATING, + criteriaTypes = setOf(Include::class), + isMultiple = true, + ), + SearchCapability( + field = DEMOGRAPHIC, + criteriaTypes = setOf(Include::class), + isMultiple = true, + ), + ), + ) + +private fun handleInclude(builder: MangaListFilter.Builder, criterion: Include<*>) { + val type = criterion.field.type + + when (criterion.field) { + TAG -> builder.addTags(filterValues(criterion, type)) + STATE -> builder.addStates(filterValues(criterion, type)) + CONTENT_TYPE -> builder.addTypes(filterValues(criterion, type)) + CONTENT_RATING -> builder.addContentRatings(filterValues(criterion, type)) + DEMOGRAPHIC -> builder.addDemographics(filterValues(criterion, type)) + LANGUAGE -> builder.locale(getFirstValue(criterion, type)) + ORIGINAL_LANGUAGE -> builder.originalLocale(getFirstValue(criterion, type)) + else -> throw IllegalArgumentException("Unsupported field for Include criterion: ${criterion.field}") + } +} + +private fun handleExclude(builder: MangaListFilter.Builder, criterion: Exclude<*>) { + val type = criterion.field.type + + when (criterion.field) { + TAG -> builder.excludeTags(filterValues(criterion, type)) + else -> throw IllegalArgumentException("Unsupported field for Exclude criterion: ${criterion.field}") + } +} + +private fun handleBetween(builder: MangaListFilter.Builder, criterion: Range<*>) { + val type = criterion.field.type + + when (criterion.field) { + PUBLICATION_YEAR -> { + builder.yearFrom(getValue(criterion.from, type, YEAR_UNKNOWN)) + builder.yearTo(getValue(criterion.to, type, YEAR_UNKNOWN)) + } + + else -> throw IllegalArgumentException("Unsupported field for Between criterion: ${criterion.field}") + } +} + +private fun handleMatch(builder: MangaListFilter.Builder, criterion: Match<*>) { + val type = criterion.field.type + + when (criterion.field) { + TITLE_NAME -> builder.query(getValue(criterion.value, type, "")) + PUBLICATION_YEAR -> builder.year(getValue(criterion.value, type, YEAR_UNKNOWN)) + else -> throw IllegalArgumentException("Unsupported field for Match criterion: ${criterion.field}") + } +} + +@Suppress("UNCHECKED_CAST") +private fun filterValues(criterion: Include<*>, type: Class<*>): List { + return criterion.values.filter { type.isInstance(it) } as List +} + +@Suppress("UNCHECKED_CAST") +private fun filterValues(criterion: Exclude<*>, type: Class<*>): List { + return criterion.values.filter { type.isInstance(it) } as List +} + +@Suppress("UNCHECKED_CAST") +private fun getFirstValue(criterion: Include<*>, type: Class<*>): T? { + return criterion.values.firstOrNull { type.isInstance(it) } as? T +} + +@Suppress("UNCHECKED_CAST") +private fun getValue(value: Any?, type: Class<*>, default: T): T { + val isCompatibleIntType = (type == Int::class.java && Integer::class.isInstance(value)) + + return if (type.isInstance(value) || isCompatibleIntType) value as T else default +} + +private fun validateYear(year: Int) = year != YEAR_UNKNOWN diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt new file mode 100644 index 000000000..e6875282a --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt @@ -0,0 +1,237 @@ +@file:JvmName("StringUtils") + +package org.koitharu.kotatsu.parsers.util + +import androidx.collection.MutableIntList +import java.math.BigInteger +import java.net.URLDecoder +import java.net.URLEncoder +import java.security.MessageDigest +import java.util.* +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.math.min + +private val REGEX_WHITESPACE = Regex("\\s+") +internal const val LONG_HASH_SEED = 1125899906842597L + +public fun String.removeSurrounding(vararg chars: Char): String { + if (isEmpty()) { + return this + } + for (c in chars) { + if (first() == c && last() == c) { + return substring(1, length - 1) + } + } + return this +} + +public inline fun C?.ifNullOrEmpty(defaultValue: () -> R): R { + contract { + callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) + } + return if (this.isNullOrEmpty()) defaultValue() else this +} + +public fun String.longHashCode(): Long { + var h = LONG_HASH_SEED + val len: Int = this.length + for (i in 0 until len) { + h = 31 * h + this[i].code + } + return h +} + +public fun String.toCamelCase(): String { + if (isEmpty()) { + return this + } + val result = StringBuilder(length) + var capitalize = true + for (char in this) { + result.append( + if (capitalize) { + char.uppercase() + } else { + char.lowercase() + }, + ) + capitalize = char.isWhitespace() + } + return result.toString() +} + +public fun String.digits(): String = filter { it.isDigit() } + +public fun String.toTitleCase(): String { + return replaceFirstChar { x -> x.uppercase() } +} + +public fun String.toTitleCase(locale: Locale): String { + return replaceFirstChar { x -> x.uppercase(locale) } +} + +public fun String.ellipsize(maxLength: Int): String = if (this.length > maxLength) { + this.take(maxLength - 1) + Typography.ellipsis +} else this + +public fun String.splitTwoParts(delimiter: Char): Pair? { + val indices = MutableIntList(4) + for ((i, c) in this.withIndex()) { + if (c == delimiter) { + indices += i + } + } + if (indices.isEmpty() || (indices.size and 1) == 0) { + return null + } + val index = indices[indices.size / 2] + return substring(0, index) to substring(index + 1) +} + +public fun String.urlEncoded(): String = URLEncoder.encode(this, Charsets.UTF_8.name()) + +public fun String.urlDecode(): String = URLDecoder.decode(this, Charsets.UTF_8.name()) + +public fun String.nl2br(): String = replace("\n", "
") + +public fun String.splitByWhitespace(): List = trim().split(REGEX_WHITESPACE) + +public fun T.nullIfEmpty(): T? = takeUnless { it.isEmpty() } + +public fun ByteArray.byte2HexFormatted(): String { + val str = StringBuilder(size * 2) + for (i in indices) { + var h = Integer.toHexString(this[i].toInt()) + val l = h.length + if (l == 1) { + h = "0$h" + } + if (l > 2) { + h = h.substring(l - 2, l) + } + str.append(h.uppercase(Locale.ROOT)) + if (i < size - 1) { + str.append(':') + } + } + return str.toString() +} + +public fun String.md5(): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(toByteArray())) + .toString(16) + .padStart(32, '0') +} + +public fun String.substringBetween(from: String, to: String, fallbackValue: String = this): String { + val fromIndex = indexOf(from) + if (fromIndex == -1) { + return fallbackValue + } + val toIndex = lastIndexOf(to) + return if (toIndex == -1) { + fallbackValue + } else { + substring(fromIndex + from.length, toIndex) + } +} + +public fun String.substringBetweenFirst(from: String, to: String): String? { + val fromIndex = indexOf(from) + if (fromIndex == -1) { + return null + } + val toIndex = indexOf(to, fromIndex) + return if (toIndex == -1) { + null + } else { + substring(fromIndex + from.length, toIndex) + } +} + +public fun String.substringBetweenLast(from: String, to: String, fallbackValue: String = this): String { + val fromIndex = lastIndexOf(from) + if (fromIndex == -1) { + return fallbackValue + } + val toIndex = lastIndexOf(to) + return if (toIndex == -1) { + fallbackValue + } else { + substring(fromIndex + from.length, toIndex) + } +} + +public fun String.find(regex: Regex): String? = regex.find(this)?.value + +public fun String.findGroupValue(regex: Regex): String? = regex.find(this)?.groupValues?.getOrNull(1) + +public fun String.removeSuffix(suffix: Char): String { + if (lastOrNull() == suffix) { + return substring(0, length - 1) + } + return this +} + +public fun String.levenshteinDistance(other: String): Int { + if (this == other) { + return 0 + } + if (this.isEmpty()) { + return other.length + } + if (other.isEmpty()) { + return this.length + } + + val lhsLength = this.length + 1 + val rhsLength = other.length + 1 + + var cost = Array(lhsLength) { it } + var newCost = Array(lhsLength) { 0 } + + for (i in 1 until rhsLength) { + newCost[0] = i + + for (j in 1 until lhsLength) { + val match = if (this[j - 1] == other[i - 1]) 0 else 1 + + val costReplace = cost[j - 1] + match + val costInsert = cost[j] + 1 + val costDelete = newCost[j - 1] + 1 + + newCost[j] = min(min(costInsert, costDelete), costReplace) + } + + val swap = cost + cost = newCost + newCost = swap + } + + return cost[lhsLength - 1] +} + +/** + * @param threshold 0 = exact match + */ +public fun String.almostEquals(other: String, threshold: Float): Boolean { + if (threshold <= 0f) { + return equals(other, ignoreCase = true) + } + val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f) + return diff < threshold +} + +public fun String.isNumeric(): Boolean = all { c -> c.isDigit() } + +internal fun StringBuilder.removeTrailingZero() { + if (length > 2 && get(length - 1) == '0') { + val dot = get(length - 2) + if (dot == ',' || dot == '.') { + delete(length - 2, length) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt index 12f7cad71..bc10c58f7 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt @@ -7,7 +7,7 @@ public class WebViewHelper( ) { public suspend fun getLocalStorageValue(domain: String, key: String): String? { - return context.evaluateJs("https://$domain/", "window.localStorage.getItem(\"$key\")") + return context.evaluateJs("$SCHEME_HTTPS://$domain/", "window.localStorage.getItem(\"$key\")") } public suspend fun getUrlValue(url: String, value: String): String? { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt new file mode 100644 index 000000000..0c5ac5477 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/EscapeUtils.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.parsers.util.json + +public fun String.unescapeJson(): String { + val builder = StringBuilder() + var i = 0 + while (i < length) { + val delimiter = this[i] + i++ // consume letter or backslash + if (delimiter == '\\' && i < length) { + val ch = this[i] + i++ + + when (ch) { + '\\', '/', '"', '\'' -> builder.append(ch) + 'n' -> builder.append('\n') + 'r' -> builder.append('\r') + 't' -> builder.append('\t') + 'b' -> builder.append('\b') + 'u' -> { + val hex = StringBuilder(4) + require(i + 4 <= length) { "Not enough unicode digits!" } + for (x in substring(i, i + 4)) { + require(x.isLetterOrDigit()) { "Bad character in unicode escape" } + hex.append(x.lowercase()) + } + i += 4 // consume those four digits. + val code = hex.toString().toInt(16) + builder.append(code.toChar()) + } + + else -> throw IllegalArgumentException("Illegal escape sequence: \\$ch") + } + } else { + builder.append(delimiter) + } + } + return builder.toString() +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt new file mode 100644 index 000000000..b5ebf25ac --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedIterator.kt @@ -0,0 +1,33 @@ +package org.koitharu.kotatsu.parsers.util.json + +import org.json.JSONArray + +internal class JSONArrayTypedIterator( + private val array: JSONArray, + private val typeClass: Class, + startIndex: Int, +) : ListIterator { + + private val total = array.length() + private var index = startIndex + + override fun hasNext() = index < total + + override fun next(): T { + if (!hasNext()) throw NoSuchElementException() + return get(index++) + } + + override fun hasPrevious(): Boolean = index > 0 + + override fun nextIndex(): Int = index + + override fun previous(): T { + if (!hasPrevious()) throw NoSuchElementException() + return get(--index) + } + + override fun previousIndex(): Int = index - 1 + + private fun get(i: Int): T = typeClass.cast(array[i]) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt new file mode 100644 index 000000000..2caa8d75e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONArrayTypedListWrapper.kt @@ -0,0 +1,53 @@ +package org.koitharu.kotatsu.parsers.util.json + +import org.json.JSONArray + +internal class JSONArrayTypedListWrapper( + private val jsonArray: JSONArray, + private val typeClass: Class, +) : List { + + override fun contains(element: T): Boolean = indexOf(element) != -1 + + override fun containsAll(elements: Collection): Boolean = elements.all { contains(it) } + + override fun get(index: Int): T = typeClass.cast(jsonArray[index]) + + override fun indexOf(element: T): Int { + repeat(jsonArray.length()) { i -> + if (jsonArray[i] == element) { + return i + } + } + return -1 + } + + override fun isEmpty(): Boolean = jsonArray.length() == 0 + + override fun iterator(): Iterator = listIterator(0) + + override fun lastIndexOf(element: T): Int { + val total = jsonArray.length() + repeat(total) { i -> + if (jsonArray[total - i - 1] == element) { + return i + } + } + return -1 + } + + override fun listIterator(): ListIterator = listIterator(0) + + override fun listIterator(index: Int): ListIterator = JSONArrayTypedIterator(jsonArray, typeClass, index) + + override fun subList(fromIndex: Int, toIndex: Int): List { + val result = ArrayList(toIndex - fromIndex + 1) + for (i in fromIndex..toIndex) { + result.add(get(i)) + } + return result + } + + override val size: Int + get() = jsonArray.length() +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt new file mode 100644 index 000000000..32c94c971 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JSONObjectTypedIterableWrapper.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.parsers.util.json + +import org.json.JSONObject + +internal class JSONObjectTypedIterableWrapper( + private val json: JSONObject, + private val typeClass: Class, +) : Iterable> { + + override fun iterator(): Iterator> = IteratorImpl() + + private inner class IteratorImpl : Iterator> { + + private val keyIterator = json.keys().iterator() + + override fun hasNext(): Boolean = keyIterator.hasNext() + + override fun next(): Map.Entry = keyIterator.next().let { key -> + JSONEntry(key, typeClass.cast(json.get(key))) + } + } + + private class JSONEntry(override val key: String, override val value: T) : Map.Entry +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt new file mode 100644 index 000000000..b95a7ddcf --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/JsonExt.kt @@ -0,0 +1,167 @@ +package org.koitharu.kotatsu.parsers.util.json + +import androidx.collection.ArraySet +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.koitharu.kotatsu.parsers.util.nullIfEmpty +import java.util.* +import kotlin.contracts.contract + +public fun String.toJSONObjectOrNull(): JSONObject? = try { + JSONObject(this) +} catch (_: JSONException) { + null +} + +public fun String.toJSONArrayOrNull(): JSONArray? = try { + JSONArray(this) +} catch (_: JSONException) { + null +} + +public inline fun > JSONArray.mapJSONTo( + destination: C, + block: (JSONObject) -> R, +): C { + val len = length() + for (i in 0 until len) { + val jo = getJSONObject(i) + destination.add(block(jo)) + } + return destination +} + +public inline fun > JSONArray.mapJSONNotNullTo( + destination: C, + block: (JSONObject) -> R?, +): C { + val len = length() + for (i in 0 until len) { + val jo = getJSONObject(i) + destination.add(block(jo) ?: continue) + } + return destination +} + +public inline fun JSONArray.mapJSON(block: (JSONObject) -> T): List { + return mapJSONTo(ArrayList(length()), block) +} + +public inline fun JSONArray.mapJSONNotNull(block: (JSONObject) -> T?): List { + return mapJSONNotNullTo(ArrayList(length()), block) +} + +public inline fun JSONArray.mapJSONToSet(mapper: (JSONObject) -> T): Set { + return mapJSONTo(ArraySet(length()), mapper) +} + +public inline fun JSONArray.mapJSONNotNullToSet(mapper: (JSONObject) -> T?): Set { + return mapJSONNotNullTo(ArraySet(length()), mapper) +} + +public fun JSONArray.mapJSONIndexed(block: (Int, JSONObject) -> T): List { + val len = length() + val result = ArrayList(len) + for (i in 0 until len) { + val jo = getJSONObject(i) + result.add(block(i, jo)) + } + return result +} + +public fun JSONObject.getStringOrNull(name: String): String? = opt(name)?.takeUnless { + it === JSONObject.NULL +}?.toString()?.nullIfEmpty() + +public fun JSONObject.getBooleanOrDefault(name: String, defaultValue: Boolean): Boolean { + return when (val rawValue = opt(name)) { + null, JSONObject.NULL -> defaultValue + is Boolean -> rawValue + is Number -> rawValue.toInt() != 0 + is String -> rawValue.lowercase(Locale.ROOT).toBooleanStrictOrNull() ?: defaultValue + else -> defaultValue + } +} + +public fun JSONObject.getLongOrDefault(name: String, defaultValue: Long): Long { + return when (val rawValue = opt(name)) { + null, JSONObject.NULL -> defaultValue + is Long -> rawValue + is Number -> rawValue.toLong() + is String -> rawValue.toLongOrNull() ?: defaultValue + else -> defaultValue + } +} + +public fun JSONObject.getIntOrDefault(name: String, defaultValue: Int): Int { + return when (val rawValue = opt(name)) { + null, JSONObject.NULL -> defaultValue + is Int -> rawValue + is Number -> rawValue.toInt() + is String -> rawValue.toIntOrNull() ?: defaultValue + else -> defaultValue + } +} + +public fun JSONObject.getDoubleOrDefault(name: String, defaultValue: Double): Double { + return when (val rawValue = opt(name)) { + null, JSONObject.NULL -> defaultValue + is Double -> rawValue + is Number -> rawValue.toDouble() + is String -> rawValue.toDoubleOrNull() ?: defaultValue + else -> defaultValue + } +} + +public fun JSONObject.getFloatOrDefault(name: String, defaultValue: Float): Float { + return when (val rawValue = opt(name)) { + null, JSONObject.NULL -> defaultValue + is Float -> rawValue + is Number -> rawValue.toFloat() + is String -> rawValue.toFloatOrNull() ?: defaultValue + else -> defaultValue + } +} + +public fun > JSONObject.getEnumValueOrNull(name: String, enumClass: Class): E? { + val enumName = getStringOrNull(name) ?: return null + return enumClass.enumConstants?.find { x -> + enumName.equals(x.name, ignoreCase = true) + } +} + +public fun > JSONObject.getEnumValueOrDefault(name: String, defaultValue: E): E { + return getEnumValueOrNull(name, defaultValue.javaClass) ?: defaultValue +} + +public fun JSONArray?.isNullOrEmpty(): Boolean { + contract { + returns(false) implies (this@isNullOrEmpty != null) + } + + return this == null || this.length() == 0 +} + +public fun JSONArray.asTypedList(typeClass: Class): List { + return JSONArrayTypedListWrapper(this, typeClass) +} + +public inline fun JSONArray.asTypedList(): List = asTypedList(T::class.java) + +public fun JSONObject.entries(typeClass: Class): Iterable> { + return JSONObjectTypedIterableWrapper(this, typeClass) +} + +public inline fun JSONObject.entries(): Iterable> = entries(T::class.java) + +public fun JSONArray.toStringSet(): Set { + val set = ArraySet(length()) + repeat(length()) { i -> + val str = optString(i) + if (!str.isNullOrEmpty()) { + set.add(str) + } + } + return set +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt new file mode 100644 index 000000000..9d4743edc --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/json/NextJs.kt @@ -0,0 +1,220 @@ +/** + * Utility for extracting Next.js data from HTML or RSC responses. + * Based on the Next.js extraction logic from the Keiyoushi repository. + */ +@file:JvmName("NextJsUtils") + +package org.koitharu.kotatsu.parsers.util.json + +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.jsoup.nodes.Document +import org.koitharu.kotatsu.parsers.util.parseHtml + +private val NEXT_F_REGEX = Regex("""self\.__next_f\.push\(\s*(\[.*])\s*\)\s*;?\s*$""", RegexOption.DOT_MATCHES_ALL) + +private fun extractValueNextJs( + payload: Any, + predicate: (Any) -> Boolean, +): Any? { + if (payload !is JSONObject && payload !is JSONArray) return null + if (predicate(payload)) { + return payload + } + + if (payload is JSONObject) { + val keys = payload.keys() + while (keys.hasNext()) { + val key = keys.next() + val child = payload.opt(key) + if (child != null) { + val result = extractValueNextJs(child, predicate) + if (result != null) return result + } + } + } else if (payload is JSONArray) { + for (i in 0 until payload.length()) { + val child = payload.opt(i) + if (child != null) { + val result = extractValueNextJs(child, predicate) + if (result != null) return result + } + } + } + return null +} + +private fun Document.extractAppRouterPayloads(): List = select("script:not([src])") + .map { it.data() } + .filter { "self.__next_f.push" in it } + .flatMap { script -> + try { + val raw = NEXT_F_REGEX.find(script)?.groupValues?.get(1) ?: return@flatMap emptyList() + val arr = JSONArray(raw) + val content = arr.optString(1, null) ?: return@flatMap emptyList() + + extractRscPayloads(content) + } catch (_: Exception) { + emptyList() + } + } + +private fun Document.extractPagesRouterPayloads(): List { + val data = selectFirst("script#__NEXT_DATA__")?.data() ?: return emptyList() + return try { + val root = JSONObject(data) + val pageProps = root.optJSONObject("props")?.optJSONObject("pageProps") + listOfNotNull(pageProps, root) + } catch (_: Exception) { + emptyList() + } +} + +private fun extractRscPayloads(body: String): List { + val results = mutableListOf() + var pos = 0 + + while (pos < body.length) { + val colonIdx = body.indexOf(':', pos) + if (colonIdx == -1) break + + val id = body.substring(pos, colonIdx) + if (id.isEmpty() || !id.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' }) { + pos++ + continue + } + + pos = colonIdx + 1 + if (pos >= body.length) break + + if (body[pos] == 'T') { + pos++ + val commaIdx = body.indexOf(',', pos) + if (commaIdx == -1) break + val byteLen = body.substring(pos, commaIdx).toIntOrNull(16) ?: break + pos = commaIdx + 1 + var bytes = 0 + val start = pos + while (pos < body.length && bytes < byteLen) { + when { + body[pos].code < 0x80 -> bytes += 1 + body[pos].code < 0x800 -> bytes += 2 + Character.isHighSurrogate(body[pos]) -> { + bytes += 4 + pos++ + } + else -> bytes += 3 + } + pos++ + } + try { + results.add(parseJsonType(body.substring(start, pos))) + } catch (_: Exception) {} + } else { + val (element, end) = parseJsonAt(body, pos) + if (element != null) results.add(element) + pos = end + } + } + + return results +} + +private fun parseJsonType(jsonStr: String): Any { + val trimmed = jsonStr.trim() + return when { + trimmed.startsWith("{") -> JSONObject(trimmed) + trimmed.startsWith("[") -> JSONArray(trimmed) + else -> throw JSONException("Not a JSONObject or JSONArray") + } +} + +private fun parseJsonAt(body: String, start: Int): Pair { + if (start >= body.length) return Pair(null, start) + + var depth = 0 + var inString = false + var escape = false + var i = start + + while (i < body.length) { + val c = body[i++] + if (escape) { + escape = false + continue + } + if (c == '\\' && inString) { + escape = true + continue + } + if (c == '"') { + inString = !inString + continue + } + if (inString) continue + when (c) { + '{', '[' -> depth++ + '}', ']' -> if (--depth == 0) { + return try { + Pair(parseJsonType(body.substring(start, i)), i) + } catch (_: Exception) { + Pair(null, i) + } + } + } + if (depth == 0 && c.isWhitespace()) { + return try { + Pair(parseJsonType(body.substring(start, i - 1)), i) + } catch (_: Exception) { + Pair(null, i) + } + } + } + return Pair(null, i) +} + +public fun Document.extractNextJs( + predicate: (Any) -> Boolean, +): Any? { + val payloads = extractAppRouterPayloads().ifEmpty { extractPagesRouterPayloads() } + for (payload in payloads) { + val result = extractValueNextJs(payload, predicate) + if (result != null) return result + } + return null +} + +public inline fun Document.extractNextJsTyped( + noinline predicate: (Any) -> Boolean, +): T? = extractNextJs(predicate) as? T + +public fun String.extractNextJsRsc( + predicate: (Any) -> Boolean, +): Any? { + for (payload in extractRscPayloads(this)) { + val result = extractValueNextJs(payload, predicate) + if (result != null) return result + } + return null +} + +public inline fun String.extractNextJsRscTyped( + noinline predicate: (Any) -> Boolean, +): T? = extractNextJsRsc(predicate) as? T + +public fun Response.extractNextJs( + predicate: (Any) -> Boolean, +): Any? { + val contentType = header("Content-Type") ?: "" + return when { + "text/x-component" in contentType -> body.string().extractNextJsRsc(predicate) + "text/html" in contentType -> parseHtml().extractNextJs(predicate) + else -> error("Unsupported Content-Type for Next.js extraction: $contentType") + } +} + +public inline fun Response.extractNextJsTyped( + noinline predicate: (Any) -> Boolean, +): T? = extractNextJs(predicate) as? T diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt new file mode 100644 index 000000000..8c106e3e0 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SoftSuspendLazyImpl.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.parsers.util.suspendlazy + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.lang.ref.SoftReference +import kotlin.coroutines.CoroutineContext + +/** + * Like a [SuspendLazy] but with [SoftReference] under the hood + */ +internal class SoftSuspendLazyImpl( + private val coroutineContext: CoroutineContext, + private val initializer: SuspendLazyInitializer, +) : SuspendLazy { + + private val mutex: Mutex = Mutex() + private var cachedValue: SoftReference? = null + + override val isInitialized: Boolean + get() = cachedValue?.get() != null + + override suspend fun get(): T { + // fast way + cachedValue?.get()?.let { + return it + } + return mutex.withLock { + cachedValue?.get()?.let { + return it + } + val result = withContext(coroutineContext) { + initializer() + } + cachedValue = SoftReference(result) + result + } + } + + override fun peek(): T? = cachedValue?.get() +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt new file mode 100644 index 000000000..a923f71f8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt @@ -0,0 +1,39 @@ +package org.koitharu.kotatsu.parsers.util.suspendlazy + +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +internal typealias SuspendLazyInitializer = suspend () -> T + +public interface SuspendLazy { + + public val isInitialized: Boolean + + public suspend fun get(): T + + public fun peek(): T? +} + +public suspend fun SuspendLazy.getOrNull(): T? = runCatchingCancellable { + get() +}.getOrNull() + +public suspend fun SuspendLazy.getOrDefault(defaultValue: R): R = runCatchingCancellable { + get() +}.getOrDefault(defaultValue) + +public fun suspendLazy( + context: CoroutineContext = EmptyCoroutineContext, + initializer: SuspendLazyInitializer, +): SuspendLazy = SuspendLazyImpl(context, initializer) + +public fun suspendLazy( + context: CoroutineContext = EmptyCoroutineContext, + soft: Boolean, + initializer: SuspendLazyInitializer, +): SuspendLazy = if (soft) { + SoftSuspendLazyImpl(context, initializer) +} else { + SuspendLazyImpl(context, initializer) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt new file mode 100644 index 000000000..bb5bdb6ec --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazyImpl.kt @@ -0,0 +1,47 @@ +package org.koitharu.kotatsu.parsers.util.suspendlazy + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext + +internal class SuspendLazyImpl( + private val coroutineContext: CoroutineContext, + private val initializer: SuspendLazyInitializer, +) : SuspendLazy { + + private val mutex: Mutex = Mutex() + private var cachedValue: Any? = Uninitialized + + override val isInitialized: Boolean + get() = cachedValue !== Uninitialized + + @Suppress("UNCHECKED_CAST") + override suspend fun get(): T { + // fast way + cachedValue.let { + if (it !== Uninitialized) { + return it as T + } + } + return mutex.withLock { + cachedValue.let { + if (it !== Uninitialized) { + return it as T + } + } + val result = withContext(coroutineContext) { + initializer() + } + cachedValue = result + result + } + } + + @Suppress("UNCHECKED_CAST") + override fun peek(): T? { + return cachedValue?.takeUnless { it === Uninitialized } as T? + } + + private object Uninitialized +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt new file mode 100644 index 000000000..8b3617636 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/AuthCheckExtension.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.parsers + +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable + +class AuthCheckExtension : BeforeAllCallback { + + private val loaderContext: MangaLoaderContext = MangaLoaderContextMock + + override fun beforeAll(context: ExtensionContext) { + for (source in MangaParserSource.entries) { + val parser = loaderContext.newParserInstance(source) + if (parser is MangaParserAuthProvider) { + checkAuthorization(source, parser) + } + } + } + + private fun checkAuthorization(source: MangaParserSource, parser: MangaParserAuthProvider) = runTest { + runCatchingCancellable { + parser.getUsername() + }.onSuccess { username -> + println("Signed in to ${source.name} as $username") + }.onFailure { error -> + System.err.println("Auth failed for ${source.name}: ${error.javaClass.name}(${error.message})") + } + } +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt new file mode 100644 index 000000000..69efa9a40 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareInterceptor.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.parsers + +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.internal.closeQuietly +import org.koitharu.kotatsu.parsers.network.CloudFlareHelper + +internal class CloudFlareInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + if (CloudFlareHelper.checkResponseForProtection(response) != CloudFlareHelper.PROTECTION_NOT_DETECTED) { + response.closeQuietly() + throw CloudFlareProtectedException( + url = response.request.url.toString(), + headers = request.headers, + ) + } + return response + } +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt new file mode 100644 index 000000000..bbfdae8ae --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/CloudFlareProtectedException.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu.parsers + +import okhttp3.Headers +import okio.IOException + +class CloudFlareProtectedException( + val url: String, + val headers: Headers, +) : IOException("Protected by CloudFlare: $url") diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt new file mode 100644 index 000000000..48b47c111 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/CommonHeadersInterceptor.kt @@ -0,0 +1,40 @@ +package org.koitharu.kotatsu.parsers + +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaSource + +private const val HEADER_REFERER = CommonHeaders.REFERER + +internal class CommonHeadersInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val source = request.tag(MangaSource::class.java) + val parser = if (source is MangaParserSource) { + MangaLoaderContextMock.newParserInstance(source) + } else { + null + } + val headersBuilder = request.headers.newBuilder() + if (headersBuilder[HEADER_REFERER] == null && parser != null) { + headersBuilder[HEADER_REFERER] = "https://${parser.domain}/" + } + val newRequest = request.newBuilder().headers(headersBuilder.build()).build() + return if (parser is Interceptor) { + parser.intercept(ProxyChain(chain, newRequest)) + } else { + chain.proceed(newRequest) + } + } + + private class ProxyChain( + private val delegate: Interceptor.Chain, + private val request: Request, + ) : Interceptor.Chain by delegate { + + override fun request(): Request = request + } +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt new file mode 100644 index 000000000..1fad9c6c2 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/InMemoryCookieJar.kt @@ -0,0 +1,55 @@ +package org.koitharu.kotatsu.parsers + +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl +import org.koitharu.kotatsu.parsers.util.insertCookie +import org.koitharu.kotatsu.parsers.util.splitByWhitespace +import org.koitharu.kotatsu.test_util.component6 +import org.koitharu.kotatsu.test_util.component7 +import java.io.InputStream +import java.util.* +import java.util.concurrent.TimeUnit + +class InMemoryCookieJar : CookieJar { + + private val cache = HashMap() + + override fun loadForRequest(url: HttpUrl): List { + val time = System.currentTimeMillis() + return cache.values.filter { it.matches(url) && it.expiresAt >= time } + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + cookies.forEach { + val key = CookieKey(url.host, it.name) + cache[key] = it + } + } + + fun loadFromStream(stream: InputStream) { + val reader = stream.bufferedReader() + for (line in reader.lineSequence()) { + if (line.isBlank() || line.startsWith("# ")) { + continue + } + val (host, _, path, secure, expire, name, value) = line.splitByWhitespace() + val domain = host.removePrefix("#HttpOnly_").trimStart('.') + val httpOnly = host.startsWith("#HttpOnly_") + val cookie = Cookie.Builder() + cookie.domain(domain) + if (httpOnly) cookie.httpOnly() + cookie.path(path) + if (secure.lowercase(Locale.ROOT).toBooleanStrict()) cookie.secure() + cookie.expiresAt(TimeUnit.SECONDS.toMillis(expire.toLong())) + cookie.name(name) + cookie.value(value) + insertCookie(domain, cookie.build()) + } + } + + private data class CookieKey( + val host: String, + val name: String, + ) +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt new file mode 100644 index 000000000..04dad7f83 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt @@ -0,0 +1,107 @@ +package org.koitharu.kotatsu.parsers + +import com.koushikdutta.quack.QuackContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import org.koitharu.kotatsu.parsers.bitmap.Bitmap +import org.koitharu.kotatsu.parsers.config.MangaSourceConfig +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.network.UserAgents +import org.koitharu.kotatsu.parsers.util.await +import org.koitharu.kotatsu.parsers.util.requireBody +import org.koitharu.kotatsu.test_util.BitmapTestImpl +import java.awt.image.BufferedImage +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import javax.imageio.ImageIO +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager + +internal object MangaLoaderContextMock : MangaLoaderContext() { + + override val cookieJar = InMemoryCookieJar() + + override val httpClient: OkHttpClient = OkHttpClient.Builder() + .cookieJar(cookieJar) + .permissiveSSL() + .addInterceptor(CommonHeadersInterceptor()) + .addInterceptor(CloudFlareInterceptor()) + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .build() + + init { + loadTestCookies() + } + + @Deprecated("Provide a base url") + override suspend fun evaluateJs(script: String): String? = evaluateJs("", script) + + override suspend fun evaluateJs(baseUrl: String, script: String): String? { + return QuackContext.create().use { + it.evaluate(script)?.toString() + } + } + + override fun getConfig(source: MangaSource): MangaSourceConfig { + return SourceConfigMock() + } + + override fun getDefaultUserAgent(): String = UserAgents.FIREFOX_MOBILE + + override fun redrawImageResponse(response: Response, redraw: (Bitmap) -> Bitmap): Response { + val srcImage = response.requireBody().byteStream().use(ImageIO::read) + checkNotNull(srcImage) { "Cannot decode image" } + val resImage = (redraw(BitmapTestImpl(srcImage)) as BitmapTestImpl) + return response.newBuilder() + .body(resImage.compress("png").toResponseBody("image/png".toMediaTypeOrNull())) + .build() + } + + override fun createBitmap(width: Int, height: Int): Bitmap { + return BitmapTestImpl(BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)) + } + + suspend fun doRequest(url: String, source: MangaSource?): Response { + val request = Request.Builder() + .get() + .url(url) + if (source != null) { + request.tag(MangaSource::class.java, source) + } + return httpClient.newCall(request.build()).await() + } + + private fun loadTestCookies() { + // https://addons.mozilla.org/ru/firefox/addon/cookies-txt/ + javaClass.getResourceAsStream("/cookies.txt")?.use { + cookieJar.loadFromStream(it) + } ?: println("No cookies loaded!") + } + + private fun OkHttpClient.Builder.permissiveSSL() = also { builder -> + runCatching { + val trustAllCerts = object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) = Unit + + override fun checkServerTrusted(chain: Array, authType: String) = Unit + + override fun getAcceptedIssuers(): Array = emptyArray() + } + val sslContext = SSLContext.getInstance("SSL") + sslContext.init(null, arrayOf(trustAllCerts), SecureRandom()) + val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory + builder.sslSocketFactory(sslSocketFactory, trustAllCerts) + builder.hostnameVerifier { _, _ -> true } + }.onFailure { + it.printStackTrace() + } + } + +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt new file mode 100644 index 000000000..f1444c5bf --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt @@ -0,0 +1,300 @@ +package org.koitharu.kotatsu.parsers + +import kotlinx.coroutines.test.runTest +import okhttp3.HttpUrl +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.params.ParameterizedTest +import org.koitharu.kotatsu.parsers.core.PagedMangaParser +import org.koitharu.kotatsu.parsers.core.SinglePageMangaParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.Include +import org.koitharu.kotatsu.parsers.model.search.SearchableField.* +import org.koitharu.kotatsu.parsers.util.medianOrNull +import org.koitharu.kotatsu.parsers.util.mimeType +import org.koitharu.kotatsu.test_util.* +import kotlin.time.Duration.Companion.minutes + +//@ExtendWith(AuthCheckExtension::class) +internal class MangaParserTest { + + private val context = MangaLoaderContextMock + private val timeout = 2.minutes + + @ParameterizedTest(name = "{index}|list|{0}") + @MangaSources + fun list(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val list = parser.getList(MangaSearchQuery.Builder().build()) + checkMangaList(list, "list") + assert(list.all { it.source == source }) + } + + @ParameterizedTest(name = "{index}|pagination|{0}") + @MangaSources + fun pagination(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + if (parser is SinglePageMangaParser) { + return@runTest + } + val page1 = parser.getList(MangaSearchQuery.EMPTY) + val page2 = + parser.getList(MangaSearchQuery.Builder().offset(page1.size).build()) + if (parser is PagedMangaParser) { + assert(parser.pageSize >= page1.size) { + "Page size is ${page1.size} but ${parser.pageSize} expected" + } + } + assert(page1.isNotEmpty()) { "Page 1 is empty" } + assert(page2.isNotEmpty()) { "Page 2 is empty" } + assert(page1 != page2) { "Pages are equal" } + val intersection = page1.intersect(page2.toSet()) + assert(intersection.isEmpty()) { + "Pages are intersected by " + intersection.size + } + } + + @ParameterizedTest(name = "{index}|search|{0}") + @MangaSources + fun searchByTitleName(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val subject = parser.getList(MangaSearchQuery.EMPTY).minByOrNull { + it.title.length + } ?: error("No manga found") + + val query = subject.title + check(query.isNotBlank()) { "Manga title '$query' is blank" } + val list = parser.getList( + MangaSearchQuery.Builder() + .order(SortOrder.RELEVANCE) + .criterion(QueryCriteria.Match(TITLE_NAME, query)) + .build(), + ) + assert(list.isNotEmpty()) { "Empty search results by \"$query\"" } + assert(list.singleOrNull { it.url == subject.url && it.id == subject.id } != null) { + "Single subject '${subject.title} (${subject.publicUrl})' not found in search results" + } + checkMangaList(list, "search('$query')") + assert(list.all { it.source == source }) + } + + @ParameterizedTest(name = "{index}|tags|{0}") + @MangaSources + fun tags(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val tags = parser.getFilterOptions().availableTags + assert(tags.isNotEmpty()) { "No tags found" } + val keys = tags.map { it.key } + assert(keys.isDistinct()) + assert("" !in keys) + val titles = tags.map { it.title } + assert(titles.isDistinct()) + assert("" !in titles) + assert(titles.all { it.isCapitalized() }) { + val badTags = titles.filterNot { it.isCapitalized() }.joinToString() + "Not all tags are capitalized: $badTags" + } + assert(tags.all { it.source == source }) + + val tag = tags.last() + val list = parser.getList( + MangaSearchQuery.Builder() + .offset(0) + .criterion(Include(TAG, setOf(tag))) + .build(), + ) + checkMangaList(list, "${tag.title} (${tag.key})") + assert(list.all { it.source == source }) + } + + @ParameterizedTest(name = "{index}|tags_multiple|{0}") + @MangaSources + fun tagsMultiple(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) +// if (!parser.filterCapabilities.isMultipleTagsSupported) return@runTest + val tags = parser.getFilterOptions().availableTags.shuffled().take(2).toSet() + + val list = parser.getList( + MangaSearchQuery.Builder() + .offset(0) + .criterion(Include(TAG, tags)) + .build(), + ) + + checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})") + assert(list.all { it.source == source }) + } + + @ParameterizedTest(name = "{index}|locale|{0}") + @MangaSources + fun locale(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val locales = parser.getFilterOptions().availableLocales + if (locales.isEmpty()) { + return@runTest + } + val locale = locales.random() + val list = parser.getList( + MangaSearchQuery.Builder() + .criterion(Include(LANGUAGE, setOf(locale))) + .criterion(Include(LANGUAGE, setOf(locale))) + .criterion(Include(ORIGINAL_LANGUAGE, setOf(locales.random()))) + .build(), + ) + checkMangaList(list, locale.toString()) + assert(list.all { it.source == source }) + } + + + @ParameterizedTest(name = "{index}|details|{0}") + @MangaSources + fun details(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val list = parser.getList(MangaSearchQuery.EMPTY) + + val manga = list.random() + parser.getDetails(manga).apply { + assert(!chapters.isNullOrEmpty()) { "Chapters are null or empty" } + assert(publicUrl.isUrlAbsolute()) { "Manga public url is not absolute: '$publicUrl'" } + assert(description != null) { "Detailed description is null: '$publicUrl'" } + assert(title.startsWith(manga.title)) { + "Titles are mismatch: '$title' and '${manga.title}' for $publicUrl" + } + assert(this.source == source) + val c = checkNotNull(chapters) + assert(c.isDistinctBy { it.id }) { + "Chapters are not distinct by id: ${c.maxDuplicates { it.id }} for $publicUrl" + } + assert(c.isDistinctByNotNull { it.key() }) { + val dup = c.mapNotNull { it.key() }.maxDuplicates { it } + "Chapters are not distinct by branch/volume/number: $dup for $publicUrl" + } + assert(c.all { it.source == source }) + checkImageRequest(coverUrl, source) + largeCoverUrl?.let { + checkImageRequest(it, source) + } + } + } + + @ParameterizedTest(name = "{index}|pages|{0}") + @MangaSources + fun pages(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val list = parser.getList(MangaSearchQuery.EMPTY) + val manga = list.first() + val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null at ${manga.publicUrl}") + val pages = parser.getPages(chapter) + + assert(pages.isNotEmpty()) + assert(pages.isDistinctBy { it.id }) + assert(pages.all { it.source == source }) + + arrayOf( + pages.first(), + pages.medianOrNull() ?: error("No page"), + ).forEach { page -> + val pageUrl = parser.getPageUrl(page) + assert(pageUrl.isNotEmpty()) + assert(pageUrl.isUrlAbsolute()) + checkImageRequest(pageUrl, page.source) + } + } + + @ParameterizedTest(name = "{index}|favicon|{0}") + @MangaSources + fun favicon(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val favicons = parser.getFavicons() + val types = setOf("png", "svg", "ico", "gif", "jpg", "jpeg", "webp", "avif") + assert(favicons.isNotEmpty()) + favicons.forEach { + assert(it.url.isUrlAbsolute()) { "Favicon url is not absolute: ${it.url}" } + assert(it.type in types) { "Unknown icon type: ${it.type}" } + } + val favicon = favicons.find(24) + checkNotNull(favicon) + checkImageRequest(favicon.url, source) + } + + @ParameterizedTest(name = "{index}|domain|{0}") + @MangaSources + fun domain(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val defaultDomain = parser.domain + val url = HttpUrl.Builder().host(defaultDomain).scheme("https").toString() + val response = context.doRequest(url, source) + val realUrl = response.request.url + val realDomain = realUrl.topPrivateDomain() + val realHost = realUrl.host + assert(defaultDomain == realHost || defaultDomain == realDomain) { + "Domain mismatch:\nRequired:\t\t\t$defaultDomain\nActual:\t\t\t$realDomain\nHost:\t\t\t$realHost" + } + } + + @ParameterizedTest(name = "{index}|link|{0}") + @MangaSources + fun link(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + val manga = parser.getList(MangaSearchQuery.Builder().build()).first() + val resolved = context.newLinkResolver(manga.publicUrl).getManga() + Assertions.assertNotNull(resolved) + resolved ?: return@runTest + Assertions.assertEquals(manga.id, resolved.id) + Assertions.assertEquals(manga.publicUrl, resolved.publicUrl) + Assertions.assertEquals(manga.url, resolved.url) + Assertions.assertEquals(manga.title, resolved.title) + } + + @ParameterizedTest(name = "{index}|authorization|{0}") + @MangaSources + @Disabled + fun authorization(source: MangaParserSource) = runTest(timeout = timeout) { + val parser = context.newParserInstance(source) + if (parser is MangaParserAuthProvider) { + val username = parser.getUsername() + assert(username.isNotBlank()) { "Username is blank" } + println("Signed in to ${source.name} as $username") + } + } + + private suspend fun checkMangaList(list: List, cause: String) { + assert(list.isNotEmpty()) { "Manga list for '$cause' is empty" } + assert(list.isDistinctBy { it.id }) { "Manga list for '$cause' contains duplicated ids" } + for (item in list) { + assert(item.url.isNotEmpty()) { "Url is empty" } + assert(!item.url.isUrlAbsolute()) { "Url looks like absolute: ${item.url}" } + item.coverUrl?.let { + assert(it.isUrlAbsolute()) { "Cover url is not absolute: ${item.coverUrl}" } + } + assert(item.title.isNotEmpty()) { "Title for ${item.publicUrl} is empty" } + assert(item.publicUrl.isUrlAbsolute()) + } + val testItem = list.random() + checkImageRequest(testItem.coverUrl, testItem.source) + } + + private suspend fun checkImageRequest(url: String?, source: MangaSource) { + if (url == null) { + return + } + context.doRequest(url, source).use { + assert(it.isSuccessful) { "Request failed: ${it.code}(${it.message}): $url" } + assert(it.mimeType?.startsWith("image/") == true) { + "Wrong response mime type: ${it.mimeType}" + } + } + } + + private fun String.isCapitalized(): Boolean { + return !first().isLowerCase() + } + + private fun MangaChapter.key(): Any? = when { + number > 0f && volume > 0 -> Triple(branch, volume, number) + number > 0f -> Pair(branch, number) + else -> null + } +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt new file mode 100644 index 000000000..752930766 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.parsers + +import org.junit.jupiter.params.provider.EnumSource +import org.koitharu.kotatsu.parsers.model.MangaParserSource + +// Change 'names' to test specified parsers +@EnumSource(MangaParserSource::class, names = [], mode = EnumSource.Mode.INCLUDE) +internal annotation class MangaSources diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt new file mode 100644 index 000000000..1141c0094 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/SourceConfigMock.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu.parsers + +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.config.MangaSourceConfig + +internal class SourceConfigMock : MangaSourceConfig { + + override fun get(key: ConfigKey): T = key.defaultValue +} \ No newline at end of file diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt new file mode 100644 index 000000000..86b1757fa --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt @@ -0,0 +1,72 @@ +package org.koitharu.kotatsu.parsers.model.search + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.model.search.SearchableField.* +import java.util.* + +class MangaSearchQueryCapabilitiesTest { + + private val capabilities = MangaSearchQueryCapabilities( + capabilities = setOf( + SearchCapability(TITLE_NAME, setOf(Match::class), isMultiple = false, isExclusive = true), + SearchCapability(TAG, setOf(Include::class, Exclude::class), isMultiple = true, isExclusive = false), + SearchCapability(PUBLICATION_YEAR, setOf(Range::class), isMultiple = false, isExclusive = false), + SearchCapability(STATE, setOf(Include::class), isMultiple = false, isExclusive = false), + ), + ) + + @Test + fun validateValidSingleCriterionQuery() { + val query = MangaSearchQuery.Builder() + .criterion(Match(TITLE_NAME, "title")) + .build() + + assertDoesNotThrow { capabilities.validate(query) } + } + + @Test + fun validateUnsupportedFieldThrowsException() { + val query = MangaSearchQuery.Builder() + .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.ENGLISH))) + .build() + + assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } + } + + @Test + fun validateUnsupportedMultiValueThrowsException() { + val query = MangaSearchQuery.Builder() + .criterion(Include(STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))) + .build() + + assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } + } + + @Test + fun validateMultipleCriteriaWithOtherCriteriaAllowed() { + val query = MangaSearchQuery.Builder() + .criterion(Include(TAG, setOf(buildTag("tag1"), buildTag("tag2")))) + .criterion(Exclude(TAG, setOf(buildTag("tag3")))) + .build() + + assertDoesNotThrow { capabilities.validate(query) } + } + + @Test + fun validateMultipleCriteriaWithStrictCapabilityThrowsException() { + val query = MangaSearchQuery.Builder() + .criterion(Match(TITLE_NAME, "title")) + .criterion(Range(PUBLICATION_YEAR, 1990, 2000)) + .build() + + assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } + } + + private fun buildTag(name: String) = MangaTag(title = name, key = "${name}Key", source = MangaParserSource.MANGADEX) +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt new file mode 100644 index 000000000..9f4501f6f --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.parsers.util + +import org.junit.jupiter.api.Test +import org.koitharu.kotatsu.parsers.MangaLoaderContextMock +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.newParser +import java.io.File + +class IntentFilterGenerator { + + @Test + fun generateIntentFilter() { + val output = File("out/test/resources/intent-filter.xml") + output.printWriter(Charsets.UTF_8).use { writer -> + writer.appendLine("") + writer.appendTab().appendLine("") + writer.appendLine() + writer.appendTab().appendLine("") + writer.appendTab().appendLine("") + writer.appendLine() + writer.appendTab().appendLine("") + writer.appendTab().appendLine("") + writer.appendLine() + for (source in MangaParserSource.entries) { + val parser = source.newParser(MangaLoaderContextMock) + parser.configKeyDomain.presetValues.forEach { domain -> + writer.appendTab().append("") + } + } + writer.appendLine() + writer.appendLine("") + } + println(output.absolutePath) + } + + private fun Appendable.appendTab() = append('\t') +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt new file mode 100644 index 000000000..78bf4e5c3 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/LinkResolverTest.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.parsers.util + +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.koitharu.kotatsu.parsers.MangaLoaderContextMock +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import kotlin.time.Duration.Companion.minutes + +internal class LinkResolverTest { + + private val context = MangaLoaderContextMock + + @Test + fun supportedSource() = runTest(timeout = 2.minutes) { + val resolver = context.newLinkResolver("REDACTED" /* do not publish links to manga on GitHub */) + Assertions.assertEquals(MangaParserSource.MANGADEX, resolver.getSource()) + val manga = resolver.getManga() + Assertions.assertEquals(resolver.link.toString(), manga?.publicUrl) + } + + @Test + fun unsupportedSource2() = runTest(timeout = 2.minutes) { + val resolver = context.newLinkResolver("REDACTED" /* do not publish links to manga on GitHub */) + Assertions.assertEquals(MangaParserSource.BATOTO, resolver.getSource()) + val manga = resolver.getManga() + Assertions.assertEquals(resolver.link.toString(), manga?.publicUrl) + } +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt new file mode 100644 index 000000000..a6ed694ae --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt @@ -0,0 +1,77 @@ +package org.koitharu.kotatsu.parsers.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.ContentType.MANGA +import org.koitharu.kotatsu.parsers.model.ContentType.MANHUA +import org.koitharu.kotatsu.parsers.model.Demographic.SEINEN +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.model.search.SearchableField.* +import java.util.* + +class ListFilterToSearchQueryConverterTest { + + @Test + fun convertToMangaSearchQueryTest() { + val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) + val excludedTags = setOf(buildMangaTag("exclude_tag")) + val states = setOf(MangaState.ONGOING) + val contentRatings = setOf(ContentRating.SAFE) + val contentTypes = setOf(MANGA, MANHUA) + val demographics = setOf(SEINEN) + + val filter = MangaListFilter( + query = "title_name", + tags = tags, + tagsExclude = excludedTags, + locale = Locale.ENGLISH, + originalLocale = Locale.JAPANESE, + states = states, + contentRating = contentRatings, + types = contentTypes, + demographics = demographics, + year = 2020, + yearFrom = 1997, + yearTo = 2024, + ) + + val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) + + val expectedQuery = MangaSearchQuery.Builder() + .offset(0) + .order(SortOrder.NEWEST) + .criterion(Match(TITLE_NAME, "title_name")) + .criterion(Include(TAG, tags)) + .criterion(Exclude(TAG, excludedTags)) + .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) + .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) + .criterion(Include(STATE, states)) + .criterion(Include(CONTENT_RATING, contentRatings)) + .criterion(Include(CONTENT_TYPE, contentTypes)) + .criterion(Include(DEMOGRAPHIC, demographics)) + .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) + .criterion(Match(PUBLICATION_YEAR, 2020)) + .build() + + assertEquals(expectedQuery, searchQuery) + } + + @Test + fun convertToMangaSearchQueryWithEmptyFieldsTest() { + val filter = MangaListFilter() + + val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) + + assertEquals(MangaSearchQuery.Builder().offset(0).order(SortOrder.NEWEST).build(), searchQuery) + } + + private fun buildMangaTag(name: String): MangaTag { + return MangaTag( + key = "${name}Key", + title = name, + source = MangaParserSource.MANGADEX, + ) + } +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt new file mode 100644 index 000000000..7b7fe70e7 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/PaginatorTest.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.parsers.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class PaginatorTest { + + @Test + fun singlePaginationTest() { + val paginator = Paginator(24) + assertEquals(1, paginator.getPage(0)) + assertEquals(2, paginator.getPage(24)) + assertEquals(3, paginator.getPage(48)) + } + + @Test + fun adaptivePaginationTest() { + val paginator = Paginator(12) + assertEquals(1, paginator.getPage(0)) + paginator.onListReceived(0, 1, 24) + assertEquals(2, paginator.getPage(24)) + paginator.onListReceived(24, 2, 18) + assertEquals(3, paginator.getPage(42)) + } + + @Test + fun endReachPaginationTest() { + val pageSize = 24 + val paginator = Paginator(pageSize) + var size = 0 + repeat(5) { i -> + val offset = i * pageSize + assertEquals(i + 1, paginator.getPage(offset)) + paginator.onListReceived(offset, i + 1, pageSize) + size += pageSize + } + val nextPage = paginator.getPage(size) + assertEquals(nextPage, paginator.getPage(size)) + paginator.onListReceived(size, nextPage, 0) + assertEquals(nextPage, paginator.getPage(size)) + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt new file mode 100644 index 000000000..f9dfbc11e --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt @@ -0,0 +1,94 @@ +package org.koitharu.kotatsu.parsers.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.ContentType.MANGA +import org.koitharu.kotatsu.parsers.model.ContentType.MANHUA +import org.koitharu.kotatsu.parsers.model.Demographic.SEINEN +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.model.search.SearchableField.* +import java.util.* + +class ConvertToMangaListFilterTest { + + @Test + fun convertToMangaListFilterTest() { + val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) + val excludedTags = setOf(buildMangaTag("exclude_tag")) + val states = setOf(MangaState.ONGOING) + val contentRatings = setOf(ContentRating.SAFE) + val contentTypes = setOf(MANGA, MANHUA) + val demographics = setOf(SEINEN) + + val query = MangaSearchQuery.Builder() + .criterion(Match(TITLE_NAME, "title_name")) + .criterion(Include(TAG, tags)) + .criterion(Exclude(TAG, excludedTags)) + .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) + .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) + .criterion(Include(STATE, states)) + .criterion(Include(CONTENT_RATING, contentRatings)) + .criterion(Include(CONTENT_TYPE, contentTypes)) + .criterion(Include(DEMOGRAPHIC, demographics)) + .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) + .criterion(Match(PUBLICATION_YEAR, 2020)) + .build() + + val listFilter = convertToMangaListFilter(query) + + assertEquals(listFilter.query, "title_name") + assertEquals(listFilter.tags, tags) + assertEquals(listFilter.tagsExclude, excludedTags) + assertEquals(listFilter.locale, Locale.ENGLISH) + assertEquals(listFilter.originalLocale, Locale.JAPANESE) + assertEquals(listFilter.states, states) + assertEquals(listFilter.contentRating, contentRatings) + assertEquals(listFilter.types, contentTypes) + assertEquals(listFilter.demographics, demographics) + assertEquals(listFilter.year, 2020) + assertEquals(listFilter.yearFrom, 1997) + assertEquals(listFilter.yearTo, 2024) + } + + @Test + fun convertToMangaListFilterWithMultipleTagsIncludeTest() { + val tags1 = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) + val tags2 = setOf(buildMangaTag("tag3"), buildMangaTag("tag4")) + + val query = MangaSearchQuery.Builder() + .criterion(Include(TAG, tags1)) + .criterion(Include(TAG, tags2)) + .build() + + val listFilter = convertToMangaListFilter(query) + + assertEquals(listFilter.tags, tags1 union tags2) + } + + @Test + fun convertToMangaListFilterWithUnsupportedFieldTest() { + val query = MangaSearchQuery.Builder() + .criterion(Include(AUTHOR, setOf(buildMangaTag("author")))) + .build() + + val exception = assertThrows { + convertToMangaListFilter(query) + } + + assert(exception.message!!.contains("Unsupported field for Include criterion: AUTHOR")) + } + + private fun buildMangaTag(name: String): MangaTag { + return MangaTag( + key = "${name}Key", + title = name, + source = MangaParserSource.MANGADEX, + ) + } +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt b/src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt new file mode 100644 index 000000000..b566bafbf --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/test_util/BitmapTestImpl.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.test_util + +import org.koitharu.kotatsu.parsers.bitmap.Bitmap +import org.koitharu.kotatsu.parsers.bitmap.Rect +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import javax.imageio.ImageIO + +class BitmapTestImpl( + val image: BufferedImage, +) : Bitmap { + + override val width: Int + get() = image.width + + override val height: Int + get() = image.height + + override fun drawBitmap( + sourceBitmap: Bitmap, + src: Rect, + dst: Rect, + ) { + val graphics = image.createGraphics() + val subImage = (sourceBitmap as BitmapTestImpl).image.getSubimage( + src.left, src.top, src.width, src.height, + ) + graphics.drawImage(subImage, dst.left, dst.top, dst.width, dst.height, null) + graphics.dispose() + } + + fun compress(format: String): ByteArray = ByteArrayOutputStream().also { stream -> + ImageIO.write(image, format, stream) + }.toByteArray() +} diff --git a/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt b/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt new file mode 100644 index 000000000..5a631d8a0 --- /dev/null +++ b/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.test_util + +import androidx.collection.ArraySet +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN +import org.koitharu.kotatsu.parsers.util.LONG_HASH_SEED +import org.koitharu.kotatsu.parsers.util.toRelativeUrl + +private val PATTERN_URL_ABSOLUTE = Regex("^https?://[\\s\\S]+", setOf(RegexOption.IGNORE_CASE)) +private val PATTERN_URL_RELATIVE = Regex("^/[\\s\\S]+", setOf(RegexOption.IGNORE_CASE)) + +internal fun Collection.isDistinct(): Boolean { + val set = ArraySet(size) + for (item in this) { + if (!set.add(item)) { + return false + } + } + return set.size == size +} + +internal fun Collection.isDistinctBy(selector: (T) -> K): Boolean { + val set = ArraySet(size) + for (item in this) { + if (!set.add(selector(item))) { + return false + } + } + return set.size == size +} + +internal fun Collection.isDistinctByNotNull(selector: (T) -> K?): Boolean { + val set = ArraySet(size) + for (item in this) { + if (!set.add(selector(item) ?: continue)) { + return false + } + } + return true +} + +internal fun String.isUrlRelative() = matches(PATTERN_URL_RELATIVE) +internal fun String.isUrlAbsolute() = matches(PATTERN_URL_ABSOLUTE) + +internal inline fun Collection.maxDuplicates(selector: (T) -> K): K? { + return groupBy(selector).maxByOrNull { it.value.size }?.key +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun List.component6(): T = get(5) + +@Suppress("NOTHING_TO_INLINE") +inline operator fun List.component7(): T = get(6) + +fun mangaOf(source: MangaParserSource, url: String): Manga { + val httpUrl = url.toHttpUrlOrNull() + var id = LONG_HASH_SEED + source.name.forEach { c -> id = 31 * id + c.code } + url.forEach { c -> id = 31 * id + c.code } + return Manga( + id = id, + title = httpUrl?.pathSegments?.last() ?: url, + altTitles = emptySet(), + url = httpUrl?.let { url.toRelativeUrl(it.host) } ?: url, + publicUrl = url, + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = "", + tags = emptySet(), + state = null, + authors = emptySet(), + largeCoverUrl = null, + description = null, + chapters = null, + source = source, + ) +} diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..82de190bf --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,2 @@ +junit.jupiter.execution.parallel.enabled = true +junit.jupiter.execution.parallel.mode.default = concurrent \ No newline at end of file From f1dd4dbc3fdea3e44be7237c5c6570d07ef8a2e6 Mon Sep 17 00:00:00 2001 From: InvalidDavid <118831950+InvalidDavid@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:39:42 +0200 Subject: [PATCH 3/5] Update README.md (#422) --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d7432efdc..5c00ad00d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +# ABANDONED PROJECT + +Please move to: [UMA](https://github.com/InvalidDavid/UMA) + +> # NOTE: +> +> This plugin can still work in the future but will not receive any updates if somethings breaks! + # kotatsu-parsers This library provides a collection of manga parsers for convenient access to manga available on the web. It can be used in From b3423bc69b1f4b38427961e96fd141df52892687 Mon Sep 17 00:00:00 2001 From: InvalidDavid <118831950+InvalidDavid@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:50:12 +0200 Subject: [PATCH 4/5] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5c00ad00d..6881e993b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ Please move to: [UMA](https://github.com/InvalidDavid/UMA) > # NOTE: > > This plugin can still work in the future but will not receive any updates if somethings breaks! +Support Server: [![Discord][discord-shield]][discord-url] + +[discord-shield]: https://img.shields.io/discord/1518057632064209017?style=for-the-badge&logo=discord&label=Discord&color=5865F2 +[discord-url]: https://discord.gg/CyJeVDP7Cw # kotatsu-parsers From b7f009e504eca0f29261f5dbdb585fdae569111a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:25:19 +0000 Subject: [PATCH 5/5] chore(deps): bump com.squareup.okio:okio from 3.17.0 to 3.18.1 Bumps [com.squareup.okio:okio](https://github.com/lysine-dev/okio) from 3.17.0 to 3.18.1. - [Changelog](https://github.com/lysine-dev/okio/blob/main/CHANGELOG.md) - [Commits](https://github.com/lysine-dev/okio/compare/parent-3.17.0...parent-3.18.1) --- updated-dependencies: - dependency-name: com.squareup.okio:okio dependency-version: 3.18.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77ce34a61..af93cc916 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ ksp = "2.2.10-2.0.2" coroutines = "1.10.2" junit = "5.14.3" okhttp = "5.3.2" -okio = "3.17.0" +okio = "3.18.1" json = "20251224" androidx-collection = "1.6.0" # Keep Android 5, 6 compatible (with SDK 21 - 23)