mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 16:18:55 +01:00 
			
		
		
		
	See CHANGELOG.md for this commit
This commit is contained in:
		@@ -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
 | 
			
		||||
- Add HentaiCafe source
 | 
			
		||||
- Add Tsumino source
 | 
			
		||||
@@ -144,9 +144,23 @@
 | 
			
		||||
                    android:pathPrefix="/g/"
 | 
			
		||||
                    android:scheme="https"/>
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="nhentai.net"
 | 
			
		||||
                    android:pathPrefix="/g/"
 | 
			
		||||
                    android:host="www.perveden.com"
 | 
			
		||||
                    android:scheme="http"/>
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="www.perveden.com"
 | 
			
		||||
                    android:scheme="https"/>
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="hentai.cafe"
 | 
			
		||||
                    android:scheme="http"/>
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="hentai.cafe"
 | 
			
		||||
                    android:scheme="https"/>
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="www.tsumino.com"
 | 
			
		||||
                    android:scheme="http"/>
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="www.tsumino.com"
 | 
			
		||||
                    android:scheme="https"/>
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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<HentaiCafeMetadata, Document>
 | 
			
		||||
    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<HentaiCafeMetadata, Document>
 | 
			
		||||
 | 
			
		||||
        url = Uri.decode(it.location())
 | 
			
		||||
        title = eTitle.text()
 | 
			
		||||
        
 | 
			
		||||
        thumbnailUrl = content.select("img").attr("src")
 | 
			
		||||
 | 
			
		||||
        tags.clear()
 | 
			
		||||
        val eDetails = content.select("p > a[rel=tag]")
 | 
			
		||||
 
 | 
			
		||||
@@ -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<TsuminoMetadata, Document> {
 | 
			
		||||
    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<AdvSearchEntry> = 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<SortFilter>().first().state],
 | 
			
		||||
                LengthType.values()[f.filterIsInstance<LengthFilter>().first().state],
 | 
			
		||||
                f.filterIsInstance<MinimumRatingFilter>().first().state,
 | 
			
		||||
                f.filterIsInstance<ExcludeParodiesFilter>().first().state,
 | 
			
		||||
                f.filterIsInstance<AdvSearchEntryFilter>().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<List<Page>> {
 | 
			
		||||
        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<SortType>("Sort by", SortType.values())
 | 
			
		||||
    class LengthFilter : Filter.Select<LengthType>("Length", LengthType.values())
 | 
			
		||||
    class MinimumRatingFilter : Filter.Select<String>("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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Tag> = 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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										130
									
								
								app/src/main/java/exh/metadata/models/TsuminoMetadata.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								app/src/main/java/exh/metadata/models/TsuminoMetadata.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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<String> = RealmList()
 | 
			
		||||
    
 | 
			
		||||
    var character: RealmList<String> = RealmList()
 | 
			
		||||
    
 | 
			
		||||
    override var tags: RealmList<Tag> = RealmList()
 | 
			
		||||
    
 | 
			
		||||
    override fun getTitles() = listOfNotNull(title)
 | 
			
		||||
    
 | 
			
		||||
    @Ignore
 | 
			
		||||
    override val titleFields = listOf(
 | 
			
		||||
            TsuminoMetadata::title.name
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    @Index
 | 
			
		||||
    override var mangaId: Long? = null
 | 
			
		||||
    
 | 
			
		||||
    class EmptyQuery : GalleryQuery<TsuminoMetadata>(TsuminoMetadata::class)
 | 
			
		||||
    
 | 
			
		||||
    class UrlQuery(
 | 
			
		||||
            val url: String
 | 
			
		||||
    ) : GalleryQuery<TsuminoMetadata>(TsuminoMetadata::class) {
 | 
			
		||||
        override fun transform() = Query(
 | 
			
		||||
                tmIdFromUrl(url)
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    class Query(
 | 
			
		||||
            val tmId: String
 | 
			
		||||
    ) : GalleryQuery<TsuminoMetadata>(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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user