From 6defaacb09e0b8701dd1109de7330e6ef21c4653 Mon Sep 17 00:00:00 2001 From: FourTOne5 <59261191+FourTOne5@users.noreply.github.com> Date: Wed, 13 Apr 2022 00:37:56 +0600 Subject: [PATCH] Zero Scans migrate to new site. (#11358) * Initial Commit * Update * Add space in extension name. * Review updates --- .../multisrc/genkan/GenkanGenerator.kt | 1 - src/en/zeroscans/AndroidManifest.xml | 2 + src/en/zeroscans/CHANGELOG.md | 3 + src/en/zeroscans/build.gradle | 12 + .../zeroscans/res/mipmap-hdpi/ic_launcher.png | Bin .../zeroscans/res/mipmap-mdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher.png | Bin .../en}/zeroscans/res/web_hi_res_512.png | Bin .../extension/en/zeroscans/ZeroScans.kt | 349 ++++++++++++++++++ .../extension/en/zeroscans/ZeroScansDto.kt | 116 ++++++ .../extension/en/zeroscans/ZeroScansHelper.kt | 122 ++++++ 13 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 src/en/zeroscans/AndroidManifest.xml create mode 100644 src/en/zeroscans/CHANGELOG.md create mode 100644 src/en/zeroscans/build.gradle rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-hdpi/ic_launcher.png (100%) rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-mdpi/ic_launcher.png (100%) rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-xhdpi/ic_launcher.png (100%) rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {multisrc/overrides/genkan => src/en}/zeroscans/res/web_hi_res_512.png (100%) create mode 100644 src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt create mode 100644 src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt create mode 100644 src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt index fe3cc4382..668dc728d 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt @@ -13,7 +13,6 @@ class GenkanGenerator : ThemeSourceGenerator { override val sources = listOf( SingleLang("Hunlight Scans", "https://hunlight-scans.info", "en"), - SingleLang("ZeroScans", "https://zeroscans.com", "en"), SingleLang("The Nonames Scans", "https://the-nonames.com", "en"), SingleLang("Edelgarde Scans", "https://edelgardescans.com", "en"), SingleLang("LynxScans", "https://lynxscans.com", "en", overrideVersionCode = 3), diff --git a/src/en/zeroscans/AndroidManifest.xml b/src/en/zeroscans/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/en/zeroscans/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/en/zeroscans/CHANGELOG.md b/src/en/zeroscans/CHANGELOG.md new file mode 100644 index 000000000..354ab5394 --- /dev/null +++ b/src/en/zeroscans/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.2.4 + +Migrated to the new site. \ No newline at end of file diff --git a/src/en/zeroscans/build.gradle b/src/en/zeroscans/build.gradle new file mode 100644 index 000000000..91739ec68 --- /dev/null +++ b/src/en/zeroscans/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Zero Scans' + pkgNameSuffix = 'en.zeroscans' + extClass = '.ZeroScans' + extVersionCode = 4 +} + +apply from: "$rootDir/common.gradle" diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-hdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/genkan/zeroscans/res/mipmap-hdpi/ic_launcher.png rename to src/en/zeroscans/res/mipmap-hdpi/ic_launcher.png diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-mdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/genkan/zeroscans/res/mipmap-mdpi/ic_launcher.png rename to src/en/zeroscans/res/mipmap-mdpi/ic_launcher.png diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/genkan/zeroscans/res/mipmap-xhdpi/ic_launcher.png rename to src/en/zeroscans/res/mipmap-xhdpi/ic_launcher.png diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/genkan/zeroscans/res/mipmap-xxhdpi/ic_launcher.png rename to src/en/zeroscans/res/mipmap-xxhdpi/ic_launcher.png diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/genkan/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png rename to src/en/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/multisrc/overrides/genkan/zeroscans/res/web_hi_res_512.png b/src/en/zeroscans/res/web_hi_res_512.png similarity index 100% rename from multisrc/overrides/genkan/zeroscans/res/web_hi_res_512.png rename to src/en/zeroscans/res/web_hi_res_512.png diff --git a/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt new file mode 100644 index 000000000..e35807bbe --- /dev/null +++ b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt @@ -0,0 +1,349 @@ +package eu.kanade.tachiyomi.extension.en.zeroscans + +import android.util.Log +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.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.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import rx.Single +import rx.schedulers.Schedulers +import uy.kohesive.injekt.injectLazy + +class ZeroScans : HttpSource() { + + override val name: String = "Zero Scans" + + override val lang: String = "en" + + override val baseUrl: String = "https://beta.zeroscans.com" + + override val supportsLatest: Boolean = true + + private val json: Json by injectLazy() + + private lateinit var comicList: List + + private lateinit var rankings: ZeroScansRankingsDto + + private val zsHelper = ZeroScansHelper() + + override fun fetchLatestUpdates(page: Int): Observable { + if (page == 1) runCatching { updateComicsData() } + return super.fetchLatestUpdates(page) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/$API_PATH/new-chapters") + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val newChapters = response.parseAs() + + val titlesList = newChapters.all.mapNotNull { + comicList.firstOrNull { comic -> comic.slug == it.slug } + }.map { comic -> zsHelper.zsComicEntryToSManga(comic) } + + return MangasPage(titlesList, false) + } + + override fun fetchPopularManga(page: Int): Observable { + return fetchSearchManga(page, query = "", filters = getFilterList()) + } + + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList + ): Observable { + if (page == 1) runCatching { updateComicsData() } + + filters.filterIsInstance().firstOrNull()?.let { rankingFilter -> + val type = rankingList[rankingFilter.state!!.index].type + val ascending = rankingFilter.state!!.ascending + getRankingsIfNeeded(type, ascending)?.let { + return Observable.just(MangasPage(it, false)) + } + } + + var filteredComics = comicList + + if (query.isNotBlank()) { + filteredComics = filteredComics.filter { it.name.contains(query, ignoreCase = true) } + } + + filters.forEach { filter -> + when (filter) { + is StatusFilter -> { + filteredComics = filteredComics.filter { zsHelper.checkStatusFilter(filter, it) } + } + is GenreFilter -> { + filteredComics = filteredComics.filter { zsHelper.checkGenreFilter(filter, it) } + } + is SortFilter -> { + filter.state?.let { + val type = sortList[it.index].type + val ascending = it.ascending + filteredComics = zsHelper.applySortFilter(type, ascending, filteredComics) + } + } + else -> { /* Do Nothing */ } + } + } + + // Get 20 comics at a time + val chunkedFilteredComics = filteredComics.chunked(20) + if (chunkedFilteredComics.isEmpty()) { + return Observable.just(MangasPage(emptyList(), false)) + } + + val comics = chunkedFilteredComics[page - 1].map { comic -> zsHelper.zsComicEntryToSManga(comic) } + val hasNextPage = page < chunkedFilteredComics.size + + return Observable.just(MangasPage(comics, hasNextPage)) + } + + private fun getRankingsIfNeeded(type: String?, ascending: Boolean): List? { + if (type.isNullOrBlank()) return null + + val rankingEntries = when (type) { + "weekly" -> { + if (!ascending) rankings.weekly.reversed() + else rankings.weekly + } + "monthly" -> { + if (!ascending) rankings.monthly.reversed() + else rankings.monthly + } + else -> { + if (!ascending) rankings.allTime.reversed() + else rankings.allTime + } + } + + val titlesList = rankingEntries.mapNotNull { rankingEntry -> + comicList.firstOrNull { rankingEntry.slug == it.slug } + }.map { comic -> zsHelper.zsComicEntryToSManga(comic) } + + return titlesList + } + + override fun fetchMangaDetails(manga: SManga): Observable { + runCatching { updateComicsData() } + val mangaSlug = "$baseUrl${manga.url}".toHttpUrl().pathSegments[1] + + try { + val comic = comicList.first { comic -> comic.slug == mangaSlug } + .let { zsHelper.zsComicEntryToSManga(it) } + + return Observable.just(comic) + } catch (e: NoSuchElementException) { + throw Exception("Migrate from Zero Scans to Zero Scans") + } + } + + override fun fetchChapterList(manga: SManga): Observable> { + try { + var page = 1 + + val response = client.newCall(zsChapterListRequest(page, manga)).execute() + + val zsChapterPage = zsChapterListParse(response) + + val zsChapters = zsChapterPage.chapters.toMutableList() + + var hasMoreResult = zsChapterPage.hasNextPage + + while (hasMoreResult) { + page++ + val newResponse = client.newCall(zsChapterListRequest(page, manga)).execute() + val newZSChapterPage = zsChapterListParse(newResponse) + zsChapters.addAll(newZSChapterPage.chapters) + hasMoreResult = newZSChapterPage.hasNextPage + } + + zsChapters.map { it.toSChapter(manga) }.let { + return Observable.just(it) + } + } catch (e: Exception) { + Log.e("Zero Scans", "Error parsing chapter list", e) + throw(e) + } + } + + private fun zsChapterListRequest(page: Int, manga: SManga): Request { + val mangaId = "$baseUrl${manga.url}".toHttpUrl().queryParameter("id") + return GET("$baseUrl/$API_PATH/comic/$mangaId/chapters?sort=desc&page=$page") + } + + private fun zsChapterListParse(response: Response): ZeroScansChapterPage { + return response.parseAs>() + .data.let { + ZeroScansChapterPage( + it.data, + it.currentPage < it.lastPage + ) + } + } + + class ZeroScansChapterPage( + val chapters: List, + val hasNextPage: Boolean + ) + + private fun ZeroScansChapterDto.toSChapter(manga: SManga): SChapter { + val comicSlug = "$baseUrl${manga.url}".toHttpUrl().pathSegments[1] + val zsChapter = this + return SChapter.create().apply { + name = "Chapter ${zsChapter.name}" + scanlator = zsChapter.group + chapter_number = zsChapter.name.toFloat() + date_upload = zsHelper.parseChapterUploadDate(zsChapter.createdAt) + url = "/comics/$comicSlug/${zsChapter.id}" + } + } + + override fun pageListRequest(chapter: SChapter): Request { + val chapterUrlPaths = "$baseUrl${chapter.url}".toHttpUrl().pathSegments + val mangaSlug = chapterUrlPaths[1] + val chapterId = chapterUrlPaths[2] + return GET("$baseUrl/$API_PATH/comic/$mangaSlug/chapters/$chapterId") + } + + override fun pageListParse(response: Response): List { + val allQualityZSPages = response.parseAs>().data.chapter + + val highResZSPages = allQualityZSPages.highQuality.takeIf { it.isNotEmpty() } ?: allQualityZSPages.goodQuality + val pages = highResZSPages.mapIndexed { index, url -> + Page(index, imageUrl = url) + } + + return pages + } + + // Fetch Comics, Genres, Statuses and Rankings on creating source + init { + Single.fromCallable { + runCatching { updateComicsData() } + }.subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe({}, {}) + } + + // Filters + override fun getFilterList(): FilterList { + val filters = mutableListOf>() + if (genreList.isNotEmpty()) { + filters.add(GenreFilter(genreList)) + } + if (statusList.isNotEmpty()) { + filters.add(StatusFilter(statusList)) + } + filters += listOf( + SortFilter(sortList), + RankingsHeader(), + RankingsHeader2(), + RankingsFilter(rankingList) + ) + + return FilterList(filters) + } + + class GenreFilter(genres: List) : Filter.Group("Genre", genres) + class Genre(name: String, val id: Int) : Filter.TriState(name) + + private var genreList: List = emptyList() + + class StatusFilter(statuses: List) : Filter.Group("Status", statuses) + class Status(name: String, val id: Int) : Filter.TriState(name) + + private var statusList: List = emptyList() + + class SortFilter(sorts: List) : + Filter.Sort("Sort by", sorts.map { it.name }.toTypedArray(), Selection(3, false)) + + class Sort(val name: String, val type: String) + + private val sortList = listOf( + Sort("Alphabetic", "alphabetic"), + Sort("Rating", "rating"), + Sort("Chapter Count", "chapter_count"), + Sort("Bookmark Count", "bookmark_count"), + Sort("View Count", "view_count") + ) + + class RankingsHeader : + Filter.Header("Note: Genre, Sort, Status filter and Search query") + class RankingsHeader2 : + Filter.Header("are not applied to rankings") + + class RankingsFilter(rankings: List) : + Filter.Sort("Rankings", rankings.map { it.name }.toTypedArray(), Selection(0, false)) + + class Ranking(val name: String, val type: String? = null) + + private val rankingList = listOf( + Ranking("None"), + Ranking("All Time", "all-time"), + Ranking("Weekly", "weekly"), + Ranking("Monthly", "monthly") + ) + + // Helpers + private inline fun Response.parseAs(): T = use { + json.decodeFromString(it.body?.string().orEmpty()) + } + + private fun comicsDataRequest(): Request { + return GET("$baseUrl/$API_PATH/comics") + } + + private fun comicsDataParse(response: Response): ZeroScansComicsDataDto { + return response.parseAs>().data + } + + private fun updateComicsData() { + val response = client.newCall(comicsDataRequest()).execute() + comicsDataParse(response).let { + genreList = it.genres.map { genreDto -> + Genre(genreDto.name, genreDto.id) + } + statusList = it.statuses.map { statusDto -> + Status(statusDto.name, statusDto.id) + } + comicList = it.comics + rankings = it.rankings + } + } + + // Unused Stuff + override fun imageUrlParse(response: Response): String = "" + + override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException("Not Used") + + override fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not Used") + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = + throw UnsupportedOperationException("Not Used") + + override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not Used") + + override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException("Not Used") + + override fun chapterListParse(response: Response): List = throw UnsupportedOperationException("Not Used") + + override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException("Not Used") + + companion object { + const val API_PATH = "swordflake" + } +} diff --git a/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt new file mode 100644 index 000000000..e2914f1e5 --- /dev/null +++ b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt @@ -0,0 +1,116 @@ +package eu.kanade.tachiyomi.extension.en.zeroscans + +import eu.kanade.tachiyomi.source.model.Page +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class NewChaptersResponseDto( + val all: List +) + +@Serializable +data class NewChaptersMangaDto( + val slug: String +) + +@Serializable +data class ZeroScansResponseDto( + val success: Boolean? = null, + val data: T, + val message: String? = null +) + +@Serializable +data class ZeroScansComicsDataDto( + val comics: List, + val genres: List, + val statuses: List, + val rankings: ZeroScansRankingsDto +) + +@Serializable +data class ZeroScansComicDto( + val name: String, + val slug: String, + val id: Int, + val cover: ZeroScansCoverDto, + val summary: String, + val statuses: List, + val genres: List, + @SerialName("chapter_count") val chapterCount: Int, + @SerialName("bookmark_count") val bookmarkCount: Int, + @SerialName("view_count") val viewCount: Int, + val rating: JsonElement +) { + fun getRating(): Float { + return this.rating.toString().toFloatOrNull() ?: 0F + } +} + +@Serializable +data class ZeroScansGenreDto( + val name: String, + val id: Int +) + +@Serializable +data class ZeroScansStatusDto( + val name: String, + val id: Int +) + +@Serializable +data class ZeroScansRankingsDto( + @SerialName("all_time") val allTime: List, + @SerialName("weekly_comics") val weekly: List, + @SerialName("monthly_comics") val monthly: List +) + +@Serializable +data class ZeroScansRankingsEntryDto( + val slug: String +) + +@Serializable +data class ZeroScansCoverDto( + val horizontal: String? = null, + val vertical: String? = null, + val full: String? = null +) { + fun getHighResCover(): String { + return when { + !this.full.isNullOrBlank() -> this.full + !this.horizontal.isNullOrBlank() -> this.horizontal.replace("-horizontal", "-full") + !this.vertical.isNullOrBlank() -> this.vertical.replace("-vertical", "-full") + else -> "" + } + } +} + +@Serializable +data class ZeroScansChaptersResponseDto( + val data: List = emptyList(), + @SerialName("current_page") val currentPage: Int, + @SerialName("last_page") val lastPage: Int +) + +@Serializable +data class ZeroScansChapterDto( + val id: Int, + val name: Int, + val group: String?, + @SerialName("created_at") val createdAt: String +) + +@Serializable +data class ZeroScansPageResponseDto( + val chapter: ZeroScansChapterPagesDto +) + +@Serializable +data class ZeroScansChapterPagesDto( + @SerialName("high_quality") val highQuality: List = emptyList(), + @SerialName("good_quality") val goodQuality: List = emptyList() +) diff --git a/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt new file mode 100644 index 000000000..81e0f32c4 --- /dev/null +++ b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt @@ -0,0 +1,122 @@ +package eu.kanade.tachiyomi.extension.en.zeroscans + +import eu.kanade.tachiyomi.source.model.SManga +import java.util.Calendar +import java.util.Locale + +class ZeroScansHelper { + + // Search Related + fun checkStatusFilter( + filter: ZeroScans.StatusFilter, + comic: ZeroScansComicDto + ): Boolean { + val includedStatusIds = filter.state.filter { it.isIncluded() }.map { it.id } + val excludedStatusIds = filter.state.filter { it.isExcluded() }.map { it.id } + + val comicStatusesId = comic.statuses.map { it.id } + + if (includedStatusIds.isEmpty() && excludedStatusIds.isEmpty()) return true + + return includedStatusIds.any { it in comicStatusesId } && excludedStatusIds.any { it !in comicStatusesId } + } + + fun checkGenreFilter( + filter: ZeroScans.GenreFilter, + comic: ZeroScansComicDto + ): Boolean { + val includedGenreIds = filter.state.filter { it.isIncluded() }.map { it.id } + val excludedGenreIds = filter.state.filter { it.isExcluded() }.map { it.id } + + val comicStatusesId = comic.genres.map { it.id } + + if (includedGenreIds.isEmpty() && excludedGenreIds.isEmpty()) return true + + return includedGenreIds.any { it in comicStatusesId } && excludedGenreIds.any { it !in comicStatusesId } + } + + fun applySortFilter( + type: String, + ascending: Boolean, + comics: List + ): List { + var sortedList = when (type) { + "alphabetic" -> comics.sortedBy { it.name.toLowerCase(Locale.ROOT) } + "rating" -> comics.sortedBy { it.getRating() } + "chapter_count" -> comics.sortedBy { it.chapterCount } + "bookmark_count" -> comics.sortedBy { it.bookmarkCount } + "view_count" -> comics.sortedBy { it.viewCount } + else -> comics + } + + if (!ascending) { + sortedList = sortedList.reversed() + } + + return sortedList + } + + // Chapter Related + fun parseChapterUploadDate(date: String): Long { + val value = date.split(' ')[0].toInt() + + return when (date.split(' ')[1].removeSuffix("s")) { + "sec" -> Calendar.getInstance().apply { + add(Calendar.SECOND, value * -1) + }.timeInMillis + "min" -> Calendar.getInstance().apply { + add(Calendar.MINUTE, value * -1) + }.timeInMillis + "hour" -> Calendar.getInstance().apply { + add(Calendar.HOUR_OF_DAY, value * -1) + }.timeInMillis + "day" -> Calendar.getInstance().apply { + add(Calendar.DATE, value * -1) + }.timeInMillis + "week" -> Calendar.getInstance().apply { + add(Calendar.DATE, value * 7 * -1) + }.timeInMillis + "month" -> Calendar.getInstance().apply { + add(Calendar.MONTH, value * -1) + }.timeInMillis + "year" -> Calendar.getInstance().apply { + add(Calendar.YEAR, value * -1) + }.timeInMillis + else -> { + return 0 + } + } + } + + // Manga Related + fun zsComicEntryToSManga(comic: ZeroScansComicDto): SManga { + var comicDescription = comic.summary + if (comic.statuses.any { it.id == 4 }) { + comicDescription = "The series has been dropped.\n\n$comicDescription" + } + return SManga.create().apply { + title = comic.name + url = "/comics/${comic.slug}?id=${comic.id}" + thumbnail_url = comic.cover.getHighResCover() + description = comicDescription + genre = comic.genres.joinToString { it.name } + status = comic.getTachiyomiStatus() + initialized = true + } + } + + private fun ZeroScansComicDto.getTachiyomiStatus(): Int { + // 1 = New & 4 = Dropped + val compatibleStatus = statuses.filterNot { it.id in listOf(1, 4) } + + // TODO Apply 6 to ON_HIATUS after ext-lib 1.3 merge + compatibleStatus.firstOrNull { it.id in listOf(5, 6) } + ?.also { return SManga.ONGOING } + + compatibleStatus.firstOrNull { it.id == 3 } + ?.also { return SManga.COMPLETED } + + // Nothing Matched + return SManga.UNKNOWN + } +}