diff --git a/src/en/aniplay/build.gradle b/src/en/aniplay/build.gradle new file mode 100644 index 00000000..f8264495 --- /dev/null +++ b/src/en/aniplay/build.gradle @@ -0,0 +1,12 @@ +ext { + extName = 'AniPlay' + extClass = '.AniPlay' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(":lib-multisrc:anilist")) + implementation(project(":lib:playlist-utils")) +} diff --git a/src/en/aniplay/ic_launcher-playstore.png b/src/en/aniplay/ic_launcher-playstore.png new file mode 100644 index 00000000..7e70c67f Binary files /dev/null and b/src/en/aniplay/ic_launcher-playstore.png differ diff --git a/src/en/aniplay/res/drawable/ic_launcher_foreground.xml b/src/en/aniplay/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..d150c0b4 --- /dev/null +++ b/src/en/aniplay/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher.xml b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/src/en/aniplay/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/en/aniplay/res/mipmap-hdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..940e0da3 Binary files /dev/null and b/src/en/aniplay/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/src/en/aniplay/res/mipmap-hdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..2387abfc Binary files /dev/null and b/src/en/aniplay/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/src/en/aniplay/res/mipmap-mdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..87d7ecc9 Binary files /dev/null and b/src/en/aniplay/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/src/en/aniplay/res/mipmap-mdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..4f02183c Binary files /dev/null and b/src/en/aniplay/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/src/en/aniplay/res/mipmap-xhdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..16243651 Binary files /dev/null and b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/src/en/aniplay/res/mipmap-xhdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..d883f306 Binary files /dev/null and b/src/en/aniplay/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..faf5bdf2 Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..712e3abf Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher.webp b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..04d7c07b Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..ad17c1ba Binary files /dev/null and b/src/en/aniplay/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/src/en/aniplay/res/values/ic_launcher_background.xml b/src/en/aniplay/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..de1841dc --- /dev/null +++ b/src/en/aniplay/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #05010D + \ No newline at end of file diff --git a/src/en/aniplay/res/web_hi_res_512.png b/src/en/aniplay/res/web_hi_res_512.png new file mode 100644 index 00000000..11b79914 Binary files /dev/null and b/src/en/aniplay/res/web_hi_res_512.png differ diff --git a/src/en/aniplay/src/eu/kanade/tachiyomi/animeextension/en/aniplay/AniPlay.kt b/src/en/aniplay/src/eu/kanade/tachiyomi/animeextension/en/aniplay/AniPlay.kt new file mode 100644 index 00000000..d86dfaed --- /dev/null +++ b/src/en/aniplay/src/eu/kanade/tachiyomi/animeextension/en/aniplay/AniPlay.kt @@ -0,0 +1,375 @@ +package eu.kanade.tachiyomi.animeextension.en.aniplay + +import android.app.Application +import android.util.Base64 +import android.widget.Toast +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +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.lib.playlistutils.PlaylistUtils +import eu.kanade.tachiyomi.multisrc.anilist.AniListAnimeHttpSource +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.parallelFlatMapBlocking +import eu.kanade.tachiyomi.util.parseAs +import kotlinx.serialization.encodeToString +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.SimpleDateFormat +import java.util.Locale + +class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource { + override val name = "AniPlay" + override val lang = "en" + + override val baseUrl: String + get() = "https://${preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)}" + + private val playlistUtils by lazy { PlaylistUtils(client, headers) } + + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + /* ================================= AniList configurations ================================= */ + + override fun mapAnimeDetailUrl(animeId: Int): String { + return "$baseUrl/anime/info/$animeId" + } + + override fun mapAnimeId(animeDetailUrl: String): Int { + val httpUrl = animeDetailUrl.toHttpUrl() + + return httpUrl.pathSegments[2].toInt() + } + + override fun getPreferredTitleLanguage(): TitleLanguage { + val preferredLanguage = preferences.getString(PREF_TITLE_LANGUAGE_KEY, PREF_TITLE_LANGUAGE_DEFAULT) + + return when (preferredLanguage) { + "romaji" -> TitleLanguage.ROMAJI + "english" -> TitleLanguage.ENGLISH + "native" -> TitleLanguage.NATIVE + else -> TitleLanguage.ROMAJI + } + } + + /* ====================================== Episode List ====================================== */ + + override fun episodeListRequest(anime: SAnime): Request { + val httpUrl = anime.url.toHttpUrl() + val animeId = httpUrl.pathSegments[2] + + return GET("$baseUrl/api/anime/episode/$animeId") + } + + override fun episodeListParse(response: Response): List { + val isMarkFiller = preferences.getBoolean(PREF_MARK_FILLER_EPISODE_KEY, PREF_MARK_FILLER_EPISODE_DEFAULT) + val episodeListUrl = response.request.url + val animeId = episodeListUrl.pathSegments[3] + val providers = response.parseAs>() + val episodes = mutableMapOf() + val episodeExtras = mutableMapOf>() + + providers.forEach { provider -> + provider.episodes.forEach { episode -> + if (!episodes.containsKey(episode.number)) { + episodes[episode.number] = episode + } + val existingEpisodeExtras = episodeExtras.getOrElse(episode.number) { emptyList() } + val episodeExtra = EpisodeExtra( + source = provider.providerId, + episodeId = episode.id, + hasDub = episode.hasDub, + ) + episodeExtras[episode.number] = existingEpisodeExtras + listOf(episodeExtra) + } + } + + return episodes.map { episodeMap -> + val episode = episodeMap.value + val episodeNumber = episode.number + val episodeExtra = episodeExtras.getValue(episodeNumber) + val episodeExtraString = json.encodeToString(episodeExtra) + .let { Base64.encode(it.toByteArray(), Base64.DEFAULT) } + .toString(Charsets.UTF_8) + + val url = baseUrl.toHttpUrl().newBuilder() + .addPathSegment("anime") + .addPathSegment("watch") + .addQueryParameter("id", animeId) + .addQueryParameter("ep", episodeNumber.toString()) + .addQueryParameter("extras", episodeExtraString) + .build() + + val name = parseEpisodeName(episodeNumber, episode.title) + val uploadDate = parseDate(episode.createdAt) + val dub = when { + episodeExtra.any { it.hasDub } -> ", Dub" + else -> "" + } + val filler = when { + episode.isFiller && isMarkFiller -> " • Filler Episode" + else -> "" + } + val scanlator = "Sub$dub$filler" + + SEpisode.create().apply { + this.url = url.toString() + this.name = name + this.date_upload = uploadDate + this.episode_number = episodeNumber.toFloat() + this.scanlator = scanlator + } + }.reversed() + } + + /* ======================================= Video List ======================================= */ + + override suspend fun getVideoList(episode: SEpisode): List