-
Notifications
You must be signed in to change notification settings - Fork 259
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(src/all): New source: Sudatchi (#3153)
- Loading branch information
1 parent
095f005
commit 0c552ed
Showing
11 changed files
with
549 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
<application> | ||
<activity | ||
android:name=".all.sudatchi.SudatchiUrlActivity" | ||
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="sudatchi.com" | ||
android:pathPattern="/anime/..*" | ||
android:scheme="https" /> | ||
</intent-filter> | ||
</activity> | ||
</application> | ||
</manifest> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
ext { | ||
extName = 'Sudatchi' | ||
extClass = '.Sudatchi' | ||
extVersionCode = 1 | ||
} | ||
|
||
apply from: "$rootDir/common.gradle" | ||
|
||
dependencies { | ||
implementation(project(":lib:playlist-utils")) | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
292 changes: 292 additions & 0 deletions
292
src/all/sudatchi/src/eu/kanade/tachiyomi/animeextension/all/sudatchi/Sudatchi.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,292 @@ | ||
package eu.kanade.tachiyomi.animeextension.all.sudatchi | ||
|
||
import android.app.Application | ||
import android.content.SharedPreferences | ||
import androidx.preference.ListPreference | ||
import androidx.preference.PreferenceScreen | ||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.DirectoryDto | ||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomeListDto | ||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.LongAnimeDto | ||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.ShortAnimeDto | ||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.SubtitleDto | ||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.WatchDto | ||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource | ||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList | ||
import eu.kanade.tachiyomi.animesource.model.AnimesPage | ||
import eu.kanade.tachiyomi.animesource.model.SAnime | ||
import eu.kanade.tachiyomi.animesource.model.SEpisode | ||
import eu.kanade.tachiyomi.animesource.model.Track | ||
import eu.kanade.tachiyomi.animesource.model.Video | ||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource | ||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils | ||
import eu.kanade.tachiyomi.network.GET | ||
import eu.kanade.tachiyomi.network.awaitSuccess | ||
import eu.kanade.tachiyomi.util.asJsoup | ||
import eu.kanade.tachiyomi.util.parseAs | ||
import kotlinx.serialization.json.Json | ||
import okhttp3.HttpUrl.Companion.toHttpUrl | ||
import okhttp3.Request | ||
import okhttp3.Response | ||
import uy.kohesive.injekt.Injekt | ||
import uy.kohesive.injekt.api.get | ||
import uy.kohesive.injekt.injectLazy | ||
|
||
class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource { | ||
|
||
override val name = "Sudatchi" | ||
|
||
override val baseUrl = "https://sudatchi.com" | ||
|
||
override val lang = "all" | ||
|
||
override val supportsLatest = true | ||
|
||
private val codeRegex by lazy { Regex("""\((.*)\)""") } | ||
|
||
private val json: Json by injectLazy() | ||
|
||
private val sudatchiFilters: SudatchiFilters by lazy { SudatchiFilters(baseUrl, client) } | ||
|
||
private val preferences: SharedPreferences by lazy { | ||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) | ||
} | ||
|
||
// ============================== Popular =============================== | ||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/api/home-list", headers) | ||
|
||
private fun Int.parseStatus() = when (this) { | ||
1 -> SAnime.UNKNOWN // Not Yet Released | ||
2 -> SAnime.ONGOING | ||
3 -> SAnime.COMPLETED | ||
else -> SAnime.UNKNOWN | ||
} | ||
|
||
private fun ShortAnimeDto.toSAnime(titleLang: String) = SAnime.create().apply { | ||
url = "/anime/$slug" | ||
title = when (titleLang) { | ||
"romaji" -> titleRomanji | ||
"japanese" -> titleJapanese | ||
else -> titleEnglish | ||
} ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it } | ||
description = synopsis | ||
status = statusId.parseStatus() | ||
thumbnail_url = "$baseUrl$imgUrl" | ||
genre = animeGenres?.joinToString { it.genre.name } | ||
} | ||
|
||
override fun popularAnimeParse(response: Response): AnimesPage { | ||
sudatchiFilters.fetchFilters() | ||
val titleLang = preferences.title | ||
return AnimesPage(response.parseAs<HomeListDto>().animeSpotlight.map { it.toSAnime(titleLang) }, false) | ||
} | ||
|
||
// =============================== Latest =============================== | ||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/directory?page=$page&genres=&status=2,3", headers) | ||
|
||
override fun latestUpdatesParse(response: Response): AnimesPage { | ||
sudatchiFilters.fetchFilters() | ||
val titleLang = preferences.title | ||
return response.parseAs<DirectoryDto>().let { | ||
AnimesPage(it.animes.map { it.toSAnime(titleLang) }, it.page != it.pages) | ||
} | ||
} | ||
|
||
// =============================== Search =============================== | ||
override fun getFilterList() = sudatchiFilters.getFilterList() | ||
|
||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { | ||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler | ||
val id = query.removePrefix(PREFIX_SEARCH) | ||
client.newCall(GET("$baseUrl/api/anime/$id", headers)) | ||
.awaitSuccess() | ||
.use(::searchAnimeByIdParse) | ||
} else { | ||
super.getSearchAnime(page, query, filters) | ||
} | ||
} | ||
|
||
private fun searchAnimeByIdParse(response: Response) = AnimesPage(listOf(animeDetailsParse(response)), false) | ||
|
||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { | ||
val url = "$baseUrl/api/directory".toHttpUrl().newBuilder() | ||
url.addQueryParameter("page", page.toString()) | ||
url.addQueryParameter("title", query) | ||
filters.filterIsInstance<SudatchiFilters.QueryParameterFilter>().forEach { | ||
val (name, value) = it.toQueryParameter() | ||
if (value != null) url.addQueryParameter(name, value) | ||
} | ||
return GET(url.build(), headers) | ||
} | ||
|
||
override fun searchAnimeParse(response: Response) = latestUpdatesParse(response) | ||
|
||
// =========================== Anime Details ============================ | ||
override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}" | ||
|
||
override fun animeDetailsRequest(anime: SAnime) = GET("$baseUrl/api${anime.url}", headers) | ||
|
||
override fun animeDetailsParse(response: Response) = response.parseAs<ShortAnimeDto>().toSAnime(preferences.title) | ||
|
||
// ============================== Episodes ============================== | ||
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime) | ||
|
||
override fun episodeListParse(response: Response): List<SEpisode> { | ||
val anime = response.parseAs<LongAnimeDto>() | ||
return anime.episodes.map { | ||
SEpisode.create().apply { | ||
name = it.title | ||
episode_number = it.number.toFloat() | ||
url = "/watch/${anime.slug}/${it.number}" | ||
} | ||
}.reversed() | ||
} | ||
|
||
// ============================ Video Links ============================= | ||
override fun videoListRequest(episode: SEpisode) = GET("$baseUrl${episode.url}", headers) | ||
|
||
private val playlistUtils: PlaylistUtils by lazy { PlaylistUtils(client, headers) } | ||
|
||
override fun videoListParse(response: Response): List<Video> { | ||
val document = response.asJsoup() | ||
val jsonString = document.selectFirst("script#__NEXT_DATA__")?.data() ?: return emptyList() | ||
val data = json.decodeFromString<WatchDto>(jsonString).props.pageProps.episodeData | ||
val subtitles = json.decodeFromString<List<SubtitleDto>>(data.subtitlesJson) | ||
// val videoUrl = client.newCall(GET("$baseUrl/api/streams?episodeId=${data.episode.id}", headers)).execute().parseAs<StreamsDto>().url | ||
// keeping it in case the simpler solution breaks, can be hardcoded to this for now : | ||
val videoUrl = "$baseUrl/videos/m3u8/episode-${data.episode.id}.m3u8" | ||
return playlistUtils.extractFromHls( | ||
videoUrl, | ||
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" }, | ||
subtitleList = subtitles.map { | ||
Track("$baseUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})") | ||
}.sort(), | ||
) | ||
} | ||
|
||
@JvmName("trackSort") | ||
private fun List<Track>.sort(): List<Track> { | ||
val subtitles = preferences.subtitles | ||
return sortedWith( | ||
compareBy( | ||
{ codeRegex.find(it.lang)!!.groupValues[1] != subtitles }, | ||
{ codeRegex.find(it.lang)!!.groupValues[1] != PREF_SUBTITLES_DEFAULT }, | ||
{ it.lang }, | ||
), | ||
) | ||
} | ||
|
||
override fun List<Video>.sort(): List<Video> { | ||
val quality = preferences.quality | ||
return sortedWith( | ||
compareBy { it.quality.contains(quality) }, | ||
).reversed() | ||
} | ||
|
||
// ============================ Preferences ============================= | ||
override fun setupPreferenceScreen(screen: PreferenceScreen) { | ||
ListPreference(screen.context).apply { | ||
key = PREF_QUALITY_KEY | ||
title = PREF_QUALITY_TITLE | ||
entries = PREF_QUALITY_ENTRIES.map { it.first }.toTypedArray() | ||
entryValues = PREF_QUALITY_ENTRIES.map { it.second }.toTypedArray() | ||
setDefaultValue(PREF_QUALITY_DEFAULT) | ||
summary = "%s" | ||
|
||
setOnPreferenceChangeListener { _, new -> | ||
val index = findIndexOfValue(new as String) | ||
preferences.edit().putString(key, entryValues[index] as String).commit() | ||
} | ||
}.also(screen::addPreference) | ||
ListPreference(screen.context).apply { | ||
key = PREF_SUBTITLES_KEY | ||
title = PREF_SUBTITLES_TITLE | ||
entries = PREF_SUBTITLES_ENTRIES.map { it.first }.toTypedArray() | ||
entryValues = PREF_SUBTITLES_ENTRIES.map { it.second }.toTypedArray() | ||
setDefaultValue(PREF_SUBTITLES_DEFAULT) | ||
summary = "%s" | ||
|
||
setOnPreferenceChangeListener { _, new -> | ||
val index = findIndexOfValue(new as String) | ||
preferences.edit().putString(key, entryValues[index] as String).commit() | ||
} | ||
}.also(screen::addPreference) | ||
ListPreference(screen.context).apply { | ||
key = PREF_TITLE_KEY | ||
title = PREF_TITLE_TITLE | ||
entries = PREF_TITLE_ENTRIES.map { it.first }.toTypedArray() | ||
entryValues = PREF_TITLE_ENTRIES.map { it.second }.toTypedArray() | ||
setDefaultValue(PREF_TITLE_DEFAULT) | ||
summary = "%s" | ||
|
||
setOnPreferenceChangeListener { _, new -> | ||
val index = findIndexOfValue(new as String) | ||
preferences.edit().putString(key, entryValues[index] as String).commit() | ||
} | ||
}.also(screen::addPreference) | ||
} | ||
|
||
// ============================= Utilities ============================== | ||
private val SharedPreferences.quality get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! | ||
private val SharedPreferences.subtitles get() = getString(PREF_SUBTITLES_KEY, PREF_SUBTITLES_DEFAULT)!! | ||
private val SharedPreferences.title get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!! | ||
|
||
companion object { | ||
const val PREFIX_SEARCH = "id:" | ||
|
||
private const val PREF_QUALITY_KEY = "preferred_quality" | ||
private const val PREF_QUALITY_TITLE = "Preferred quality" | ||
private const val PREF_QUALITY_DEFAULT = "1080" | ||
private val PREF_QUALITY_ENTRIES = arrayOf( | ||
Pair("1080p", "1080"), | ||
Pair("720p", "720"), | ||
Pair("480p", "480"), | ||
) | ||
|
||
private const val PREF_SUBTITLES_KEY = "preferred_subtitles" | ||
private const val PREF_SUBTITLES_TITLE = "Preferred subtitles" | ||
private const val PREF_SUBTITLES_DEFAULT = "eng" | ||
private val PREF_SUBTITLES_ENTRIES = arrayOf( | ||
Pair("Arabic (Saudi Arabia)", "ara"), | ||
Pair("Brazilian Portuguese", "por"), | ||
Pair("Chinese", "chi"), | ||
Pair("Croatian", "hrv"), | ||
Pair("Czech", "cze"), | ||
Pair("Danish", "dan"), | ||
Pair("Dutch", "dut"), | ||
Pair("English", "eng"), | ||
Pair("European Spanish", "spa-es"), | ||
Pair("Filipino", "fil"), | ||
Pair("Finnish", "fin"), | ||
Pair("French", "fra"), | ||
Pair("German", "deu"), | ||
Pair("Greek", "gre"), | ||
Pair("Hebrew", "heb"), | ||
Pair("Hindi", "hin"), | ||
Pair("Hungarian", "hun"), | ||
Pair("Indonesian", "ind"), | ||
Pair("Italian", "ita"), | ||
Pair("Japanese", "jpn"), | ||
Pair("Korean", "kor"), | ||
Pair("Latin American Spanish", "spa-419"), | ||
Pair("Malay", "may"), | ||
Pair("Norwegian Bokmål", "nob"), | ||
Pair("Polish", "pol"), | ||
Pair("Romanian", "rum"), | ||
Pair("Russian", "rus"), | ||
Pair("Swedish", "swe"), | ||
Pair("Thai", "tha"), | ||
Pair("Turkish", "tur"), | ||
Pair("Ukrainian", "ukr"), | ||
Pair("Vietnamese", "vie"), | ||
) | ||
|
||
private const val PREF_TITLE_KEY = "preferred_title" | ||
private const val PREF_TITLE_TITLE = "Preferred title" | ||
private const val PREF_TITLE_DEFAULT = "english" | ||
private val PREF_TITLE_ENTRIES = arrayOf( | ||
Pair("English", "english"), | ||
Pair("Romaji", "romaji"), | ||
Pair("Japanese", "japanese"), | ||
) | ||
} | ||
} |
Oops, something went wrong.