diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 5e33ea934..000000000 Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index dd62bd9b3..000000000 Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index deda02113..000000000 Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 3f4776373..000000000 Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 0dbbbf5d8..000000000 Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/hentaidexy/res/web_hi_res_512.png b/multisrc/overrides/madara/hentaidexy/res/web_hi_res_512.png deleted file mode 100644 index a0742ce37..000000000 Binary files a/multisrc/overrides/madara/hentaidexy/res/web_hi_res_512.png and /dev/null differ diff --git a/multisrc/overrides/madara/hentaidexy/src/Hentaidexy.kt b/multisrc/overrides/madara/hentaidexy/src/Hentaidexy.kt deleted file mode 100644 index 68ea81e5c..000000000 --- a/multisrc/overrides/madara/hentaidexy/src/Hentaidexy.kt +++ /dev/null @@ -1,13 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.hentaidexy - -import eu.kanade.tachiyomi.multisrc.madara.Madara -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import okhttp3.OkHttpClient -import java.util.concurrent.TimeUnit - -class Hentaidexy : Madara("Hentaidexy", "https://hentaidexy.net", "en") { - - override val client: OkHttpClient = super.client.newBuilder() - .rateLimit(1, 1, TimeUnit.SECONDS) - .build() -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt index 1925c392e..178f32c3c 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt @@ -127,7 +127,6 @@ class MadaraGenerator : ThemeSourceGenerator { SingleLang("Hentai Manga", "https://hentaimanga.me", "en", isNsfw = true, overrideVersionCode = 1), SingleLang("Hentai Teca", "https://hentaiteca.net", "pt-BR", isNsfw = true, overrideVersionCode = 1), SingleLang("Hentai20", "https://hentai20.io", "en", isNsfw = true, overrideVersionCode = 3), - SingleLang("Hentaidexy", "https://hentaidexy.net", "en", isNsfw = true, overrideVersionCode = 3), SingleLang("HentaiRead", "https://hentairead.com", "en", isNsfw = true, className = "Hentairead", overrideVersionCode = 3), SingleLang("HentaiWebtoon", "https://hentaiwebtoon.com", "en", isNsfw = true, overrideVersionCode = 1), SingleLang("HentaiXComic", "https://hentaixcomic.com", "en", isNsfw = true), diff --git a/src/en/hentaidexy/AndroidManifest.xml b/src/en/hentaidexy/AndroidManifest.xml new file mode 100644 index 000000000..8f35f8fe2 --- /dev/null +++ b/src/en/hentaidexy/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/en/hentaidexy/build.gradle b/src/en/hentaidexy/build.gradle new file mode 100644 index 000000000..b92960d2c --- /dev/null +++ b/src/en/hentaidexy/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Hentaidexy' + pkgNameSuffix = 'en.hentaidexy' + extClass = '.Hentaidexy' + extVersionCode = 32 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/hentaidexy/res/mipmap-hdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..39990edf3 Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/hentaidexy/res/mipmap-mdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..3f407bacd Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/hentaidexy/res/mipmap-xhdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f8e94a64c Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..20d8befa1 Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f1f73f010 Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/hentaidexy/res/web_hi_res_512.png b/src/en/hentaidexy/res/web_hi_res_512.png new file mode 100644 index 000000000..31fad88c0 Binary files /dev/null and b/src/en/hentaidexy/res/web_hi_res_512.png differ diff --git a/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/Hentaidexy.kt b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/Hentaidexy.kt new file mode 100644 index 000000000..ac7cefad8 --- /dev/null +++ b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/Hentaidexy.kt @@ -0,0 +1,206 @@ +package eu.kanade.tachiyomi.extension.en.hentaidexy + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class Hentaidexy : HttpSource() { + + override val name = "Hentaidexy" + + override val baseUrl = "https://hentaidexy.net" + + private val apiUrl = "https://backend.hentaidexy.net" + + override val lang = "en" + + override val supportsLatest = true + + override val versionId = 2 + + private val json: Json by injectLazy() + + override val client: OkHttpClient = super.client.newBuilder() + .rateLimitHost(apiUrl.toHttpUrl(), 1) + .build() + + override fun headersBuilder() = Headers.Builder().apply { + add("Referer", "$baseUrl/") + } + + // popular + override fun popularMangaRequest(page: Int): Request { + return GET("$apiUrl/api/v1/mangas?page=$page&limit=100&sort=-views", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val result = json.decodeFromString(response.body.string()) + return MangasPage( + result.mangas.map { manga -> + toSManga(manga).apply { + initialized = true + } + }, + hasNextPage = result.totalPages > result.page, + ) + } + + // latest + override fun latestUpdatesRequest(page: Int): Request { + return GET("$apiUrl/api/v1/mangas?page=$page&limit=100&sort=-updatedAt", headers) + } + + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + + // search + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + if (!query.startsWith(ID_SEARCH_PREFIX)) { + return super.fetchSearchManga(page, query, filters) + } + + val id = query.substringAfter(ID_SEARCH_PREFIX) + return fetchMangaDetails(SManga.create().apply { url = id }).map { + MangasPage(listOf(it), false) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$apiUrl/api/v1/mangas?page=$page&altTitles=$query&sort=createdAt", headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + + // manga details + override fun mangaDetailsRequest(manga: SManga): Request { + return GET("$apiUrl/api/v1/mangas/${manga.url}", headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + val mangaDetails = json.decodeFromString(response.body.string()) + return toSManga(mangaDetails.manga) + } + + override fun getMangaUrl(manga: SManga): String { + val mangaId = manga.url + val slug = manga.title.replace(' ', '-') + return "$baseUrl/manga/$mangaId/$slug" + } + + // chapter list + override fun chapterListRequest(manga: SManga): Request { + return paginatedChapterListRequest(manga.url, 1) + } + + private fun paginatedChapterListRequest(mangaID: String, page: Int): Request { + return GET("$apiUrl/api/v1/mangas/$mangaID/chapters?sort=-serialNumber&limit=100&page=$page", headers) + } + + override fun chapterListParse(response: Response): List { + val chapterListResponse = json.decodeFromString(response.body.string()) + + val mangaId = response.request.url.toString() + .substringAfter("/mangas/") + .substringBefore("/chapters") + + val totalPages = chapterListResponse.totalPages + var currentPage = 1 + + while (totalPages > currentPage) { + currentPage++ + val newRequest = paginatedChapterListRequest(mangaId, currentPage) + val newResponse = client.newCall(newRequest).execute() + val newChapterListResponse = json.decodeFromString(newResponse.body.string()) + + chapterListResponse.chapters += newChapterListResponse.chapters + } + + return chapterListResponse.chapters.map { chapter -> + SChapter.create().apply { + url = "/manga/$mangaId/chapter/${chapter._id}" + name = "Chapter " + chapter.serialNumber.parseChapterNumber() + date_upload = chapter.createdAt.parseDate() + } + } + } + + override fun getChapterUrl(chapter: SChapter): String { + return "$baseUrl${chapter.url}" + } + + // page list + override fun pageListRequest(chapter: SChapter): Request { + val chapterId = chapter.url.substringAfterLast('/') + return GET("$apiUrl/api/v1/chapters/$chapterId", headers) + } + + override fun pageListParse(response: Response): List { + val result = json.decodeFromString(response.body.string()) + return result.chapter.images.mapIndexed { index, image -> + Page(index = index, imageUrl = image) + } + } + + // unused + override fun imageUrlParse(response: Response): String { + throw UnsupportedOperationException("Not Used") + } + + // Helpers + private fun toSManga(manga: Manga): SManga { + return SManga.create().apply { + url = manga._id + title = manga.title + author = manga.authors?.joinToString { it.trim() } + artist = author + description = manga.summary.trim() + "\n\nAlternative Names: ${manga.altTitles?.joinToString { it.trim() }}" + genre = manga.genres?.joinToString { it.trim() } + status = manga.status.parseStatus() + thumbnail_url = manga.coverImage + } + } + + private fun String.parseStatus(): Int { + return when { + this.contains("ongoing", true) -> SManga.ONGOING + this.contains("complete", true) -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + } + + private fun Float.parseChapterNumber(): String { + return if (this.toInt().toFloat() == this) { + this.toInt().toString() + } else { + this.toString() + } + } + + private fun String.parseDate(): Long { + return runCatching { DATE_FORMATTER.parse(this)?.time } + .getOrNull() ?: 0L + } + + companion object { + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) + } + + const val ID_SEARCH_PREFIX = "id:" + } +} diff --git a/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyDto.kt b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyDto.kt new file mode 100644 index 000000000..24a24ac47 --- /dev/null +++ b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyDto.kt @@ -0,0 +1,51 @@ +package eu.kanade.tachiyomi.extension.en.hentaidexy + +import kotlinx.serialization.Serializable + +@Serializable +data class ApiMangaResponse( + val page: Int, + val totalPages: Int, + val mangas: Array, +) + +@Serializable +data class Manga( + val _id: String, + val title: String, + val altTitles: Array?, + val coverImage: String, + val summary: String = "Unknown", + val authors: Array?, + val genres: Array?, + val status: String, +) + +@Serializable +data class MangaDetails( + val manga: Manga, +) + +@Serializable +data class ApiChapterResponse( + val page: Int, + val totalPages: Int, + val chapters: MutableList, +) + +@Serializable +data class Chapter( + val _id: String, + val serialNumber: Float, + val createdAt: String, +) + +@Serializable +data class PageList( + val chapter: ChapterPageData, +) + +@Serializable +data class ChapterPageData( + val images: Array, +) diff --git a/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyUrlActivity.kt b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyUrlActivity.kt new file mode 100644 index 000000000..7da54a0fa --- /dev/null +++ b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyUrlActivity.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.extension.en.hentaidexy + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class HentaidexyUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val id = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${Hentaidexy.ID_SEARCH_PREFIX}$id") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("HentaidexyUrlActivity", e.toString()) + } + } else { + Log.e("HentaidexyUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}