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