Add Source PornPics (#8364)

* feat(pornpics): add source pornpics

* feat(pornpics): add i18n support

* fix(pornpics): Default category read error

* fix(pornpics): Content-Type is not set correctly

* fix(pornpics): properly handle searches

* fix(pornpics): properly handle searches

* fix(pornpics): correct next page detection logic.

Add +1 to requested image count per page,Compare actual received count with pageSize to determine next page.

* fix(pornpics): set base language to en

* fix(pornpics): safely handle gallery info parsing

* feat(pornpics): add filter

* chore(pornpics): remove unused dependency

* fix(pornpics): correct category urlPart values.

* refactor(pornpics): simplify category browsing logic

- Remove category unselected
- Treat all non-search requests as category browsing

* refactor(pornpics): make a singelton object and remove comanion object

* refactor(pornpics): put in class to reuse preference

* refactor(pornpics): optimize chapter loading with fetchChapterList

- Replace chapterFromElement with fetchChapterList
- Reduce unnecessary network requests

* fix(pornpics): correct CategoryType initialization

* refactor(pornpics): improve method naming

- Rename `addQueryPageParameter` to `addQueryParameterPage` for clarity

* refactor(pornpics): improve API readability with boolean parameters

- Change `buildMangasPageRequest(page: Int, period: Int)` to:
  `buildMangasPageRequest(page: Int, popular: Boolean)`
- Replace numeric period flag with semantic boolean
- Simplify request building logic

* refactor(pornpics): extract category search logic to dedicated method

- Extract `useSearch` as standalone method
- Add enhanced validation logic for category search

* refactor(pornpics): replace manual parsing with HttpUrl

- Replace custom URL parsing logic with HttpUrl utility

* fix(pornpics): remove invalid category options

* refactor(pornpics): improve JSON error handling

- Throw specific exception type when JSON parsing fails
This commit is contained in:
marioplus 2025-04-07 00:42:52 +08:00 committed by Draff
parent ebd527364e
commit 334dd69fab
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
23 changed files with 15243 additions and 0 deletions

View File

@ -0,0 +1,120 @@
[
{"id": "3089","name": "AI","link": "/ai/","flag_code": ""},
{"id": "226","name": "Amateur","link": "/amateur/","flag_code": ""},
{"id": "224","name": "Anal","link": "/anal/","flag_code": ""},
{"id": "231","name": "Anal Gape","link": "/anal-gape/","flag_code": ""},
{"id": "1853","name": "Arab","link": "/arab/","flag_code": ""},
{"id": "1163","name": "Armpit","link": "/armpit/","flag_code": ""},
{"id": "223","name": "Asian","link": "/asian/","flag_code": ""},
{"id": "254","name": "Ass","link": "/ass/","flag_code": ""},
{"id": "1707","name": "Asshole","link": "/asshole/","flag_code": ""},
{"id": "1847","name": "BBC","link": "/bbc/","flag_code": ""},
{"id": "258","name": "BBW","link": "/bbw/","flag_code": ""},
{"id": "1778","name": "BDSM","link": "/bdsm/","flag_code": ""},
{"id": "266","name": "Beach","link": "/beach/","flag_code": ""},
{"id": "2749","name": "Beautiful","link": "/beautiful/","flag_code": ""},
{"id": "930","name": "Big Clit","link": "/big-clit/","flag_code": ""},
{"id": "329","name": "Big Cock","link": "/big-cock/","flag_code": ""},
{"id": "232","name": "Big Tits","link": "/big-tits/","flag_code": ""},
{"id": "63","name": "Bikini","link": "/bikini/","flag_code": ""},
{"id": "361","name": "Blonde","link": "/blonde/","flag_code": ""},
{"id": "331","name": "Blowjob","link": "/blowjob/","flag_code": ""},
{"id": "364","name": "Bondage","link": "/bondage/","flag_code": ""},
{"id": "280","name": "Brazilian","link": "/brazilian/","flag_code": "br"},
{"id": "334","name": "Brunette","link": "/brunette/","flag_code": ""},
{"id": "304","name": "Bukkake","link": "/bukkake/","flag_code": ""},
{"id": "285","name": "Cameltoe","link": "/cameltoe/","flag_code": ""},
{"id": "2106","name": "Chinese","link": "/chinese/","flag_code": "cn"},
{"id": "41","name": "Chubby","link": "/chubby/","flag_code": ""},
{"id": "295","name": "Close Up","link": "/close-up/","flag_code": ""},
{"id": "244","name": "Cosplay","link": "/cosplay/","flag_code": ""},
{"id": "288","name": "Cowgirl","link": "/cowgirl/","flag_code": ""},
{"id": "342","name": "Creampie","link": "/creampie/","flag_code": ""},
{"id": "43","name": "Cuckold","link": "/cuckold/","flag_code": ""},
{"id": "241","name": "Cum In Mouth","link": "/cum-in-mouth/","flag_code": ""},
{"id": "336","name": "Cum In Pussy","link": "/cum-in-pussy/","flag_code": ""},
{"id": "339","name": "Cumshot","link": "/cumshot/","flag_code": ""},
{"id": "1927","name": "Curvy","link": "/curvy/","flag_code": ""},
{"id": "2031","name": "Cute","link": "/cute/","flag_code": ""},
{"id": "368","name": "Deepthroat","link": "/deepthroat/","flag_code": ""},
{"id": "341","name": "Dildo","link": "/dildo/","flag_code": ""},
{"id": "129","name": "Doggy Style","link": "/doggystyle/","flag_code": ""},
{"id": "251","name": "Double Penetration","link": "/double-penetration/","flag_code": ""},
{"id": "1706","name": "Dress","link": "/dress/","flag_code": ""},
{"id": "234","name": "Ebony","link": "/ebonies/","flag_code": ""},
{"id": "359","name": "Facial","link": "/facial/","flag_code": ""},
{"id": "347","name": "Feet","link": "/feet/","flag_code": ""},
{"id": "348","name": "Femdom","link": "/femdom/","flag_code": ""},
{"id": "2017","name": "Filipina","link": "/filipina/","flag_code": "ph"},
{"id": "272","name": "Gangbang","link": "/gangbang/","flag_code": ""},
{"id": "23","name": "Gay","link": "/gay/","flag_code": "yy"},
{"id": "267","name": "Glasses","link": "/glasses/","flag_code": ""},
{"id": "228","name": "Granny","link": "/granny/","flag_code": ""},
{"id": "1711","name": "Gym","link": "/gym/","flag_code": ""},
{"id": "227","name": "Hairy","link": "/hairy/","flag_code": ""},
{"id": "353","name": "Handjob","link": "/handjob/","flag_code": ""},
{"id": "245","name": "Hardcore","link": "/hardcore/","flag_code": ""},
{"id": "3090","name": "Hentai","link": "/hentai/","flag_code": ""},
{"id": "333","name": "High Heels","link": "/high-heels/","flag_code": ""},
{"id": "314","name": "Indian","link": "/indian/","flag_code": "in"},
{"id": "355","name": "Japanese","link": "/japanese/","flag_code": "jp"},
{"id": "25","name": "Korean","link": "/korean/","flag_code": "kr"},
{"id": "3","name": "Ladyboy","link": "/ladyboy/","flag_code": "xy"},
{"id": "345","name": "Latina","link": "/latina/","flag_code": ""},
{"id": "229","name": "Lesbian","link": "/lesbian/","flag_code": "xx"},
{"id": "330","name": "Lingerie","link": "/lingerie/","flag_code": ""},
{"id": "239","name": "MILF","link": "/milf/","flag_code": ""},
{"id": "346","name": "Maid","link": "/maid/","flag_code": ""},
{"id": "277","name": "Massage","link": "/massage/","flag_code": ""},
{"id": "290","name": "Masturbation","link": "/masturbation/","flag_code": ""},
{"id": "326","name": "Mature","link": "/mature/","flag_code": ""},
{"id": "278","name": "Missionary","link": "/missionary/","flag_code": ""},
{"id": "2095","name": "Model","link": "/model/","flag_code": ""},
{"id": "201","name": "Mom","link": "/mom/","flag_code": ""},
{"id": "29","name": "Natural Tits","link": "/natural-tits/","flag_code": ""},
{"id": "2540","name": "Nun","link": "/nun/","flag_code": ""},
{"id": "306","name": "Nurse","link": "/nurse/","flag_code": ""},
{"id": "356","name": "Office","link": "/office/","flag_code": ""},
{"id": "3046","name": "Orgasm","link": "/orgasm/","flag_code": ""},
{"id": "337","name": "Outdoor","link": "/outdoor/","flag_code": ""},
{"id": "257","name": "POV","link": "/pov/","flag_code": ""},
{"id": "259","name": "Panties","link": "/panties/","flag_code": ""},
{"id": "279","name": "Pantyhose","link": "/pantyhose/","flag_code": ""},
{"id": "264","name": "Petite","link": "/petite/","flag_code": ""},
{"id": "1906","name": "Pinay","link": "/pinay/","flag_code": ""},
{"id": "283","name": "Pissing","link": "/pissing/","flag_code": ""},
{"id": "243","name": "Pornstar","link": "/pornstar/","flag_code": ""},
{"id": "260","name": "Pregnant","link": "/pregnant/","flag_code": ""},
{"id": "298","name": "Public","link": "/public/","flag_code": ""},
{"id": "238","name": "Pussy","link": "/pussy/","flag_code": ""},
{"id": "322","name": "Pussy Licking","link": "/pussy-licking/","flag_code": ""},
{"id": "246","name": "Redhead","link": "/redhead/","flag_code": ""},
{"id": "5","name": "Russian","link": "/russian/","flag_code": "ru"},
{"id": "225","name": "Saggy Tits","link": "/saggy-tits/","flag_code": ""},
{"id": "270","name": "Schoolgirl","link": "/schoolgirl/","flag_code": ""},
{"id": "325","name": "Selfie","link": "/selfie/","flag_code": ""},
{"id": "1775","name": "Sex Doll","link": "/sex-doll/","flag_code": ""},
{"id": "2565","name": "Sexy","link": "/sexy/","flag_code": ""},
{"id": "1","name": "Shemale","link": "/shemale/","flag_code": "xy"},
{"id": "292","name": "Short Hair","link": "/short-hair/","flag_code": ""},
{"id": "305","name": "Shower","link": "/shower/","flag_code": ""},
{"id": "249","name": "Skinny","link": "/skinny/","flag_code": ""},
{"id": "236","name": "Skirt","link": "/skirt/","flag_code": ""},
{"id": "1827","name": "Solo","link": "/solo/","flag_code": ""},
{"id": "1880","name": "Step Sister","link": "/step-sister/","flag_code": ""},
{"id": "1828","name": "Stepmom","link": "/stepmom/","flag_code": ""},
{"id": "247","name": "Stockings","link": "/stockings/","flag_code": ""},
{"id": "256","name": "Tall","link": "/tall/","flag_code": ""},
{"id": "311","name": "Tattoo","link": "/tattoo/","flag_code": ""},
{"id": "281","name": "Teacher","link": "/teacher/","flag_code": ""},
{"id": "237","name": "Teen","link": "/teens/","flag_code": ""},
{"id": "265","name": "Thai","link": "/thai/","flag_code": "th"},
{"id": "2032","name": "Thick","link": "/thick/","flag_code": ""},
{"id": "230","name": "Threesome","link": "/threesome/","flag_code": ""},
{"id": "934","name": "Ukrainian","link": "/ukrainian/","flag_code": "ua"},
{"id": "273","name": "Upskirt","link": "/upskirt/","flag_code": ""},
{"id": "61","name": "Vintage","link": "/vintage/","flag_code": ""},
{"id": "2544","name": "White","link": "/white/","flag_code": ""},
{"id": "2775","name": "Yoga","link": "/yoga/","flag_code": ""},
{"id": "275","name": "Yoga Pants","link": "/yoga-pants/","flag_code": ""}
]

View File

@ -0,0 +1,120 @@
[
{"id": "3089","name": "人工智能","link": "/zh/ai/","flag_code": ""},
{"id": "226","name": "新手","link": "/zh/amateur/","flag_code": ""},
{"id": "224","name": "肛门","link": "/zh/anal/","flag_code": ""},
{"id": "231","name": "肛门间隙","link": "/zh/anal-gape/","flag_code": ""},
{"id": "1853","name": "阿拉伯人","link": "/zh/arab/","flag_code": ""},
{"id": "1163","name": "腋下","link": "/zh/armpit/","flag_code": ""},
{"id": "223","name": "亚洲人","link": "/zh/asian/","flag_code": ""},
{"id": "254","name": "屁股","link": "/zh/ass/","flag_code": ""},
{"id": "1707","name": "混蛋","link": "/zh/asshole/","flag_code": ""},
{"id": "1847","name": "无套","link": "/zh/bbc/","flag_code": ""},
{"id": "258","name": "性感胖女人","link": "/zh/bbw/","flag_code": ""},
{"id": "1778","name": "BDSM","link": "/zh/bdsm/","flag_code": ""},
{"id": "266","name": "海滩","link": "/zh/beach/","flag_code": ""},
{"id": "2749","name": "美丽","link": "/zh/beautiful/","flag_code": ""},
{"id": "930","name": "大阴蒂","link": "/zh/big-clit/","flag_code": ""},
{"id": "329","name": "大屌","link": "/zh/big-cock/","flag_code": ""},
{"id": "232","name": "大山雀","link": "/zh/big-tits/","flag_code": ""},
{"id": "63","name": "比基尼","link": "/zh/bikini/","flag_code": ""},
{"id": "361","name": "金发女郎","link": "/zh/blonde/","flag_code": ""},
{"id": "331","name": "口交","link": "/zh/blowjob/","flag_code": ""},
{"id": "364","name": "奴役","link": "/zh/bondage/","flag_code": ""},
{"id": "280","name": "巴西人","link": "/zh/brazilian/","flag_code": "br"},
{"id": "334","name": "深褐色头发的白人女子","link": "/zh/brunette/","flag_code": ""},
{"id": "304","name": "颜射","link": "/zh/bukkake/","flag_code": ""},
{"id": "285","name": "骆驼趾","link": "/zh/cameltoe/","flag_code": ""},
{"id": "2106","name": "中国人","link": "/zh/chinese/","flag_code": "cn"},
{"id": "41","name": "胖胖","link": "/zh/chubby/","flag_code": ""},
{"id": "295","name": "近拍","link": "/zh/close-up/","flag_code": ""},
{"id": "244","name": "角色扮演","link": "/zh/cosplay/","flag_code": ""},
{"id": "288","name": "女牛仔","link": "/zh/cowgirl/","flag_code": ""},
{"id": "342","name": "体内射精","link": "/zh/creampie/","flag_code": ""},
{"id": "43","name": "外遇","link": "/zh/cuckold/","flag_code": ""},
{"id": "241","name": "口腔内射精","link": "/zh/cum-in-mouth/","flag_code": ""},
{"id": "336","name": "阴道内射精","link": "/zh/cum-in-pussy/","flag_code": ""},
{"id": "339","name": "射精","link": "/zh/cumshot/","flag_code": ""},
{"id": "1927","name": "曲线","link": "/zh/curvy/","flag_code": ""},
{"id": "2031","name": "可爱","link": "/zh/cute/","flag_code": ""},
{"id": "368","name": "深喉","link": "/zh/deepthroat/","flag_code": ""},
{"id": "341","name": "假阳具","link": "/zh/dildo/","flag_code": ""},
{"id": "129","name": "后入式","link": "/zh/doggystyle/","flag_code": ""},
{"id": "251","name": "双龙","link": "/zh/double-penetration/","flag_code": ""},
{"id": "1706","name": "连衣裙","link": "/zh/dress/","flag_code": ""},
{"id": "234","name": "黑美人","link": "/zh/ebonies/","flag_code": ""},
{"id": "359","name": "面部的","link": "/zh/facial/","flag_code": ""},
{"id": "347","name": "脚","link": "/zh/feet/","flag_code": ""},
{"id": "348","name": "女性主导","link": "/zh/femdom/","flag_code": ""},
{"id": "2017","name": "菲律宾人","link": "/zh/filipina/","flag_code": "ph"},
{"id": "272","name": "猛攻","link": "/zh/gangbang/","flag_code": ""},
{"id": "23","name": "同性恋","link": "/zh/gay/","flag_code": "yy"},
{"id": "267","name": "玻璃","link": "/zh/glasses/","flag_code": ""},
{"id": "228","name": "奶奶","link": "/zh/granny/","flag_code": ""},
{"id": "1711","name": "健身房","link": "/zh/gym/","flag_code": ""},
{"id": "227","name": "多毛","link": "/zh/hairy/","flag_code": ""},
{"id": "353","name": "手冲","link": "/zh/handjob/","flag_code": ""},
{"id": "245","name": "硬核","link": "/zh/hardcore/","flag_code": ""},
{"id": "3090","name": "仙台","link": "/zh/hentai/","flag_code": ""},
{"id": "333","name": "高跟鞋","link": "/zh/high-heels/","flag_code": ""},
{"id": "314","name": "印度人","link": "/zh/indian/","flag_code": "in"},
{"id": "355","name": "日本人","link": "/zh/japanese/","flag_code": "jp"},
{"id": "25","name": "韩国人","link": "/zh/korean/","flag_code": "kr"},
{"id": "3","name": "跨性别","link": "/zh/ladyboy/","flag_code": "xy"},
{"id": "345","name": "拉丁美洲人","link": "/zh/latina/","flag_code": ""},
{"id": "229","name": "女同性恋","link": "/zh/lesbian/","flag_code": "xx"},
{"id": "330","name": "女士内衣","link": "/zh/lingerie/","flag_code": ""},
{"id": "239","name": "MILF","link": "/zh/milf/","flag_code": ""},
{"id": "346","name": "女仆","link": "/zh/maid/","flag_code": ""},
{"id": "277","name": "按摩","link": "/zh/massage/","flag_code": ""},
{"id": "290","name": "手淫","link": "/zh/masturbation/","flag_code": ""},
{"id": "326","name": "成熟","link": "/zh/mature/","flag_code": ""},
{"id": "278","name": "传教士","link": "/zh/missionary/","flag_code": ""},
{"id": "2095","name": "模特","link": "/zh/model/","flag_code": ""},
{"id": "201","name": "妈妈","link": "/zh/mom/","flag_code": ""},
{"id": "29","name": "天然山雀","link": "/zh/natural-tits/","flag_code": ""},
{"id": "2540","name": "修女","link": "/zh/nun/","flag_code": ""},
{"id": "306","name": "护士","link": "/zh/nurse/","flag_code": ""},
{"id": "356","name": "办公室","link": "/zh/office/","flag_code": ""},
{"id": "3046","name": "性高潮","link": "/zh/orgasm/","flag_code": ""},
{"id": "337","name": "戶外的","link": "/zh/outdoor/","flag_code": ""},
{"id": "257","name": "第一视角","link": "/zh/pov/","flag_code": ""},
{"id": "259","name": "童裤","link": "/zh/panties/","flag_code": ""},
{"id": "279","name": "连裤袜","link": "/zh/pantyhose/","flag_code": ""},
{"id": "264","name": "娇小","link": "/zh/petite/","flag_code": ""},
{"id": "1906","name": "菲律宾人XXX","link": "/zh/pinay/","flag_code": ""},
{"id": "283","name": "撒尿","link": "/zh/pissing/","flag_code": ""},
{"id": "243","name": "色情明星","link": "/zh/pornstar/","flag_code": ""},
{"id": "260","name": "怀孕","link": "/zh/pregnant/","flag_code": ""},
{"id": "298","name": "公開地","link": "/zh/public/","flag_code": ""},
{"id": "238","name": "阴户","link": "/zh/pussy/","flag_code": ""},
{"id": "322","name": "猫咪舔","link": "/zh/pussy-licking/","flag_code": ""},
{"id": "246","name": "红头发","link": "/zh/redhead/","flag_code": ""},
{"id": "5","name": "俄罗斯人","link": "/zh/russian/","flag_code": "ru"},
{"id": "225","name": "下垂的乳头","link": "/zh/saggy-tits/","flag_code": ""},
{"id": "270","name": "女学生","link": "/zh/schoolgirl/","flag_code": ""},
{"id": "325","name": "自拍","link": "/zh/selfie/","flag_code": ""},
{"id": "1775","name": "性爱娃娃","link": "/zh/sex-doll/","flag_code": ""},
{"id": "2565","name": "性感","link": "/zh/sexy/","flag_code": ""},
{"id": "1","name": "人妖","link": "/zh/shemale/","flag_code": "xy"},
{"id": "292","name": "短发","link": "/zh/short-hair/","flag_code": ""},
{"id": "305","name": "淋浴器","link": "/zh/shower/","flag_code": ""},
{"id": "249","name": "极瘦","link": "/zh/skinny/","flag_code": ""},
{"id": "236","name": "短裙","link": "/zh/skirt/","flag_code": ""},
{"id": "1827","name": "獨奏","link": "/zh/solo/","flag_code": ""},
{"id": "1880","name": "继姐","link": "/zh/step-sister/","flag_code": ""},
{"id": "1828","name": "继母","link": "/zh/stepmom/","flag_code": ""},
{"id": "247","name": "长筒袜","link": "/zh/stockings/","flag_code": ""},
{"id": "256","name": "高","link": "/zh/tall/","flag_code": ""},
{"id": "311","name": "纹身","link": "/zh/tattoo/","flag_code": ""},
{"id": "281","name": "教师","link": "/zh/teacher/","flag_code": ""},
{"id": "237","name": "青少年","link": "/zh/teens/","flag_code": ""},
{"id": "265","name": "泰国人","link": "/zh/thai/","flag_code": "th"},
{"id": "2032","name": "厚","link": "/zh/thick/","flag_code": ""},
{"id": "230","name": "三人","link": "/zh/threesome/","flag_code": ""},
{"id": "934","name": "乌克兰人","link": "/zh/ukrainian/","flag_code": "ua"},
{"id": "273","name": "裙底风光","link": "/zh/upskirt/","flag_code": ""},
{"id": "61","name": "古典","link": "/zh/vintage/","flag_code": ""},
{"id": "2544","name": "白人","link": "/zh/white/","flag_code": ""},
{"id": "2775","name": "瑜伽","link": "/zh/yoga/","flag_code": ""},
{"id": "275","name": "瑜伽裤","link": "/zh/yoga-pants/","flag_code": ""}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
chapter.name.default=Gallery
config-category-title=Categories
config-category-summary=Categories for popular and recently updated lists
config-category-option-default=Default
config-category-option-asian=Asian
config-category-option-chinese=Chinese
config-category-option-korean=Korean
config-category-option-japanese=Japanese
config-category-option-russian=Russian
config-category-option-ukrainian=Ukrainian
config-category-option-big-tits=Big tits
config-category-option-natural-tits=Natural tits
config-category-option-cosplay=Cosplay
config-category-option-cute=Cute
config-category-option-glasses=Glasses
config-category-option-maid=Maid
config-category-option-nurse=Nurse
config-category-option-nun=Nun
config-category-option-stockings=Stockings
config-category-option-twins=Twins
filter.header.ignored-when-search=These filters will be ignored when text search is active!
filter.header.select-active-category-type=Set activate category
filter.header.select-category-type-param=Set category parameters
filter.time.title=sort
filter.time.option.popular=Most Popular
filter.time.option.recent=Most Recent
filter.active-category-type.title=Active categories
filter.active-category-type.option.recommend=Recommend
filter.active-category-type.option.categories=Categories
filter.active-category-type.option.tags=Tags
filter.active-category-type.option.porn-star=Porn Star
filter.active-category-type.option.channels=Channels
filter.category-type.recommend.title=Recommend
filter.category-type.categories.title=Categories
filter.category-type.tags.title=Tags
filter.category-type.porn-star.title=Porn Star
filter.category-type.channels.title=Channels

View File

@ -0,0 +1,40 @@
chapter.name.default=画廊
config-category-title=分类
config-category-summary=分类用于热门和最近更新列表
config-category-option-default=默认
config-category-option-asian=亚洲人
config-category-option-chinese=中国人
config-category-option-korean=韩国人
config-category-option-japanese=日本人
config-category-option-russian=俄罗斯人
config-category-option-ukrainian=乌克兰人
config-category-option-big-tits=巨乳
config-category-option-natural-tits=天然巨乳
config-category-option-cosplay=角色扮演
config-category-option-cute=可爱
config-category-option-glasses=眼镜
config-category-option-maid=女仆
config-category-option-nurse=护士
config-category-option-nun=修女
config-category-option-stockings=丝袜
config-category-option-twins=双胞胎
filter.header.ignored-when-search=以下过滤在文本搜索时会被忽略!
filter.header.select-active-category-type=选择激活分类类型
filter.header.select-category-type-param=选择分类类型参数
filter.time.title=排序
filter.time.option.popular=最受欢迎
filter.time.option.recent=最近更新
filter.active-category-type.title=激活分类类型
filter.active-category-type.option.recommend=推荐
filter.active-category-type.option.categories=分类
filter.active-category-type.option.tags=标签
filter.active-category-type.option.porn-star=色情明星
filter.active-category-type.option.channels=频道
filter.category-type.recommend.title=推荐
filter.category-type.categories.title=分类
filter.category-type.tags.title=标签
filter.category-type.porn-star.title=色情明星
filter.category-type.channels.title=频道

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
[
{"name":"Asian","categoryTypeName": "CATEGORY","link": "/asian"},
{"name":"Chinese","categoryTypeName": "CATEGORY","link": "/chinese"},
{"name":"Korean","categoryTypeName": "CATEGORY","link": "/korean"},
{"name":"Japanese","categoryTypeName": "CATEGORY","link": "/japanese"},
{"name":"Russian","categoryTypeName": "CATEGORY","link": "/russian"},
{"name":"Ukrainian","categoryTypeName": "CATEGORY","link": "/ukrainian"},
{"name":"Big tits","categoryTypeName": "CATEGORY","link": "/big-tits"},
{"name":"Natural tits","categoryTypeName": "CATEGORY","link": "/natural-tits"},
{"name":"Cosplay","categoryTypeName": "CATEGORY","link": "/cosplay"},
{"name":"Cute","categoryTypeName": "CATEGORY","link": "/cute"},
{"name":"Glasses","categoryTypeName": "CATEGORY","link": "/glasses"},
{"name":"Maid","categoryTypeName": "CATEGORY","link": "/maid"},
{"name":"Nurse","categoryTypeName": "CATEGORY","link": "/nurse"},
{"name":"Nun","categoryTypeName": "CATEGORY","link": "/nun"},
{"name":"Stockings","categoryTypeName": "CATEGORY","link": "/stockings"},
{"name":"Twins","categoryTypeName": "CATEGORY","link": "/twins"},
{"name": "Schoolgirl", "categoryTypeName": "CATEGORY", "link": "/schoolgirl/"},
{"name": "Cowgirl", "categoryTypeName": "CATEGORY", "link": "/cowgirl/"},
{"name": "Beautiful", "categoryTypeName": "CATEGORY", "link": "/beautiful/"},
{"name": "Orgasm", "categoryTypeName": "CATEGORY", "link": "/orgasm/"},
{"name": "High Heels", "categoryTypeName": "CATEGORY", "link": "/high-heels/"},
{"name": "Onlyfans", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=onlyfans"},
{"name": "Patreon koreanbeautygirl", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+koreanbeautygirl"},
{"name": "Patreon Hornycos", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+hornycos"},
{"name": "Patreon Scarletai", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+scarletai"}
]

View File

@ -0,0 +1,29 @@
[
{"name": "亚洲人", "categoryTypeName": "CATEGORY", "link": "/zh/asian"},
{"name": "中国人", "categoryTypeName": "CATEGORY", "link": "/zh/chinese"},
{"name": "韩国人", "categoryTypeName": "CATEGORY", "link": "/zh/korean"},
{"name": "日本人", "categoryTypeName": "CATEGORY", "link": "/zh/japanese"},
{"name": "俄罗斯人", "categoryTypeName": "CATEGORY", "link": "/zh/russian"},
{"name": "乌克兰人", "categoryTypeName": "CATEGORY", "link": "/zh/ukrainian"},
{"name": "巨乳", "categoryTypeName": "CATEGORY", "link": "/zh/big-tits"},
{"name": "天然巨乳", "categoryTypeName": "CATEGORY", "link": "/zh/natural-tits"},
{"name": "角色扮演", "categoryTypeName": "CATEGORY", "link": "/zh/cosplay"},
{"name": "可爱", "categoryTypeName": "CATEGORY", "link": "/zh/cute"},
{"name": "眼镜", "categoryTypeName": "CATEGORY", "link": "/zh/glasses"},
{"name": "女仆", "categoryTypeName": "CATEGORY", "link": "/zh/maid"},
{"name": "护士", "categoryTypeName": "CATEGORY", "link": "/zh/nurse"},
{"name": "修女", "categoryTypeName": "CATEGORY", "link": "/zh/nun"},
{"name": "丝袜", "categoryTypeName": "CATEGORY", "link": "/zh/stockings"},
{"name": "双胞胎", "categoryTypeName": "CATEGORY", "link": "/zh/twins"},
{"name": "女学生", "categoryTypeName": "CATEGORY", "link": "/zh/schoolgirl/"},
{"name": "女牛仔", "categoryTypeName": "CATEGORY", "link": "/zh/cowgirl/"},
{"name": "美丽", "categoryTypeName": "CATEGORY", "link": "/zh/beautiful/"},
{"name": "性高潮", "categoryTypeName": "CATEGORY", "link": "/zh/orgasm/"},
{"name": "高跟鞋", "categoryTypeName": "CATEGORY", "link": "/zh/high-heels/"},
{"name": "Onlyfans", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=onlyfans"},
{"name": "Patreon 韩国美女", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+koreanbeautygirl"},
{"name": "Patreon Horny COS", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+hornycos"},
{"name": "Patreon Scarlet AI", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+scarletai"}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
ext {
extName = 'PornPics'
extClass = '.PornPics'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:i18n'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.extension.all.pornpics
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okio.FileNotFoundException
import okio.IOException
import uy.kohesive.injekt.injectLazy
object JsonFileLoader {
val json: Json by injectLazy()
val classLoader = this::class.java.classLoader!!
inline fun <reified T> loadJsonAs(fileName: String): T {
val fileContent = classLoader.getResourceAsStream(fileName)
?: throw FileNotFoundException("Cannot find JSON file: $fileName")
try {
return json.decodeFromStream<T>(fileContent)
} catch (e: SerializationException) {
throw IllegalArgumentException("Failed to parse JSON file: $fileName", e)
} catch (e: IOException) {
throw IOException("Failed to read JSON file: $fileName", e)
}
}
inline fun <reified T> loadLangJsonAs(name: String, lang: String): T {
val fileName = "assets/i18n/${name}_$lang.json"
return loadJsonAs<T>(fileName)
}
}

View File

@ -0,0 +1,209 @@
package eu.kanade.tachiyomi.extension.all.pornpics
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstance
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class PornPics() : SimpleParsedHttpSource(), ConfigurableSource {
override val baseUrl = "https://www.pornpics.com"
override val lang = "all"
override val name = "PornPics"
override val supportsLatest = true
private val preferences = getPreferences()
private val intl = Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "zh"),
classLoader = this::class.java.classLoader!!,
)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
PornPicsPreferences.buildPreferences(screen.context, intl).forEach(screen::addPreference)
}
override fun simpleMangaSelector() = "#main li.thumbwook > a.rel-link"
override fun simpleMangaFromElement(element: Element) = throw UnsupportedOperationException()
@Serializable
internal data class MangaDto(
val desc: String,
@SerialName("g_url")
val url: String,
@SerialName("t_url")
val thumbnailUrl: String,
)
override fun simpleMangaParse(response: Response): MangasPage {
// the default list is always JSON,
// the search list is always JSON,
// the first page of other classification lists is HTML, and other pages are JSON
val url = response.request.url
val isSearch = url.queryParameter("q") != null
val isDefault = url.queryParameter("period") != null
val offset = url.queryParameter("offset")!!.toInt()
val responseAsJson = isSearch || isDefault || offset > 0
val mangas = if (responseAsJson) {
val data = response.parseAs<List<MangaDto>>()
data.map {
SManga.create().apply {
setUrlWithoutDomain(it.url)
title = it.desc
thumbnail_url = it.thumbnailUrl
}
}
} else {
val doc = response.asJsoup()
doc.select(simpleMangaSelector()).map {
val imgEl = it.selectFirst("img")!!
SManga.create().apply {
setUrlWithoutDomain(it.absUrl("href"))
title = imgEl.attr("alt")
thumbnail_url = imgEl.absUrl("data-src")
}
}
}
// response maybe [], Add +1 to requested image count per page,
// Compare actual received count with pageSize to determine next page.
val hasNextPage = mangas.size > QUERY_PAGE_SIZE
val readerMangas = if (hasNextPage) mangas.dropLast(1) else mangas
return MangasPage(readerMangas, hasNextPage)
}
override fun simpleNextPageSelector(): String? = null
override fun popularMangaRequest(page: Int) = buildMangasPageRequest(page, true)
override fun latestUpdatesRequest(page: Int) = buildMangasPageRequest(page, false)
private fun buildMangasPageRequest(page: Int, popular: Boolean): Request {
val categoryOption = PornPicsPreferences.getCategoryOption(preferences)
if (PornPicsPreferences.DEFAULT_CATEGORY_OPTION == categoryOption) {
// the source of is the options under the pics menu in the nav bar
val period = if (popular) 1 else 2
val categoryId = 2585 + period
return "$baseUrl/popular/api/galleries/list".toHttpUrl().newBuilder()
.addQueryParameterPage(page)
.addQueryParameter("lang", intl.chosenLanguage)
.addQueryParameter("period", period)
.addQueryParameter("category_id", categoryId)
.build()
.let { GET(it, headers) }
}
// the source is the options under the categories/tags/pornstars/channels menu in the nav bar
val requestBaseUrl = if (popular) "$baseUrl/$categoryOption" else "$baseUrl/$categoryOption/recent"
return requestBaseUrl.toHttpUrl().newBuilder()
.addQueryParameterPage(page)
.addQueryParameter("lang", intl.chosenLanguage)
.build()
.let { GET(it, headers) }
}
override fun mangaDetailsParse(document: Document): SManga {
val thumbEl = document.selectFirst(simpleMangaSelector())!!
val imgEl = thumbEl.selectFirst("img")!!
val infoEl = document.selectFirst("div.gallery-info.to-gall-info")
return SManga.create().apply {
title = imgEl.attr("alt")
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
author = infoEl?.select("div.gallery-info__item:nth-child(2) a")?.joinToString { it.text() }
genre = infoEl?.select("div.gallery-info__item:not(:nth-child(2)) a")?.joinToString { it.text() }
description = infoEl?.selectFirst("div.gallery-info__item:nth-child(4)")?.text()
}
}
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(manga)
.map {
SChapter.create().apply {
chapter_number = 0F
setUrlWithoutDomain(it.url)
name = intl["chapter.name.default"]
}.let { listOf(it) }
}
}
override fun pageListParse(document: Document): List<Page> {
return document.select(simpleMangaSelector())
.mapIndexed { index, element ->
Page(index, imageUrl = element.absUrl("href"))
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isBlank()) {
buildCategoryRequest(page, filters)
} else {
buildSearchRequest(page, query, filters)
}
}
private fun buildCategoryRequest(
page: Int,
filters: FilterList,
): Request {
val activeCategoryTypeOption = filters.firstInstance<PornPicsFilters.ActiveCategoryTypeSelector>()
val categoryOption = activeCategoryTypeOption.selectedCategoryOption(filters)
val sortOption = filters.firstInstance<PornPicsFilters.SortSelector>()
val builder = baseUrl.toHttpUrl().newBuilder()
.addUrlPart(categoryOption.toUrlPart())
.addQueryParameter("lang", intl.chosenLanguage)
.addUrlPart(sortOption.toUriPart(), addPath = !categoryOption.useSearch())
.addQueryParameterPage(page)
return GET(builder.build(), headers)
}
private fun buildSearchRequest(page: Int, query: String, filters: FilterList): Request {
val sortOption = filters.firstInstance<PornPicsFilters.SortSelector>()
val builder = "$baseUrl/search/srch.php".toHttpUrl().newBuilder()
.addQueryParameter("lang", intl.chosenLanguage)
.addUrlPart(sortOption.toUriPart(), addPath = false)
.addQueryParameterPage(page)
.addQueryParameter("q", query)
return GET(builder.build(), headers)
}
override fun getFilterList() = FilterList(
PornPicsFilters.createSortSelector(intl),
Filter.Separator(),
Filter.Header(intl["filter.header.ignored-when-search"]),
Filter.Separator(),
Filter.Header(intl["filter.header.select-active-category-type"]),
PornPicsFilters.createActiveCategoryTypeSelector(intl),
Filter.Separator(),
Filter.Header(intl["filter.header.select-category-type-param"]),
PornPicsFilters.createRecommendSelector(intl),
PornPicsFilters.createCategorySelector(intl),
PornPicsFilters.createTagSelector(intl),
PornPicsFilters.createPornStarSelector(intl),
PornPicsFilters.createChannelSelector(intl),
)
}

View File

@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.extension.all.pornpics
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
internal const val QUERY_PAGE_SIZE = 19
internal fun HttpUrl.Builder.addQueryParameter(encodedName: String, encodedValue: Int) =
addQueryParameter(encodedName, encodedValue.toString())
internal fun HttpUrl.Builder.addQueryParameterPage(page: Int) =
// Add +1 to requested image count per page,
// Compare actual received count with pageSize to determine next page.
this.addQueryParameter("limit", QUERY_PAGE_SIZE + 1)
.addQueryParameter("offset", (page - 1) * QUERY_PAGE_SIZE)
internal fun HttpUrl.Builder.addUrlPart(
urlPart: String?,
addPath: Boolean = true,
addQuery: Boolean = true,
): HttpUrl.Builder {
if (urlPart == null) {
return this
}
val httpUrl = "https://fake.com/$urlPart".toHttpUrl()
if (addPath) {
httpUrl.pathSegments.forEach { addPathSegment(it) }
}
if (addQuery) {
httpUrl.queryParameterNames.forEach { name ->
httpUrl.queryParameterValues(name).forEach { value ->
addQueryParameter(name, value)
}
}
}
return this
}
fun addUrlPart(part: String) {
"https://www.baidu.com/01/?ppppp=5&".toHttpUrl().newBuilder()
.addUrlPart(part)
.build()
.let { println(it) }
}

View File

@ -0,0 +1,145 @@
package eu.kanade.tachiyomi.extension.all.pornpics
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import kotlinx.serialization.Serializable
import java.util.Locale
class PornPicsFilters {
data class SortOption(val name: String, val urlPart: String? = null) {
override fun toString() = this.name
}
class SortSelector(name: String, private val vals: Array<SortOption>) :
Filter.Select<SortOption>(name, vals) {
fun toUriPart() = vals[state].urlPart
}
enum class CategoryType() {
RECOMMEND,
CATEGORY,
TAG,
PORN_STAR,
CHANNEL,
;
companion object {
val mNameToCategoryType = CategoryType.values().associateBy { it.name.lowercase(Locale.US) }
fun of(name: String) = mNameToCategoryType[name.lowercase(Locale.US)]!!
}
}
data class CategoryOption(val name: String, val type: CategoryType, val urlPart: String) {
override fun toString() = this.name
fun toUrlPart() = urlPart
fun useSearch() = urlPart.contains("/search/srch.php?")
}
data class ActiveCategoryOption(val name: String, val categoryType: CategoryType?) {
override fun toString() = this.name
}
class ActiveCategoryTypeSelector(name: String, values: Array<ActiveCategoryOption>) :
Filter.Select<ActiveCategoryOption>(name, values) {
fun selected() = values[state].categoryType!!
fun selectedCategoryOption(filters: FilterList): CategoryOption {
val selectors = filters.filterIsInstance<CategorySelector>()
val selected = selected()
return selectors[selected.ordinal].selected()
}
}
class CategorySelector(name: String, values: Array<CategoryOption>) : Filter.Select<CategoryOption>(name, values) {
fun selected() = values[state]
}
companion object {
fun createSortSelector(intl: Intl) = SortSelector(
intl["filter.time.title"],
arrayOf(
SortOption(intl["filter.time.option.popular"]),
SortOption(intl["filter.time.option.recent"], "recent?date=latest"),
),
)
fun createActiveCategoryTypeSelector(intl: Intl) = ActiveCategoryTypeSelector(
intl["filter.active-category-type.title"],
arrayOf(
ActiveCategoryOption(intl["filter.active-category-type.option.recommend"], CategoryType.RECOMMEND),
ActiveCategoryOption(intl["filter.active-category-type.option.categories"], CategoryType.CATEGORY),
ActiveCategoryOption(intl["filter.active-category-type.option.tags"], CategoryType.TAG),
ActiveCategoryOption(intl["filter.active-category-type.option.porn-star"], CategoryType.PORN_STAR),
ActiveCategoryOption(intl["filter.active-category-type.option.channels"], CategoryType.CHANNEL),
),
)
@Serializable
data class RecommendCategoryDto(val name: String, val categoryTypeName: String, val link: String)
fun createRecommendSelector(intl: Intl): CategorySelector {
val options = JsonFileLoader.loadLangJsonAs<List<RecommendCategoryDto>>(
"recommend_categories",
intl.chosenLanguage,
)
.map { CategoryOption(it.name, CategoryType.of(it.categoryTypeName), it.link) }
.sortedBy { it.name }
.toTypedArray()
return CategorySelector(
intl["filter.category-type.recommend.title"],
options,
)
}
@Serializable
data class CategoryDto(val name: String, val link: String)
fun createCategorySelector(intl: Intl): CategorySelector {
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("categories", intl.chosenLanguage)
.map { CategoryOption(it.name, CategoryType.CATEGORY, it.link) }
.sortedBy { it.name }
.toTypedArray()
return CategorySelector(
intl["filter.category-type.categories.title"],
options,
)
}
fun createTagSelector(intl: Intl): CategorySelector {
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("tags", intl.chosenLanguage)
.map { CategoryOption(it.name, CategoryType.TAG, it.link) }
.sortedBy { it.name }
.toTypedArray()
return CategorySelector(
intl["filter.category-type.tags.title"],
options,
)
}
fun createPornStarSelector(intl: Intl): CategorySelector {
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("porn_stars", intl.chosenLanguage)
.map { CategoryOption(it.name, CategoryType.PORN_STAR, it.link) }
.sortedBy { it.name }
.toTypedArray()
return CategorySelector(
intl["filter.category-type.porn-star.title"],
options,
)
}
fun createChannelSelector(intl: Intl): CategorySelector {
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("channels", intl.chosenLanguage)
.map { CategoryOption(it.name, CategoryType.CHANNEL, it.link) }
.sortedBy { it.name }
.toTypedArray()
return CategorySelector(
intl["filter.category-type.channels.title"],
options,
)
}
}
}

View File

@ -0,0 +1,56 @@
package eu.kanade.tachiyomi.extension.all.pornpics
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.ListPreference
import eu.kanade.tachiyomi.lib.i18n.Intl
object PornPicsPreferences {
private const val PS_KEY_ROOT = "PornPics"
private const val PS_KEY_CATEGORY = "$PS_KEY_ROOT::CATEGORY"
const val DEFAULT_CATEGORY_OPTION = "/default"
private fun buildCategoryOption(intl: Intl): Map<String, String> {
return linkedMapOf(
intl["config-category-option-default"] to DEFAULT_CATEGORY_OPTION,
intl["config-category-option-asian"] to "asian",
intl["config-category-option-chinese"] to "chinese",
intl["config-category-option-korean"] to "korean",
intl["config-category-option-japanese"] to "japanese",
intl["config-category-option-russian"] to "russian",
intl["config-category-option-ukrainian"] to "ukrainian",
intl["config-category-option-big-tits"] to "big-tits",
intl["config-category-option-natural-tits"] to "natural-tits",
intl["config-category-option-cosplay"] to "cosplay",
intl["config-category-option-cute"] to "cute",
intl["config-category-option-glasses"] to "glasses",
intl["config-category-option-maid"] to "maid",
intl["config-category-option-nurse"] to "nurse",
intl["config-category-option-nun"] to "nun",
intl["config-category-option-stockings"] to "stockings",
intl["config-category-option-twins"] to "twins",
)
}
fun buildPreferences(content: Context, intl: Intl): List<ListPreference> {
val options = buildCategoryOption(intl)
return listOf(
ListPreference(content).apply {
key = PS_KEY_CATEGORY
title = intl["config-category-title"]
summary = intl["config-category-summary"]
entries = options.keys.toTypedArray()
entryValues = options.values.toTypedArray()
},
)
}
fun getCategoryOption(sharedPreferences: SharedPreferences): String? {
val option = sharedPreferences.getString(PS_KEY_CATEGORY, DEFAULT_CATEGORY_OPTION)
return if (DEFAULT_CATEGORY_OPTION == option) DEFAULT_CATEGORY_OPTION else option
}
}

View File

@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.extension.all.pornpics
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
abstract class SimpleParsedHttpSource : ParsedHttpSource() {
abstract fun simpleMangaSelector(): String
abstract fun simpleMangaFromElement(element: Element): SManga
abstract fun simpleMangaParse(response: Response): MangasPage
abstract fun simpleNextPageSelector(): String?
// region popular
override fun popularMangaSelector() = simpleMangaSelector()
override fun popularMangaNextPageSelector() = simpleNextPageSelector()
override fun popularMangaParse(response: Response) = simpleMangaParse(response)
override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element)
// endregion
// region last
override fun latestUpdatesSelector() = simpleMangaSelector()
override fun latestUpdatesFromElement(element: Element) = simpleMangaFromElement(element)
override fun latestUpdatesParse(response: Response) = simpleMangaParse(response)
override fun latestUpdatesNextPageSelector() = simpleNextPageSelector()
// endregion
// region search
override fun searchMangaSelector() = simpleMangaSelector()
override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element)
override fun searchMangaParse(response: Response) = simpleMangaParse(response)
override fun searchMangaNextPageSelector() = simpleNextPageSelector()
// endregion
override fun chapterListSelector() = simpleMangaSelector()
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// endregion
}