diff --git a/src/ru/animelib/build.gradle b/src/ru/animelib/build.gradle new file mode 100644 index 000000000..79f45dd8d --- /dev/null +++ b/src/ru/animelib/build.gradle @@ -0,0 +1,12 @@ +ext { + extName = 'Animelib' + extClass = '.Animelib' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation project(':lib:playlist-utils') +} \ No newline at end of file diff --git a/src/ru/animelib/res/mipmap-hdpi/ic_launcher.png b/src/ru/animelib/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..b3c00da43 Binary files /dev/null and b/src/ru/animelib/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ru/animelib/res/mipmap-mdpi/ic_launcher.png b/src/ru/animelib/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..a92b0439c Binary files /dev/null and b/src/ru/animelib/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ru/animelib/res/mipmap-xhdpi/ic_launcher.png b/src/ru/animelib/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..1636c0daa Binary files /dev/null and b/src/ru/animelib/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ru/animelib/res/mipmap-xxhdpi/ic_launcher.png b/src/ru/animelib/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..1eb09e12d Binary files /dev/null and b/src/ru/animelib/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ru/animelib/res/mipmap-xxxhdpi/ic_launcher.png b/src/ru/animelib/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..3be7f05be Binary files /dev/null and b/src/ru/animelib/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ru/animelib/src/eu/kanade/tachiyomi/animeextension/ru/animelib/Animelib.kt b/src/ru/animelib/src/eu/kanade/tachiyomi/animeextension/ru/animelib/Animelib.kt new file mode 100644 index 000000000..cec264b35 --- /dev/null +++ b/src/ru/animelib/src/eu/kanade/tachiyomi/animeextension/ru/animelib/Animelib.kt @@ -0,0 +1,495 @@ +package eu.kanade.tachiyomi.animeextension.ru.animelib + +import android.app.Application +import android.util.Base64 +import android.widget.Toast +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import app.cash.quickjs.QuickJs +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.POST +import eu.kanade.tachiyomi.util.asJsoup +import eu.kanade.tachiyomi.util.parseAs +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.net.URLDecoder +import java.text.SimpleDateFormat +import java.util.Locale + +class Animelib : ConfigurableAnimeSource, AnimeHttpSource() { + + override val name = "Animelib" + + override val lang = "ru" + + override val supportsLatest = true + + private val domain = "anilib.me" + override val baseUrl = "https://$domain/ru" + private val apiUrl = "https://api.lib.social/api" + + private val playlistUtils by lazy { PlaylistUtils(client, headers) } + private val dateFormatter by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) } + + companion object { + private const val PREF_QUALITY_KEY = "pref_quality" + private val PREF_QUALITY_ENTRIES = arrayOf("360", "720", "1080", "2160") + + private const val PREF_USE_MAX_QUALITY_KEY = "pref_use_max_quality" + private const val PREF_USE_MAX_QUALITY_DEFAULT = true + + private const val PREF_SERVER_KEY = "pref_server" + private val PREF_SERVER_ENTRIES = arrayOf("Основной", "Резервный 1", "Резервный 2") + + private const val PREF_DUB_TEAM_KEY = "prev_dub_team" + + private const val PREF_IGNORE_SUBS_KEY = "pref_ignore_subs" + private const val PREF_IGNORE_SUBS_DEFAULT = true + + private const val PREF_USE_KODIK_KEY = "pref_use_kodik" + private const val PREF_USE_KODIK_DEFAULT = true + + private val ATOB_REGEX = Regex("atob\\([^\"]") + } + + // =============================== Preference =============================== + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = PREF_SERVER_KEY + title = "Предпочитаемый сервер плеера Animelib" + entries = PREF_SERVER_ENTRIES + entryValues = PREF_SERVER_ENTRIES + summary = "%s" + setDefaultValue(PREF_SERVER_ENTRIES[0]) + + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putString(key, newValue as String).commit() + } + }.also(screen::addPreference) + + SwitchPreferenceCompat(screen.context).apply { + key = PREF_USE_MAX_QUALITY_KEY + title = "Использовать максимальное доступное качество" + summary = "Для каждой студии озвучки будет выбрано максимальное качество" + setDefaultValue(PREF_USE_MAX_QUALITY_DEFAULT) + + setOnPreferenceChangeListener { _, newValue -> + val value = newValue as Boolean + + val text = if (value) { + "Предпочитаемое качество пропадет после закрытия окна настроек" + } else { + "Откройте настройки заново чтобы выбрать предпочитаемое качество" + } + Toast.makeText(screen.context, text, Toast.LENGTH_LONG).show() + + preferences.edit().putBoolean(key, value).commit() + } + }.also(screen::addPreference) + + if (!preferences.getBoolean(PREF_USE_MAX_QUALITY_KEY, true)) { + MultiSelectListPreference(screen.context).apply { + key = PREF_QUALITY_KEY + title = "Предпочитаемое качество" + entries = PREF_QUALITY_ENTRIES + entryValues = PREF_QUALITY_ENTRIES + summary = "При отсутствии нужного качества могут возникать ошибки!" + setDefaultValue(PREF_QUALITY_ENTRIES.toSet()) + + setOnPreferenceChangeListener { _, newValue -> + @Suppress("UNCHECKED_CAST") + preferences.edit().putStringSet(key, newValue as Set).commit() + } + }.also(screen::addPreference) + } + + SwitchPreferenceCompat(screen.context).apply { + key = PREF_USE_KODIK_KEY + title = "Включить парсинг видео из плеера Kodik" + summary = "Некоторые видео доступны только в нем, но он может работать нестабильно" + setDefaultValue(PREF_USE_KODIK_DEFAULT) + + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(key, newValue as Boolean).commit() + } + }.also(screen::addPreference) + + SwitchPreferenceCompat(screen.context).apply { + key = PREF_IGNORE_SUBS_KEY + title = "Игнорировать субтитры" + summary = "Исключает видео с субтитрами" + setDefaultValue(PREF_IGNORE_SUBS_DEFAULT) + + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(key, newValue as Boolean).commit() + } + }.also(screen::addPreference) + + EditTextPreference(screen.context).apply { + key = PREF_DUB_TEAM_KEY + title = "Предпочитаемые студии озвучки" + summary = "Список студий или ключевых слов через запятую (экспериментальная функция)" + setDefaultValue("") + + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putString(key, newValue as String).commit() + } + }.also(screen::addPreference) + } + + // =============================== Details =============================== + override fun animeDetailsRequest(anime: SAnime): Request { + val url = apiUrl.toHttpUrl().newBuilder() + url.addPathSegment("anime") + url.addPathSegment(anime.url) + url.addQueryParameter("fields[]", "genres") + url.addQueryParameter("fields[]", "summary") + url.addQueryParameter("fields[]", "authors") + url.addQueryParameter("fields[]", "publisher") + url.addQueryParameter("fields[]", "otherNames") + url.addQueryParameter("fields[]", "anime_status_id") + + return GET(url.build()) + } + + override fun getAnimeUrl(anime: SAnime) = "$baseUrl/${anime.url}" + + override fun animeDetailsParse(response: Response) = response.parseAs().data.toSAnime() + + // =============================== Episodes =============================== + override fun episodeListRequest(anime: SAnime): Request { + val url = apiUrl.toHttpUrl().newBuilder() + url.addPathSegment("episodes") + url.addQueryParameter("anime_id", anime.url) + + return GET(url.build()) + } + + override fun episodeListParse(response: Response): List { + val episodeList = response.parseAs() + + return episodeList.data.map { it.toSEpisode() }.reversed() + } + + // =============================== Video List =============================== + override fun videoListParse(response: Response): List