diff --git a/src/all/sudatchi/AndroidManifest.xml b/src/all/sudatchi/AndroidManifest.xml new file mode 100644 index 0000000000..dc4be11abe --- /dev/null +++ b/src/all/sudatchi/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/all/sudatchi/build.gradle b/src/all/sudatchi/build.gradle new file mode 100644 index 0000000000..cbd13790e6 --- /dev/null +++ b/src/all/sudatchi/build.gradle @@ -0,0 +1,11 @@ +ext { + extName = 'Sudatchi' + extClass = '.Sudatchi' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(":lib:playlist-utils")) +} diff --git a/src/all/sudatchi/res/mipmap-hdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..0e72fb45f0 Binary files /dev/null and b/src/all/sudatchi/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/sudatchi/res/mipmap-mdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..fb9e1024af Binary files /dev/null and b/src/all/sudatchi/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/sudatchi/res/mipmap-xhdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..199c55fbef Binary files /dev/null and b/src/all/sudatchi/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/sudatchi/res/mipmap-xxhdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..5bed2af8b2 Binary files /dev/null and b/src/all/sudatchi/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/sudatchi/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/sudatchi/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..4022b54833 Binary files /dev/null and b/src/all/sudatchi/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/sudatchi/src/eu/kanade/tachiyomi/animeextension/all/sudatchi/Sudatchi.kt b/src/all/sudatchi/src/eu/kanade/tachiyomi/animeextension/all/sudatchi/Sudatchi.kt new file mode 100644 index 0000000000..7e485873ba --- /dev/null +++ b/src/all/sudatchi/src/eu/kanade/tachiyomi/animeextension/all/sudatchi/Sudatchi.kt @@ -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().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().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().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().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().toSAnime(preferences.title) + + // ============================== Episodes ============================== + override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime) + + override fun episodeListParse(response: Response): List { + val anime = response.parseAs() + 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