From 263cc1d97c787b9ee522abc8309672508cf3c219 Mon Sep 17 00:00:00 2001 From: NerdNumber9 Date: Thu, 7 Dec 2017 22:25:27 -0500 Subject: [PATCH] See CHANGELOG.md for this commit --- CHANGELOG.md | 5 +- app/src/main/AndroidManifest.xml | 18 +- .../kanade/tachiyomi/source/SourceManager.kt | 8 +- .../source/online/english/HentaiCafe.kt | 12 +- .../source/online/english/Tsumino.kt | 304 ++++++++++++++++++ app/src/main/java/exh/EHSourceHelpers.kt | 2 + app/src/main/java/exh/GalleryAdder.kt | 48 +-- .../exh/metadata/models/HentaiCafeMetadata.kt | 8 +- .../exh/metadata/models/TsuminoMetadata.kt | 130 ++++++++ 9 files changed, 503 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt create mode 100644 app/src/main/java/exh/metadata/models/TsuminoMetadata.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index f338811e8..e2eaedb2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Upstream merge - Fix PervEden search - Add ability to use high-quality thumbnails on nhentai -- Enable PervEden link importing +- Enable link importing for all NSFW sources - Fix back button in library search -- Add HentaiCafe source \ No newline at end of file +- Add HentaiCafe source +- Add Tsumino source \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b4a662fa2..f585dcc84 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -144,9 +144,23 @@ android:pathPrefix="/g/" android:scheme="https"/> + + + + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index de5a69fbe..9a29b560d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -9,9 +9,9 @@ import dalvik.system.PathClassLoader import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.YamlHttpSource +import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.PervEden import eu.kanade.tachiyomi.source.online.english.* @@ -20,7 +20,10 @@ import eu.kanade.tachiyomi.source.online.russian.Mangachan import eu.kanade.tachiyomi.source.online.russian.Mintmanga import eu.kanade.tachiyomi.source.online.russian.Readmanga import eu.kanade.tachiyomi.util.hasPermission -import exh.* +import exh.EH_SOURCE_ID +import exh.EXH_SOURCE_ID +import exh.PERV_EDEN_EN_SOURCE_ID +import exh.PERV_EDEN_IT_SOURCE_ID import exh.metadata.models.PervEdenLang import org.yaml.snakeyaml.Yaml import rx.Observable @@ -98,6 +101,7 @@ open class SourceManager(private val context: Context) { exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it) exSrcs += NHentai(context) exSrcs += HentaiCafe() + exSrcs += Tsumino() return exSrcs } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HentaiCafe.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HentaiCafe.kt index e8f998d94..d562053fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HentaiCafe.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HentaiCafe.kt @@ -11,6 +11,7 @@ import exh.HENTAI_CAFE_SOURCE_ID import exh.metadata.models.HentaiCafeMetadata import exh.metadata.models.HentaiCafeMetadata.Companion.BASE_URL import exh.metadata.models.Tag +import exh.util.urlImportFetchSearchManga import okhttp3.Request import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -34,8 +35,13 @@ class HentaiCafe : ParsedHttpSource(), LewdSource override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!") override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException("Unused method called!") override fun fetchPopularManga(page: Int) = fetchLatestUpdates(page) - - override fun searchMangaSelector() = "article.post" + + //Support direct URL importing + override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = + urlImportFetchSearchManga(query, { + super.fetchSearchManga(page, query, filters) + }) + override fun searchMangaSelector() = "article.post:not(#post-0)" override fun searchMangaFromElement(element: Element): SManga { val thumb = element.select(".entry-thumb > img") val title = element.select(".entry-title > a") @@ -111,6 +117,8 @@ class HentaiCafe : ParsedHttpSource(), LewdSource url = Uri.decode(it.location()) title = eTitle.text() + + thumbnailUrl = content.select("img").attr("src") tags.clear() val eDetails = content.select("p > a[rel=tag]") diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt new file mode 100644 index 000000000..897871aea --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt @@ -0,0 +1,304 @@ +package eu.kanade.tachiyomi.source.online.english + +import android.net.Uri +import com.github.salomonbrys.kotson.* +import com.google.gson.JsonParser +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.LewdSource +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import exh.TSUMINO_SOURCE_ID +import exh.metadata.models.Tag +import exh.metadata.models.TsuminoMetadata +import exh.metadata.models.TsuminoMetadata.Companion.BASE_URL +import exh.util.urlImportFetchSearchManga +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import java.text.SimpleDateFormat +import java.util.* + +class Tsumino: ParsedHttpSource(), LewdSource { + override val id = TSUMINO_SOURCE_ID + + override val lang = "en" + override val supportsLatest = true + override val name = "Tsumino" + + override fun queryAll() = TsuminoMetadata.EmptyQuery() + + override fun queryFromUrl(url: String) = TsuminoMetadata.UrlQuery(url) + + override val baseUrl = BASE_URL + + override val metaParser: TsuminoMetadata.(Document) -> Unit = { + url = it.location() + tags.clear() + + it.getElementById("Title")?.text()?.let { + title = it.trim() + } + + it.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let { + tags.add(Tag("artist", it, false)) + artist = it + } + + it.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let { + tags.add(Tag("uploader", it, false)) + uploader = it + } + + it.getElementById("Uploaded")?.text()?.let { + uploadDate = TM_DATE_FORMAT.parse(it.trim()).time + } + + it.getElementById("Pages")?.text()?.let { + length = it.trim().toIntOrNull() + } + + it.getElementById("Rating")?.text()?.let { + ratingString = it.trim() + } + + it.getElementById("Category")?.children()?.first()?.text()?.let { + category = it.trim() + tags.add(Tag("genre", it, false)) + } + + it.getElementById("Collection")?.children()?.first()?.text()?.let { + collection = it.trim() + } + + it.getElementById("Group")?.children()?.first()?.text()?.let { + group = it.trim() + tags.add(Tag("group", it, false)) + } + + parody.clear() + it.getElementById("Parody")?.children()?.forEach { + val entry = it.text().trim() + parody.add(entry) + tags.add(Tag("parody", entry, false)) + } + + character.clear() + it.getElementById("Character")?.children()?.forEach { + val entry = it.text().trim() + character.add(entry) + tags.add(Tag("character", entry, false)) + } + + it.getElementById("Tag")?.children()?.let { + tags.addAll(it.map { + Tag("tag", it.text().trim(), false) + }) + } + } + + fun genericMangaParse(response: Response): MangasPage { + val json = jsonParser.parse(response.body()!!.string()!!).asJsonObject + val hasNextPage = json["PageNumber"].int < json["PageCount"].int + + val manga = json["Data"].array.map { + val obj = it.obj["Entry"].obj + + SManga.create().apply { + val id = obj["Id"].long + setUrlWithoutDomain(TsuminoMetadata.mangaUrlFromId(id.toString())) + thumbnail_url = TsuminoMetadata.thumbUrlFromId(id.toString()) + + title = obj["Title"].string + } + } + + return MangasPage(manga, hasNextPage) + } + + fun genericMangaRequest(page: Int, + query: String, + sort: SortType, + length: LengthType, + minRating: Int, + excludeParodies: Boolean = false, + advSearch: List = emptyList()) + = POST("$BASE_URL/Books/Operate", body = FormBody.Builder() + .add("PageNumber", (page + 1).toString()) + .add("Text", query) + .add("Sort", sort.name) + .add("List", "0") + .add("Length", length.id.toString()) + .add("MinimumRating", minRating.toString()) + .apply { + advSearch.forEachIndexed { index, entry -> + add("Tags[$index][Type]", entry.type.toString()) + add("Tags[$index][Text]", entry.text) + add("Tags[$index][Exclude]", entry.exclude.toString()) + } + + if(excludeParodies) + add("Exclude[]", "6") + } + .build()) + + enum class SortType { + Newest, + Oldest, + Alphabetical, + Rating, + Pages, + Views, + Random, + Comments, + Popularity + } + + enum class LengthType(val id: Int) { + Any(0), + Short(1), + Medium(2), + Long(3) + } + + override fun popularMangaSelector() = throw UnsupportedOperationException("Unused method called!") + override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!") + override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!") + override fun popularMangaRequest(page: Int) = genericMangaRequest(page, + "", + SortType.Random, + LengthType.Any, + 0) + + override fun popularMangaParse(response: Response) = genericMangaParse(response) + + override fun latestUpdatesSelector() = throw UnsupportedOperationException("Unused method called!") + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!") + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Unused method called!") + override fun latestUpdatesRequest(page: Int) = genericMangaRequest(page, + "", + SortType.Newest, + LengthType.Any, + 0) + override fun latestUpdatesParse(response: Response) = genericMangaParse(response) + + //Support direct URL importing + override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = + urlImportFetchSearchManga(query, { + super.fetchSearchManga(page, query, filters) + }) + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + // Append filters again, to provide fallback in case a filter is not provided + // Since we only work with the first filter when building the result, if the filter is provided, + // the original filter is ignored + val f = filters + getFilterList() + + return genericMangaRequest( + page, + query, + SortType.values()[f.filterIsInstance().first().state], + LengthType.values()[f.filterIsInstance().first().state], + f.filterIsInstance().first().state, + f.filterIsInstance().first().state, + f.filterIsInstance().flatMap { filter -> + val splitState = filter.state.split(",").map(String::trim).filterNot(String::isBlank) + + splitState.map { + AdvSearchEntry(filter.type, it.removePrefix("-"), it.startsWith("-")) + } + } + ) + } + + override fun searchMangaSelector() = throw UnsupportedOperationException("Unused method called!") + override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!") + override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!") + override fun searchMangaParse(response: Response) = genericMangaParse(response) + + override fun mangaDetailsParse(document: Document) + = parseToManga(queryFromUrl(document.location()), document) + + override fun chapterListSelector() = throw UnsupportedOperationException("Unused method called!") + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!") + override fun fetchChapterList(manga: SManga) = lazyLoadMeta(queryFromUrl(manga.url), + client.newCall(mangaDetailsRequest(manga)).asObservableSuccess().map { it.asJsoup() } + ).map { + listOf( + SChapter.create().apply { + url = "/Read/View/${it.tmId}" + name = "Chapter" + + it.uploadDate?.let { date_upload = it } + + chapter_number = 1f + } + ) + } + + override fun fetchPageList(chapter: SChapter): Observable> { + val id = chapter.url.substringAfterLast('/') + val call = POST("$BASE_URL/Read/Load", body = FormBody.Builder().add("q", id).build()) + return client.newCall(call).asObservableSuccess().map { + val parsed = jsonParser.parse(it.body()!!.string()).obj + val pageUrls = parsed["reader_page_urls"].array + + val imageUrl = Uri.parse("$BASE_URL/Image/Object") + pageUrls.mapIndexed { index, obj -> + val newImageUrl = imageUrl.buildUpon().appendQueryParameter("name", obj.string) + Page(index, chapter.url + "#${index + 1}", newImageUrl.toString()) + } + } + } + + override fun pageListParse(document: Document) = throw UnsupportedOperationException("Unused method called!") + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Unused method called!") + + data class AdvSearchEntry(val type: Int, val text: String, val exclude: Boolean) + + override fun getFilterList() = FilterList( + Filter.Header("Separate tags with commas"), + Filter.Header("Prepend with dash to exclude"), + TagFilter(), + CategoryFilter(), + CollectionFilter(), + GroupFilter(), + ArtistFilter(), + ParodyFilter(), + CharactersFilter(), + UploaderFilter(), + + Filter.Separator(), + + SortFilter(), + LengthFilter(), + MinimumRatingFilter(), + ExcludeParodiesFilter() + ) + + class TagFilter : AdvSearchEntryFilter("Tags", 1) + class CategoryFilter : AdvSearchEntryFilter("Categories", 2) + class CollectionFilter : AdvSearchEntryFilter("Collections", 3) + class GroupFilter : AdvSearchEntryFilter("Groups", 4) + class ArtistFilter : AdvSearchEntryFilter("Artists", 5) + class ParodyFilter : AdvSearchEntryFilter("Parodies", 6) + class CharactersFilter : AdvSearchEntryFilter("Characters", 7) + class UploaderFilter : AdvSearchEntryFilter("Uploaders", 8) + open class AdvSearchEntryFilter(name: String, val type: Int) : Filter.Text(name) + + class SortFilter : Filter.Select("Sort by", SortType.values()) + class LengthFilter : Filter.Select("Length", LengthType.values()) + class MinimumRatingFilter : Filter.Select("Minimum rating", (0 .. 5).map { "$it stars" }.toTypedArray()) + class ExcludeParodiesFilter : Filter.CheckBox("Exclude parodies") + + companion object { + val jsonParser by lazy { + JsonParser() + } + + val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US) + } +} diff --git a/app/src/main/java/exh/EHSourceHelpers.kt b/app/src/main/java/exh/EHSourceHelpers.kt index 14b62a601..28f752f81 100755 --- a/app/src/main/java/exh/EHSourceHelpers.kt +++ b/app/src/main/java/exh/EHSourceHelpers.kt @@ -17,6 +17,8 @@ val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7 val HENTAI_CAFE_SOURCE_ID = LEWD_SOURCE_SERIES + 8 +val TSUMINO_SOURCE_ID = LEWD_SOURCE_SERIES + 9 + fun isLewdSource(source: Long) = source in 6900..6999 fun isEhSource(source: Long) = source == EH_SOURCE_ID diff --git a/app/src/main/java/exh/GalleryAdder.kt b/app/src/main/java/exh/GalleryAdder.kt index ab44c8ee7..bf0bbaecb 100755 --- a/app/src/main/java/exh/GalleryAdder.kt +++ b/app/src/main/java/exh/GalleryAdder.kt @@ -10,10 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.util.syncChaptersWithSource -import exh.metadata.models.ExGalleryMetadata -import exh.metadata.models.NHentaiMetadata -import exh.metadata.models.PervEdenGalleryMetadata -import exh.metadata.models.PervEdenLang +import exh.metadata.models.* import exh.util.defRealm import okhttp3.MediaType import okhttp3.Request @@ -68,7 +65,7 @@ class GalleryAdder { try { val urlObj = Uri.parse(url) val lowercasePs = urlObj.pathSegments.map(String::toLowerCase) - val firstPathSegment = lowercasePs[0] + val lcFirstPathSegment = lowercasePs[0] val source = when (urlObj.host.toLowerCase()) { "g.e-hentai.org", "e-hentai.org" -> EH_SOURCE_ID "exhentai.org" -> EXH_SOURCE_ID @@ -80,6 +77,8 @@ class GalleryAdder { else -> return GalleryAddEvent.Fail.UnknownType(url) } } + "hentai.cafe" -> HENTAI_CAFE_SOURCE_ID + "www.tsumino.com" -> TSUMINO_SOURCE_ID else -> return GalleryAddEvent.Fail.UnknownType(url) } @@ -88,7 +87,7 @@ class GalleryAdder { } val realUrl = when(source) { - EH_SOURCE_ID, EXH_SOURCE_ID -> when (firstPathSegment) { + EH_SOURCE_ID, EXH_SOURCE_ID -> when (lcFirstPathSegment) { "g" -> { //Is already gallery page, do nothing url @@ -100,7 +99,7 @@ class GalleryAdder { else -> return GalleryAddEvent.Fail.UnknownType(url) } NHENTAI_SOURCE_ID -> { - if(firstPathSegment != "g") + if(lcFirstPathSegment != "g") return GalleryAddEvent.Fail.UnknownType(url) "https://nhentai.net/g/${urlObj.pathSegments[1]}/" @@ -113,6 +112,18 @@ class GalleryAdder { } uri.toString() } + HENTAI_CAFE_SOURCE_ID -> { + if(lcFirstPathSegment == "manga") + "https://hentai.cafe/${urlObj.pathSegments[2]}" + + "https://hentai.cafe/$lcFirstPathSegment" + } + TSUMINO_SOURCE_ID -> { + if(lcFirstPathSegment != "read" && lcFirstPathSegment != "book") + return GalleryAddEvent.Fail.UnknownType(url) + + "https://tsumino.com/Book/Info/${urlObj.pathSegments[2]}" + } else -> return GalleryAddEvent.Fail.UnknownType(url) } @@ -124,6 +135,8 @@ class GalleryAdder { NHENTAI_SOURCE_ID -> realUrl //nhentai uses URLs directly (oops, my bad when implementing this source) PERV_EDEN_EN_SOURCE_ID, PERV_EDEN_IT_SOURCE_ID -> getUrlWithoutDomain(realUrl) + HENTAI_CAFE_SOURCE_ID -> getUrlWithoutDomain(realUrl) + TSUMINO_SOURCE_ID -> getUrlWithoutDomain(realUrl) else -> return GalleryAddEvent.Fail.UnknownType(url) } @@ -142,23 +155,14 @@ class GalleryAdder { //Apply metadata defRealm { realm -> when (source) { - EH_SOURCE_ID, EXH_SOURCE_ID -> - ExGalleryMetadata.UrlQuery(realUrl, isExSource(source)) - .query(realm) - .findFirst()?.copyTo(manga) - NHENTAI_SOURCE_ID -> - NHentaiMetadata.UrlQuery(realUrl) - .query(realm) - .findFirst() - ?.copyTo(manga) + EH_SOURCE_ID, EXH_SOURCE_ID -> ExGalleryMetadata.UrlQuery(realUrl, isExSource(source)) + NHENTAI_SOURCE_ID -> NHentaiMetadata.UrlQuery(realUrl) PERV_EDEN_EN_SOURCE_ID, - PERV_EDEN_IT_SOURCE_ID -> - PervEdenGalleryMetadata.UrlQuery(realUrl, PervEdenLang.source(source)) - .query(realm) - .findFirst() - ?.copyTo(manga) + PERV_EDEN_IT_SOURCE_ID -> PervEdenGalleryMetadata.UrlQuery(realUrl, PervEdenLang.source(source)) + HENTAI_CAFE_SOURCE_ID -> HentaiCafeMetadata.UrlQuery(realUrl) + TSUMINO_SOURCE_ID -> TsuminoMetadata.UrlQuery(realUrl) else -> return GalleryAddEvent.Fail.UnknownType(url) - } + }.query(realm).findFirst() } if (fav) manga.favorite = true diff --git a/app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt b/app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt index e7ec88130..ecb634e00 100644 --- a/app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt +++ b/app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt @@ -25,16 +25,18 @@ open class HentaiCafeMetadata : RealmObject(), SearchableGalleryMetadata { hcId = hcIdFromUrl(a) } } + + var thumbnailUrl: String? = null var title: String? = null var artist: String? = null - override var uploader: String? = null + override var uploader: String? = null //Always will be null as this is unknown override var tags: RealmList = RealmList() - override fun getTitles() = listOf(title).filterNotNull() + override fun getTitles() = listOfNotNull(title) @Ignore override val titleFields = listOf( @@ -45,6 +47,8 @@ open class HentaiCafeMetadata : RealmObject(), SearchableGalleryMetadata { override var mangaId: Long? = null override fun copyTo(manga: SManga) { + thumbnailUrl?.let { manga.thumbnail_url = it } + manga.title = title!! manga.artist = artist manga.author = artist diff --git a/app/src/main/java/exh/metadata/models/TsuminoMetadata.kt b/app/src/main/java/exh/metadata/models/TsuminoMetadata.kt new file mode 100644 index 000000000..172ce0af4 --- /dev/null +++ b/app/src/main/java/exh/metadata/models/TsuminoMetadata.kt @@ -0,0 +1,130 @@ +package exh.metadata.models + +import android.net.Uri +import eu.kanade.tachiyomi.source.model.SManga +import exh.metadata.EX_DATE_FORMAT +import exh.metadata.buildTagsDescription +import exh.plusAssign +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.Ignore +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey +import io.realm.annotations.RealmClass +import java.util.* + +@RealmClass +open class TsuminoMetadata : RealmObject(), SearchableGalleryMetadata { + @PrimaryKey + override var uuid: String = UUID.randomUUID().toString() + + @Index + var tmId: String? = null + + var url get() = tmId?.let { mangaUrlFromId(it) } + set(a) { + a?.let { + tmId = tmIdFromUrl(a) + } + } + + var title: String? = null + + var artist: String? = null + + override var uploader: String? = null + + var uploadDate: Long? = null + + var length: Int? = null + + var ratingString: String? = null + + var category: String? = null + + var collection: String? = null + + var group: String? = null + + var parody: RealmList = RealmList() + + var character: RealmList = RealmList() + + override var tags: RealmList = RealmList() + + override fun getTitles() = listOfNotNull(title) + + @Ignore + override val titleFields = listOf( + TsuminoMetadata::title.name + ) + + @Index + override var mangaId: Long? = null + + class EmptyQuery : GalleryQuery(TsuminoMetadata::class) + + class UrlQuery( + val url: String + ) : GalleryQuery(TsuminoMetadata::class) { + override fun transform() = Query( + tmIdFromUrl(url) + ) + } + + class Query( + val tmId: String + ) : GalleryQuery(TsuminoMetadata::class) { + override fun map() = mapOf( + TsuminoMetadata::tmId to Query::tmId + ) + } + + override fun copyTo(manga: SManga) { + title?.let { manga.title = it } + manga.thumbnail_url = thumbUrlFromId(tmId.toString()) + + artist?.let { manga.artist = it } + + manga.status = SManga.UNKNOWN + + val titleDesc = "Title: $title\n" + + val detailsDesc = StringBuilder() + uploader?.let { detailsDesc += "Uploader: $it\n" } + uploadDate?.let { detailsDesc += "Uploaded: ${EX_DATE_FORMAT.format(Date(it))}\n" } + length?.let { detailsDesc += "Length: $it pages\n" } + ratingString?.let { detailsDesc += "Rating: $it\n" } + category?.let { + manga.genre = it + detailsDesc += "Category: $it\n" + } + collection?.let { detailsDesc += "Collection: $it\n" } + group?.let { detailsDesc += "Group: $it\n" } + val parodiesString = parody.joinToString() + if(parodiesString.isNotEmpty()) { + detailsDesc += "Parody: $parodiesString\n" + } + val charactersString = character.joinToString() + if(charactersString.isNotEmpty()) { + detailsDesc += "Character: $charactersString\n" + } + + val tagsDesc = buildTagsDescription(this) + + manga.description = listOf(titleDesc, detailsDesc.toString(), tagsDesc.toString()) + .filter(String::isNotBlank) + .joinToString(separator = "\n") + } + + companion object { + val BASE_URL = "https://www.tsumino.com" + + fun tmIdFromUrl(url: String) + = Uri.parse(url).pathSegments[2] + + fun mangaUrlFromId(id: String) = "$BASE_URL/Book/Info/$id" + + fun thumbUrlFromId(id: String) = "$BASE_URL/Image/Thumb/$id" + } +} \ No newline at end of file