NEW - TaddyINK extension (#18699)

* sss feed parsing complete

* removed old references

* Updates based on comments

* remove unneeded image

* using SwitchPreferenceCompat

* misc changes

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyUtils.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyUtils.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInkUrlActivity.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInkUrlActivity.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyInk.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* changes as per review

* Update src/all/taddyink/build.gradle

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyUtils.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* Update src/all/taddyink/src/eu/kanade/tachiyomi/extension/all/taddyink/TaddyUtils.kt

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>

* small fix

---------

Co-authored-by: Daniel Mathews <dmathewwws@Daniels-Air.pnwlumber.com>
Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>
This commit is contained in:
Daniel Mathews 2023-10-27 07:16:52 -07:00 committed by GitHub
parent c6f5a54c96
commit 308d945eb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 413 additions and 0 deletions

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.taddyink.TaddyInkUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="taddy.org"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Taddy INK (Webtoons)'
pkgNameSuffix = 'all.taddyink'
extClass = '.TaddyInkFactory'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -0,0 +1,188 @@
package eu.kanade.tachiyomi.extension.all.taddyink
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
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.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
open class TaddyInk(
override val lang: String,
private val taddyLang: String,
) : ConfigurableSource, HttpSource() {
final override val baseUrl = "https://taddy.org"
override val name = "Taddy INK (Webtoons)"
override val supportsLatest = false
override val client: OkHttpClient by lazy {
network.cloudflareClient.newBuilder()
.rateLimit(4)
.build()
}
private val json: Json by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = TITLE_PREF_KEY
title = TITLE_PREF
summaryOn = "Full Title"
summaryOff = "Short Title"
setDefaultValue(true)
}.also(screen::addPreference)
}
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used!")
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used!")
override fun popularMangaRequest(page: Int): Request {
val url = "$baseUrl/feeds/directory/list".toHttpUrl().newBuilder()
.addQueryParameter("lang", taddyLang)
.addQueryParameter("taddyType", "comicseries")
.addQueryParameter("ua", "tc")
.addQueryParameter("page", page.toString())
.addQueryParameter("limit", POPULAR_MANGA_LIMIT.toString())
return GET(url.build(), headers)
}
override fun popularMangaParse(response: Response) = parseManga(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val shouldFilterByGenre = filterList.findInstance<GenreFilter>()?.state != 0
val shouldFilterByCreator = filterList.findInstance<CreatorFilter>()?.state?.isNotBlank() ?: false
val shouldFilterForTags = filterList.findInstance<TagFilter>()?.state?.isNotBlank() ?: false
val url = "$baseUrl/feeds/directory/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("lang", taddyLang)
.addQueryParameter("taddyType", "comicseries")
.addQueryParameter("ua", "tc")
.addQueryParameter("page", page.toString())
.addQueryParameter("limit", SEARCH_MANGA_LIMIT.toString())
if (shouldFilterByGenre) {
filterList.findInstance<GenreFilter>()?.let { f ->
url.addQueryParameter("genre", f.toUriPart())
}
}
if (shouldFilterByCreator) {
filterList.findInstance<CreatorFilter>()?.let { name ->
url.addQueryParameter("creator", name.state)
}
}
if (shouldFilterForTags) {
filterList.findInstance<TagFilter>()?.let { tags ->
url.addQueryParameter("tags", tags.state)
}
}
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response) = parseManga(response)
private fun parseManga(response: Response): MangasPage {
val comicSeries = json.decodeFromString<ComicResults>(response.body.string())
val mangas = comicSeries.comicseries.map { TaddyUtils.getManga(it) }
val hasNextPage = comicSeries.comicseries.size == POPULAR_MANGA_LIMIT
return MangasPage(mangas, hasNextPage)
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(manga.url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val comicObj = json.decodeFromString<Comic>(response.body.string())
return TaddyUtils.getManga(comicObj)
}
override fun chapterListRequest(manga: SManga): Request {
return GET(manga.url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val comic = json.decodeFromString<Comic>(response.body.string())
val sssUrl = comic.url
val chapters = comic.issues.orEmpty().mapIndexed { i, chapter ->
SChapter.create().apply {
url = "$sssUrl#${chapter.identifier}"
name = chapter.name
date_upload = TaddyUtils.getTime(chapter.datePublished)
chapter_number = (comic.issues.orEmpty().size - i).toFloat()
}
}
return chapters.reversed()
}
override fun pageListRequest(chapter: SChapter): Request {
return GET(chapter.url, headers)
}
override fun pageListParse(response: Response): List<Page> {
val requestUrl = response.request.url.toString()
val issueUuid = requestUrl.substringAfterLast("#")
val comic = json.decodeFromString<Comic>(response.body.string())
val issue = comic.issues.orEmpty().firstOrNull { it.identifier == issueUuid }
return issue?.stories.orEmpty().mapIndexed { index, storyObj ->
Page(index, "", "${storyObj.storyImage?.base_url}${storyObj.storyImage?.story}")
}
}
override fun imageUrlParse(response: Response) = ""
override fun getFilterList(): FilterList = FilterList(
GenreFilter(),
Filter.Separator(),
Filter.Header("Filter by the creator or tags:"),
CreatorFilter(),
TagFilter(),
)
class CreatorFilter : AdvSearchEntryFilter("Creator")
class TagFilter : AdvSearchEntryFilter("Tags")
open class AdvSearchEntryFilter(name: String) : Filter.Text(name)
private class GenreFilter : UriPartFilter(
"Filter By Genre",
TaddyUtils.genrePairs,
)
private open class UriPartFilter(displayName: String, val vals: List<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
companion object {
private const val TITLE_PREF_KEY = "display_full_title"
private const val TITLE_PREF = "Display manga title as"
private const val POPULAR_MANGA_LIMIT = 25
private const val SEARCH_MANGA_LIMIT = 25
}
}

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.extension.all.taddyink
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class TaddyInkFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
TaddyInk("all", ""),
)
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.extension.all.taddyink
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://nhentai.net/g/xxxxxx intents and redirects them to
* the main Tachiyomi process.
*/
class TaddyInkUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("TaddyInkUrlActivity", e.toString())
}
} else {
Log.e("TaddyInkUrlActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -0,0 +1,144 @@
package eu.kanade.tachiyomi.extension.all.taddyink
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
object TaddyUtils {
private val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
fun getManga(comicObj: Comic): SManga {
val name = comicObj.name
val sssUrl = comicObj.url
val sssDescription = comicObj.description
val genres = comicObj.genres.orEmpty()
.mapNotNull { genreMap[it] }
.joinToString()
val creators = comicObj.creators
?.mapNotNull { it.name }
?.joinToString()
val thumbnailBaseUrl = comicObj.coverImage?.base_url ?: ""
val thumbnail = comicObj.coverImage?.cover_sm ?: ""
val thumbnailUrl = if (thumbnailBaseUrl.isNotEmpty() && thumbnail.isNotEmpty()) "$thumbnailBaseUrl$thumbnail" else ""
return SManga.create().apply {
url = sssUrl
title = name
creators?.takeIf { it.isNotBlank() }?.let { author = it }
description = sssDescription
thumbnail_url = thumbnailUrl
status = SManga.ONGOING
genre = genres
initialized = true
}
}
fun getTime(timeString: String): Long {
return runCatching { formatter.parse(timeString)?.time }
.getOrNull() ?: 0L
}
val genrePairs: List<Pair<String, String>> = listOf(
Pair("", ""),
Pair("Action", "COMICSERIES_ACTION"),
Pair("Comedy", "COMICSERIES_COMEDY"),
Pair("Drama", "COMICSERIES_DRAMA"),
Pair("Educational", "COMICSERIES_EDUCATIONAL"),
Pair("Fantasy", "COMICSERIES_FANTASY"),
Pair("Historical", "COMICSERIES_HISTORICAL"),
Pair("Horror", "COMICSERIES_HORROR"),
Pair("Inspirational", "COMICSERIES_INSPIRATIONAL"),
Pair("Mystery", "COMICSERIES_MYSTERY"),
Pair("Romance", "COMICSERIES_ROMANCE"),
Pair("Sci-Fi", "COMICSERIES_SCI_FI"),
Pair("Slice Of Life", "COMICSERIES_SLICE_OF_LIFE"),
Pair("Superhero", "COMICSERIES_SUPERHERO"),
Pair("Supernatural", "COMICSERIES_SUPERNATURAL"),
Pair("Wholesome", "COMICSERIES_WHOLESOME"),
Pair("BL (Boy Love)", "COMICSERIES_BL"),
Pair("GL (Girl Love)", "COMICSERIES_GL"),
Pair("LGBTQ+", "COMICSERIES_LGBTQ"),
Pair("Thriller", "COMICSERIES_THRILLER"),
Pair("Zombies", "COMICSERIES_ZOMBIES"),
Pair("Post Apocalyptic", "COMICSERIES_POST_APOCALYPTIC"),
Pair("School", "COMICSERIES_SCHOOL"),
Pair("Sports", "COMICSERIES_SPORTS"),
Pair("Animals", "COMICSERIES_ANIMALS"),
Pair("Gaming", "COMICSERIES_GAMING"),
)
val genreMap: Map<String, String> = genrePairs.associateBy({ it.second }, { it.first })
}
@Serializable
data class ComicResults(
val status: String,
val comicseries: List<Comic> = emptyList(),
)
@Serializable
data class Comic(
val identifier: String? = null,
val name: String = "Unknown",
val url: String,
val description: String? = null,
val genres: List<String>? = emptyList(),
val creators: List<Creator>? = emptyList(),
val coverImage: CoverImage? = null,
val bannerImage: BannerImage? = null,
val thumbnailImage: ThumbnailImage? = null,
val contentRating: String? = null,
val inLanguage: String? = null,
val seriesType: String? = null,
val issues: List<Chapter>? = emptyList(),
)
@Serializable
data class CoverImage(
val base_url: String?,
val cover_sm: String?,
val cover_md: String?,
val cover_lg: String?,
)
@Serializable
data class BannerImage(
val base_url: String?,
val banner_sm: String?,
val banner_md: String?,
val banner_lg: String?,
)
@Serializable
data class ThumbnailImage(
val base_url: String?,
val thumbnail: String?,
)
@Serializable
data class Creator(
val identifier: String? = null,
val name: String? = null,
)
@Serializable
data class Chapter(
val identifier: String,
val name: String,
val datePublished: String,
val stories: List<Story>? = emptyList(),
)
@Serializable
data class Story(
val storyImage: StoryImage?,
)
@Serializable
data class StoryImage(
val base_url: String?,
val story: String?,
)