diff --git a/src/en/hbrowse/build.gradle b/src/en/hbrowse/build.gradle new file mode 100644 index 000000000..4cbbaa803 --- /dev/null +++ b/src/en/hbrowse/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'HBrowse' + pkgNameSuffix = 'en.hbrowse' + extClass = '.HBrowse' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/hbrowse/res/mipmap-hdpi/ic_launcher.png b/src/en/hbrowse/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..b5a43f854 Binary files /dev/null and b/src/en/hbrowse/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/hbrowse/res/mipmap-mdpi/ic_launcher.png b/src/en/hbrowse/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..b3f8bf5eb Binary files /dev/null and b/src/en/hbrowse/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/hbrowse/res/mipmap-xhdpi/ic_launcher.png b/src/en/hbrowse/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..de4b9ad1d Binary files /dev/null and b/src/en/hbrowse/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/hbrowse/res/mipmap-xxhdpi/ic_launcher.png b/src/en/hbrowse/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..bd90e5c50 Binary files /dev/null and b/src/en/hbrowse/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/hbrowse/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/hbrowse/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..251593a90 Binary files /dev/null and b/src/en/hbrowse/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/hbrowse/res/web_hi_res_512.png b/src/en/hbrowse/res/web_hi_res_512.png new file mode 100644 index 000000000..3a020d8c5 Binary files /dev/null and b/src/en/hbrowse/res/web_hi_res_512.png differ diff --git a/src/en/hbrowse/src/eu/kanade/tachiyomi/extension/en/hbrowse/HBrowse.kt b/src/en/hbrowse/src/eu/kanade/tachiyomi/extension/en/hbrowse/HBrowse.kt new file mode 100644 index 000000000..08b7d2c6c --- /dev/null +++ b/src/en/hbrowse/src/eu/kanade/tachiyomi/extension/en/hbrowse/HBrowse.kt @@ -0,0 +1,198 @@ +package eu.kanade.tachiyomi.extension.en.hbrowse + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import java.io.IOException +import okhttp3.CookieJar +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +class HBrowse : ParsedHttpSource() { + + override val name = "HBrowse" + + override val baseUrl = "http://www.hbrowse.com" + + override val lang = "en" + + override val supportsLatest = true + + private val searchClient = OkHttpClient().newBuilder() + .followRedirects(false) + .cookieJar(CookieJar.NO_COOKIES) + .build() + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .followRedirects(false) + .addInterceptor { chain -> + val originalRequest = chain.request() + if (originalRequest.url().toString() == searchUrl) { + val sessionCookie = searchClient.newCall(originalRequest).execute() + .headers("Set-Cookie") + .firstOrNull { it.contains("PHPSESSID") } + ?.toString() + ?.substringBefore(";") + ?: throw IOException("PHPSESSID missing") + + val newHeaders = headersBuilder() + .add("Cookie", sessionCookie) + + val contentLength = originalRequest.body()!!.contentLength() + + searchClient.newCall(GET("$baseUrl/${if (contentLength > 8000) "result" else "search"}/1", newHeaders.build())).execute() + } else { + chain.proceed(originalRequest) + } + } + .build() + + // Popular + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/browse/title/rank/DESC/$page", headers) + } + + override fun popularMangaSelector() = "table.thumbTable tbody" + + override fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + element.select("div.thumbDiv a").let { + setUrlWithoutDomain(it.attr("href")) + title = it.attr("title").substringAfter("\'").substringBefore("\'") + } + thumbnail_url = element.select("img.thumbImg").attr("abs:src") + } + } + + override fun popularMangaNextPageSelector() = "a[title^=\"jump to next\"]" + + // Latest + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/browse/title/date/DESC/$page", headers) + } + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + // Search + + private val searchUrl = "$baseUrl/content/process.php" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val filterList = if (filters.isEmpty()) getFilterList() else filters + + val rBody = FormBody.Builder().apply { + if (query.isNotBlank()) { + add("type", "search") + add("needle", query) + } else { + add("type", "advance") + filterList.filterIsInstance() + .flatMap { it.vals } + .forEach { filter -> add(filter.formName, filter.formValue()) } + } + } + + return POST(searchUrl, headers, rBody.build()) + } + + override fun searchMangaSelector() = "tbody > tr td.browseTitle a" + + override fun searchMangaFromElement(element: Element): SManga { + return SManga.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.text() + thumbnail_url = "$baseUrl/thumbnails/${url.removePrefix("/").substringBefore("/")}_1.jpg" + } + } + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + // Details + + override fun mangaDetailsRequest(manga: SManga): Request { + return GET("$baseUrl/thumbnails${manga.url}") + } + + override fun mangaDetailsParse(document: Document): SManga { + return SManga.create().apply { + with(document.select("div#main").first()) { + title = select("td:contains(title) + td.listLong").first().text() + artist = select("td:contains(artist) + td.listLong").text() + genre = select("td:contains(genre) + td.listLong").joinToString { it.text() } + description = select("tr:has(.listLong)") + .filterNot { it.select("td:first-child").text().contains(Regex("""(Title|Artist|Genre)""")) } + .joinToString("\n") { tr -> tr.select("td").joinToString(": ") { it.text() } } + thumbnail_url = select("tbody img").first().attr("abs:src") + } + } + } + + // Chapters + + override fun chapterListSelector() = "h2:contains(read manga online) + table a:contains(chapter)" + + override fun chapterFromElement(element: Element): SChapter { + return SChapter.create().apply { + name = element.text().substringAfter("View ") + setUrlWithoutDomain(element.attr("href")) + } + } + + // Pages + + override fun pageListParse(document: Document): List { + return document.select("td.pageList a").mapIndexed { i, element -> + Page(i, element.attr("abs:href")) + } + } + + override fun imageUrlParse(document: Document): String { + return document.select("img#mangaImage").attr("abs:src") + } + + // Filters + + override fun getFilterList(): FilterList { + return FilterList(advFilterMap.map { AdvancedFilter(getAdvTriStateList(it.key, it.value.split(", "))) }) + } + + private class AdvTriStateFilter(val groupName: String, name: String) : Filter.TriState(name) { + val formName = "${groupName[0].toLowerCase() + groupName.drop(1).replace(" ", "")}_$name" + fun formValue() = when { + this.isIncluded() -> "y" + this.isExcluded() -> "n" + else -> "" + } + } + private class AdvancedFilter(val vals: List) : Filter.Group(vals.first().groupName, vals) + + private val advFilterMap = mapOf( + Pair("Genre", "action, adventure, anime, bizarre, comedy, drama, fantasy, gore, historic, horror, medieval, modern, myth, psychological, romance, school_life, scifi, supernatural, video_game, visual_novel"), + Pair("Type", "anthology, bestiality, dandere, deredere, deviant, fully_colored, furry, futanari, gender_bender, guro, harem, incest, kuudere, lolicon, long_story, netorare, non-con, partly_colored, reverse_harem, ryona, short_story, shotacon, transgender, tsundere, uncensored, vanilla, yandere, yaoi, yuri"), + Pair("Setting", "amusement_park, attic, automobile, balcony, basement, bath, beach, bedroom, cabin, castle, cave, church, classroom, deck, dining_room, doctors, dojo, doorway, dream, dressing_room, dungeon, elevator, festival, gym, haunted_building, hospital, hotel, hot_springs, kitchen, laboratory, library, living_room, locker_room, mansion, office, other, outdoor, outer_space, park, pool, prison, public, restaurant, restroom, roof, sauna, school, school_nurses_office, shower, shrine, storage_room, store, street, teachers_lounge, theater, tight_space, toilet, train, transit, virtual_reality, warehouse, wilderness"), + Pair("Fetish", "androphobia, apron, assertive_girl, bikini, bloomers, breast_expansion, business_suit, chastity_device, chinese_dress, christmas, collar, corset, cosplay_(female), cosplay_(male), crossdressing_(female), crossdressing_(male), eye_patch, food, giantess, glasses, gothic_lolita, gyaru, gynophobia, high_heels, hot_pants, impregnation, kemonomimi, kimono, knee_high_socks, lab_coat, latex, leotard, lingerie, maid_outfit, mother_and_daughter, none, nonhuman_girl, olfactophilia, pregnant, rich_girl, school_swimsuit, shy_girl, sisters, sleeping_girl, sporty, stockings, strapon, student_uniform, swimsuit, tanned, tattoo, time_stop, twins_(coed), twins_(female), twins_(male), uniform, wedding_dress"), + Pair("Role", "alien, android, angel, athlete, bride, bunnygirl, cheerleader, delinquent, demon, doctor, dominatrix, escort, foreigner, ghost, housewife, idol, magical_girl, maid, mamono, massagist, miko, mythical_being, neet, nekomimi, newlywed, ninja, normal, nun, nurse, office_lady, other, police, priest, princess, queen, school_nurse, scientist, sorcerer, student, succubus, teacher, tomboy, tutor, waitress, warrior, witch"), + Pair("Relationship", "acquaintance, anothers_daughter, anothers_girlfriend, anothers_mother, anothers_sister, anothers_wife, aunt, babysitter, childhood_friend, classmate, cousin, customer, daughter, daughter-in-law, employee, employer, enemy, fiance, friend, friends_daughter, friends_girlfriend, friends_mother, friends_sister, friends_wife, girlfriend, landlord, manager, master, mother, mother-in-law, neighbor, niece, none, older_sister, patient, pet, physician, relative, relatives_friend, relatives_girlfriend, relatives_wife, servant, server, sister-in-law, slave, stepdaughter, stepmother, stepsister, stranger, student, teacher, tutee, tutor, twin, underclassman, upperclassman, wife, workmate, younger_sister"), + Pair("Male Body", "adult, animal, animal_ears, bald, beard, dark_skin, elderly, exaggerated_penis, fat, furry, goatee, hairy, half_animal, horns, large_penis, long_hair, middle_age, monster, muscular, mustache, none, short, short_hair, skinny, small_penis, tail, tall, tanned, tan_line, teenager, wings, young"), + Pair("Female Body", "adult, animal_ears, bald, big_butt, chubby, dark_skin, elderly, elf_ears, exaggerated_breasts, fat, furry, hairy, hair_bun, half_animal, halo, hime_cut, horns, large_breasts, long_hair, middle_age, monster_girl, muscular, none, pigtails, ponytail, short, short_hair, skinny, small_breasts, tail, tall, tanned, tan_line, teenager, twintails, wings, young"), + Pair("Group", "foursome_(1_female), foursome_(1_male), foursome_(mixed), foursome_(only_female), one_on_one, one_on_one_(2_females), one_on_one_(2_males), orgy_(1_female), orgy_(1_male), orgy_(mainly_female), orgy_(mainly_male), orgy_(mixed), orgy_(only_female), orgy_(only_male), solo_(female), solo_(male), threesome_(1_female), threesome_(1_male), threesome_(only_female), threesome_(only_male)"), + Pair("Scene", "adultery, ahegao, anal_(female), anal_(male), aphrodisiac, armpit_sex, asphyxiation, blackmail, blowjob, bondage, breast_feeding, breast_sucking, bukkake, cheating_(female), cheating_(male), chikan, clothed_sex, consensual, cunnilingus, defloration, discipline, dominance, double_penetration, drunk, enema, exhibitionism, facesitting, fingering_(female), fingering_(male), fisting, footjob, grinding, groping, handjob, humiliation, hypnosis, intercrural, interracial_sex, interspecies_sex, lactation, lotion, masochism, masturbation, mind_break, nonhuman, orgy, paizuri, phone_sex, props, rape, reverse_rape, rimjob, sadism, scat, sex_toys, spanking, squirt, submission, sumata, swingers, tentacles, voyeurism, watersports, x-ray_blowjob, x-ray_sex"), + Pair("Position", "69, acrobat, arch, bodyguard, butterfly, cowgirl, dancer, deck_chair, deep_stick, doggy, drill, ex_sex, jockey, lap_dance, leg_glider, lotus, mastery, missionary, none, other, pile_driver, prison_guard, reverse_piggyback, rodeo, spoons, standing, teaspoons, unusual, victory") + ) + + private fun getAdvTriStateList(groupName: String, vals: List) = vals.map { AdvTriStateFilter(groupName, it) } +}