forked from Kohi-den/extensions-source
Initial commit
This commit is contained in:
commit
98ed7e8839
2263 changed files with 108711 additions and 0 deletions
7
src/all/jellyfin/build.gradle
Normal file
7
src/all/jellyfin/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Jellyfin'
|
||||
extClass = '.JellyfinFactory'
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/jellyfin/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/all/jellyfin/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
src/all/jellyfin/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
src/all/jellyfin/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
src/all/jellyfin/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
src/all/jellyfin/res/web_hi_res_512.png
Normal file
BIN
src/all/jellyfin/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
|
@ -0,0 +1,803 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.text.InputType
|
||||
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 eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.UnmeteredSource
|
||||
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.network.GET
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Dns
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpSource(), UnmeteredSource {
|
||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val name by lazy { "Jellyfin (${getCustomLabel()})" }
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private fun getUnsafeOkHttpClient(): OkHttpClient {
|
||||
// Create a trust manager that does not validate certificate chains
|
||||
val trustAllCerts = arrayOf<TrustManager>(
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
object : X509TrustManager {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
|
||||
},
|
||||
)
|
||||
|
||||
// Install the all-trusting trust manager
|
||||
val sslContext = SSLContext.getInstance("SSL")
|
||||
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
|
||||
// Create an ssl socket factory with our all-trusting manager
|
||||
val sslSocketFactory = sslContext.socketFactory
|
||||
|
||||
return network.client.newBuilder()
|
||||
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
||||
.hostnameVerifier { _, _ -> true }.build()
|
||||
}
|
||||
|
||||
override val client by lazy {
|
||||
if (preferences.getTrustCert) {
|
||||
getUnsafeOkHttpClient()
|
||||
} else {
|
||||
network.client
|
||||
}.newBuilder()
|
||||
.dns(Dns.SYSTEM)
|
||||
.build()
|
||||
}
|
||||
|
||||
override val id by lazy {
|
||||
val key = "jellyfin" + (if (suffix == "1") "" else " ($suffix)") + "/all/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
|
||||
internal val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private var username = preferences.getUserName
|
||||
private var password = preferences.getPassword
|
||||
private var parentId = preferences.getMediaLibId
|
||||
private var apiKey = preferences.getApiKey
|
||||
private var userId = preferences.getUserId
|
||||
|
||||
init {
|
||||
login(false)
|
||||
}
|
||||
|
||||
private fun login(new: Boolean, context: Context? = null): Boolean? {
|
||||
if (apiKey == null || userId == null || new) {
|
||||
username = preferences.getUserName
|
||||
password = preferences.getPassword
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
if (username != "demo") return null
|
||||
}
|
||||
val (newKey, newUid) = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
JellyfinAuthenticator(preferences, getPrefBaseUrl(), client)
|
||||
.login(username, password)
|
||||
}
|
||||
}
|
||||
if (newKey != null && newUid != null) {
|
||||
apiKey = newKey
|
||||
userId = newUid
|
||||
} else {
|
||||
context?.let { Toast.makeText(it, "Login failed.", Toast.LENGTH_LONG).show() }
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
require(parentId.isNotEmpty()) { "Select library in the extension settings." }
|
||||
val startIndex = (page - 1) * SEASONS_LIMIT
|
||||
|
||||
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("api_key", apiKey)
|
||||
addQueryParameter("StartIndex", startIndex.toString())
|
||||
addQueryParameter("Limit", SEASONS_LIMIT.toString())
|
||||
addQueryParameter("Recursive", "true")
|
||||
addQueryParameter("SortBy", "SortName")
|
||||
addQueryParameter("SortOrder", "Ascending")
|
||||
addQueryParameter("IncludeItemTypes", "Movie,Season,BoxSet")
|
||||
addQueryParameter("ImageTypeLimit", "1")
|
||||
addQueryParameter("ParentId", parentId)
|
||||
addQueryParameter("EnableImageTypes", "Primary")
|
||||
}.build()
|
||||
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val splitCollections = preferences.getSplitCol
|
||||
val page = response.request.url.queryParameter("StartIndex")!!.toInt() / SEASONS_LIMIT + 1
|
||||
val data = response.parseAs<ItemsDto>()
|
||||
val animeList = data.items.flatMap {
|
||||
if (it.type == "BoxSet" && splitCollections) {
|
||||
val url = popularAnimeRequest(page).url.newBuilder().apply {
|
||||
setQueryParameter("ParentId", it.id)
|
||||
}.build()
|
||||
|
||||
popularAnimeParse(
|
||||
client.newCall(GET(url)).execute(),
|
||||
).animes
|
||||
} else {
|
||||
listOf(it.toSAnime(baseUrl, userId!!, apiKey!!))
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, SEASONS_LIMIT * page < data.itemCount)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = popularAnimeRequest(page).url.newBuilder().apply {
|
||||
setQueryParameter("SortBy", "DateCreated,SortName")
|
||||
setQueryParameter("SortOrder", "Descending")
|
||||
}.build()
|
||||
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage =
|
||||
popularAnimeParse(response)
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = popularAnimeRequest(page).url.newBuilder().apply {
|
||||
// Search for series, rather than seasons, since season names can just be "Season 1"
|
||||
setQueryParameter("IncludeItemTypes", "Movie,Series")
|
||||
setQueryParameter("Limit", SERIES_LIMIT.toString())
|
||||
setQueryParameter("SearchTerm", query)
|
||||
}.build()
|
||||
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val page = response.request.url.queryParameter("StartIndex")!!.toInt() / SERIES_LIMIT + 1
|
||||
val data = response.parseAs<ItemsDto>()
|
||||
|
||||
// Get all seasons from series
|
||||
val animeList = data.items.flatMap { series ->
|
||||
val seasonsUrl = popularAnimeRequest(1).url.newBuilder().apply {
|
||||
setQueryParameter("ParentId", series.id)
|
||||
removeAllQueryParameters("StartIndex")
|
||||
removeAllQueryParameters("Limit")
|
||||
}.build()
|
||||
|
||||
val seasonsData = client.newCall(
|
||||
GET(seasonsUrl),
|
||||
).execute().parseAs<ItemsDto>()
|
||||
|
||||
seasonsData.items.map { it.toSAnime(baseUrl, userId!!, apiKey!!) }
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, SERIES_LIMIT * page < data.itemCount)
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
if (!anime.url.startsWith("http")) throw Exception("Migrate from jellyfin to jellyfin")
|
||||
return GET(anime.url)
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val data = response.parseAs<ItemDto>()
|
||||
val infoData = if (preferences.useSeriesData && data.seriesId != null) {
|
||||
val url = response.request.url.let { url ->
|
||||
url.newBuilder().apply {
|
||||
removePathSegment(url.pathSize - 1)
|
||||
addPathSegment(data.seriesId)
|
||||
}.build()
|
||||
}
|
||||
|
||||
client.newCall(
|
||||
GET(url),
|
||||
).execute().parseAs<ItemDto>()
|
||||
} else {
|
||||
data
|
||||
}
|
||||
|
||||
return infoData.toSAnime(baseUrl, userId!!, apiKey!!)
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
if (!anime.url.startsWith("http")) throw Exception("Migrate from jellyfin to jellyfin")
|
||||
val httpUrl = anime.url.toHttpUrl()
|
||||
val fragment = httpUrl.fragment!!
|
||||
|
||||
// Get episodes of season
|
||||
val url = if (fragment.startsWith("seriesId")) {
|
||||
httpUrl.newBuilder().apply {
|
||||
encodedPath("/")
|
||||
encodedQuery(null)
|
||||
fragment(null)
|
||||
|
||||
addPathSegment("Shows")
|
||||
addPathSegment(fragment.split(",").last())
|
||||
addPathSegment("Episodes")
|
||||
addQueryParameter("api_key", apiKey)
|
||||
addQueryParameter("seasonId", httpUrl.pathSegments.last())
|
||||
addQueryParameter("userId", userId)
|
||||
addQueryParameter("Fields", "Overview,MediaSources")
|
||||
}.build()
|
||||
} else if (fragment.startsWith("movie")) {
|
||||
httpUrl.newBuilder().fragment(null).build()
|
||||
} else if (fragment.startsWith("boxSet")) {
|
||||
val itemId = httpUrl.pathSegments[3]
|
||||
httpUrl.newBuilder().apply {
|
||||
removePathSegment(3)
|
||||
addQueryParameter("Recursive", "true")
|
||||
addQueryParameter("SortBy", "SortName")
|
||||
addQueryParameter("SortOrder", "Ascending")
|
||||
addQueryParameter("IncludeItemTypes", "Movie,Season,BoxSet,Series")
|
||||
addQueryParameter("ParentId", itemId)
|
||||
}.build()
|
||||
} else if (fragment.startsWith("series")) {
|
||||
val itemId = httpUrl.pathSegments[3]
|
||||
httpUrl.newBuilder().apply {
|
||||
encodedPath("/")
|
||||
encodedQuery(null)
|
||||
addPathSegment("Shows")
|
||||
addPathSegment(itemId)
|
||||
addPathSegment("Episodes")
|
||||
addQueryParameter("api_key", apiKey)
|
||||
}.build()
|
||||
} else {
|
||||
httpUrl
|
||||
}
|
||||
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val httpUrl = response.request.url
|
||||
val episodeList = if (httpUrl.fragment == "boxSet") {
|
||||
val data = response.parseAs<ItemsDto>()
|
||||
val animeList = data.items.map {
|
||||
it.toSAnime(baseUrl, userId!!, apiKey!!)
|
||||
}.sortedByDescending { it.title }
|
||||
animeList.flatMap {
|
||||
client.newCall(episodeListRequest(it))
|
||||
.execute()
|
||||
.let { res ->
|
||||
episodeListParse(res, "${it.title} - ")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
episodeListParse(response, "")
|
||||
}
|
||||
|
||||
return if (preferences.sortEp) {
|
||||
episodeList.sortedByDescending { it.date_upload }
|
||||
} else {
|
||||
episodeList
|
||||
}
|
||||
}
|
||||
|
||||
private fun episodeListParse(response: Response, prefix: String): List<SEpisode> {
|
||||
val httpUrl = response.request.url
|
||||
val epDetails = preferences.getEpDetails
|
||||
return if (response.request.url.toString().startsWith("$baseUrl/Users/")) {
|
||||
val data = response.parseAs<ItemDto>()
|
||||
listOf(data.toSEpisode(baseUrl, userId!!, apiKey!!, epDetails, EpisodeType.MOVIE, prefix))
|
||||
} else if (httpUrl.fragment == "series") {
|
||||
val data = response.parseAs<ItemsDto>()
|
||||
data.items.map {
|
||||
val name = prefix + (it.seasonName?.let { "$it - " } ?: "")
|
||||
it.toSEpisode(baseUrl, userId!!, apiKey!!, epDetails, EpisodeType.EPISODE, name)
|
||||
}
|
||||
} else {
|
||||
val data = response.parseAs<ItemsDto>()
|
||||
data.items.map {
|
||||
it.toSEpisode(baseUrl, userId!!, apiKey!!, epDetails, EpisodeType.EPISODE, prefix)
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
enum class EpisodeType {
|
||||
EPISODE,
|
||||
MOVIE,
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
if (!episode.url.startsWith("http")) throw Exception("Migrate from jellyfin to jellyfin")
|
||||
return GET(episode.url)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val id = response.parseAs<ItemDto>().id
|
||||
|
||||
val sessionData = client.newCall(
|
||||
GET("$baseUrl/Items/$id/PlaybackInfo?userId=$userId&api_key=$apiKey"),
|
||||
).execute().parseAs<SessionDto>()
|
||||
|
||||
val videoList = mutableListOf<Video>()
|
||||
val subtitleList = mutableListOf<Track>()
|
||||
val externalSubtitleList = mutableListOf<Track>()
|
||||
|
||||
val prefSub = preferences.getSubPref
|
||||
val prefAudio = preferences.getAudioPref
|
||||
|
||||
var audioIndex = 1
|
||||
var subIndex: Int? = null
|
||||
var width = 1920
|
||||
var height = 1080
|
||||
|
||||
sessionData.mediaSources.first().mediaStreams.forEach { media ->
|
||||
when (media.type) {
|
||||
"Video" -> {
|
||||
width = media.width!!
|
||||
height = media.height!!
|
||||
}
|
||||
"Audio" -> {
|
||||
if (media.lang != null && media.lang == prefAudio) {
|
||||
audioIndex = media.index
|
||||
}
|
||||
}
|
||||
"Subtitle" -> {
|
||||
if (media.supportsExternalStream) {
|
||||
val subtitleUrl = "$baseUrl/Videos/$id/$id/Subtitles/${media.index}/0/Stream.${media.codec}?api_key=$apiKey"
|
||||
if (media.lang != null) {
|
||||
if (media.lang == prefSub) {
|
||||
try {
|
||||
if (media.isExternal) {
|
||||
externalSubtitleList.add(0, Track(subtitleUrl, media.displayTitle!!))
|
||||
}
|
||||
subtitleList.add(0, Track(subtitleUrl, media.displayTitle!!))
|
||||
} catch (e: Exception) {
|
||||
subIndex = media.index
|
||||
}
|
||||
} else {
|
||||
if (media.isExternal) {
|
||||
externalSubtitleList.add(Track(subtitleUrl, media.displayTitle!!))
|
||||
}
|
||||
subtitleList.add(Track(subtitleUrl, media.displayTitle!!))
|
||||
}
|
||||
} else {
|
||||
if (media.isExternal) {
|
||||
externalSubtitleList.add(Track(subtitleUrl, media.displayTitle!!))
|
||||
}
|
||||
subtitleList.add(Track(subtitleUrl, media.displayTitle!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop over qualities
|
||||
JellyfinConstants.QUALITIES_LIST.forEach { quality ->
|
||||
if (width < quality.width && height < quality.height) {
|
||||
val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey"
|
||||
videoList.add(Video(url, "Source", url, subtitleTracks = externalSubtitleList))
|
||||
|
||||
return videoList.reversed()
|
||||
} else {
|
||||
val url = "$baseUrl/videos/$id/main.m3u8".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("api_key", apiKey)
|
||||
addQueryParameter("VideoCodec", "h264")
|
||||
addQueryParameter("AudioCodec", "aac,mp3")
|
||||
addQueryParameter("AudioStreamIndex", audioIndex.toString())
|
||||
subIndex?.let { addQueryParameter("SubtitleStreamIndex", it.toString()) }
|
||||
addQueryParameter("VideoCodec", "h264")
|
||||
addQueryParameter("VideoCodec", "h264")
|
||||
addQueryParameter(
|
||||
"VideoBitrate",
|
||||
quality.videoBitrate.toString(),
|
||||
)
|
||||
addQueryParameter(
|
||||
"AudioBitrate",
|
||||
quality.audioBitrate.toString(),
|
||||
)
|
||||
addQueryParameter("PlaySessionId", sessionData.playSessionId)
|
||||
addQueryParameter("TranscodingMaxAudioChannels", "6")
|
||||
addQueryParameter("RequireAvc", "false")
|
||||
addQueryParameter("SegmentContainer", "ts")
|
||||
addQueryParameter("MinSegments", "1")
|
||||
addQueryParameter("BreakOnNonKeyFrames", "true")
|
||||
addQueryParameter("h264-profile", "high,main,baseline,constrainedbaseline")
|
||||
addQueryParameter("h264-level", "51")
|
||||
addQueryParameter("h264-deinterlace", "true")
|
||||
addQueryParameter("TranscodeReasons", "VideoCodecNotSupported,AudioCodecNotSupported,ContainerBitrateExceedsLimit")
|
||||
}
|
||||
videoList.add(Video(url.toString(), quality.description, url.toString(), subtitleTracks = subtitleList))
|
||||
}
|
||||
}
|
||||
|
||||
val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey"
|
||||
videoList.add(Video(url, "Source", url, subtitleTracks = externalSubtitleList))
|
||||
|
||||
return videoList.reversed()
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
companion object {
|
||||
const val APIKEY_KEY = "api_key"
|
||||
const val USERID_KEY = "user_id"
|
||||
|
||||
internal const val EXTRA_SOURCES_COUNT_KEY = "extraSourcesCount"
|
||||
internal const val EXTRA_SOURCES_COUNT_DEFAULT = "3"
|
||||
private val EXTRA_SOURCES_ENTRIES = (1..10).map { it.toString() }.toTypedArray()
|
||||
|
||||
private const val PREF_CUSTOM_LABEL_KEY = "pref_label"
|
||||
private const val PREF_CUSTOM_LABEL_DEFAULT = ""
|
||||
|
||||
private const val HOSTURL_KEY = "host_url"
|
||||
private const val HOSTURL_DEFAULT = "http://127.0.0.1:8096"
|
||||
|
||||
private const val USERNAME_KEY = "username"
|
||||
private const val USERNAME_DEFAULT = ""
|
||||
|
||||
private const val PASSWORD_KEY = "password"
|
||||
private const val PASSWORD_DEFAULT = ""
|
||||
|
||||
private const val MEDIALIB_KEY = "library_pref"
|
||||
private const val MEDIALIB_DEFAULT = ""
|
||||
|
||||
private const val SEASONS_LIMIT = 20
|
||||
private const val SERIES_LIMIT = 5
|
||||
|
||||
private const val PREF_EP_DETAILS_KEY = "pref_episode_details_key"
|
||||
private val PREF_EP_DETAILS = arrayOf("Overview", "Runtime", "Size")
|
||||
private val PREF_EP_DETAILS_DEFAULT = emptySet<String>()
|
||||
|
||||
private const val PREF_SUB_KEY = "preferred_subLang"
|
||||
private const val PREF_SUB_DEFAULT = "eng"
|
||||
|
||||
private const val PREF_AUDIO_KEY = "preferred_audioLang"
|
||||
private const val PREF_AUDIO_DEFAULT = "jpn"
|
||||
|
||||
private const val PREF_INFO_TYPE = "preferred_meta_type"
|
||||
private const val PREF_INFO_DEFAULT = false
|
||||
|
||||
private const val PREF_TRUST_CERT_KEY = "preferred_trust_all_certs"
|
||||
private const val PREF_TRUST_CERT_DEFAULT = false
|
||||
|
||||
private const val PREF_SPLIT_COLLECTIONS_KEY = "preferred_split_col"
|
||||
private const val PREF_SPLIT_COLLECTIONS_DEFAULT = false
|
||||
|
||||
private const val PREF_SORT_EPISODES_KEY = "preferred_sort_ep"
|
||||
private const val PREF_SORT_EPISODES_DEFAULT = false
|
||||
}
|
||||
|
||||
private fun getCustomLabel(): String =
|
||||
preferences.getString(PREF_CUSTOM_LABEL_KEY, suffix)!!.ifBlank { suffix }
|
||||
|
||||
private fun getPrefBaseUrl(): String =
|
||||
preferences.getString(HOSTURL_KEY, HOSTURL_DEFAULT)!!
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
if (suffix == "1") {
|
||||
ListPreference(screen.context).apply {
|
||||
key = EXTRA_SOURCES_COUNT_KEY
|
||||
title = "Number of sources"
|
||||
summary = "Number of jellyfin sources to create. There will always be at least one Jellyfin source."
|
||||
entries = EXTRA_SOURCES_ENTRIES
|
||||
entryValues = EXTRA_SOURCES_ENTRIES
|
||||
|
||||
setDefaultValue(EXTRA_SOURCES_COUNT_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val setting = preferences.edit().putString(EXTRA_SOURCES_COUNT_KEY, newValue as String).commit()
|
||||
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
setting
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = PREF_CUSTOM_LABEL_KEY
|
||||
title = "Custom Label"
|
||||
summary = "Show the given label for the source instead of the default."
|
||||
setDefaultValue(PREF_CUSTOM_LABEL_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
runCatching {
|
||||
val value = (newValue as String).trim().ifBlank { PREF_CUSTOM_LABEL_DEFAULT }
|
||||
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
preferences.edit().putString(key, value).commit()
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
val mediaLibPref = medialibPreference(screen)
|
||||
screen.addPreference(
|
||||
screen.editTextPreference(
|
||||
HOSTURL_KEY,
|
||||
"Host URL",
|
||||
HOSTURL_DEFAULT,
|
||||
baseUrl,
|
||||
false,
|
||||
"",
|
||||
mediaLibPref,
|
||||
),
|
||||
)
|
||||
screen.addPreference(
|
||||
screen.editTextPreference(
|
||||
USERNAME_KEY,
|
||||
"Username",
|
||||
USERNAME_DEFAULT,
|
||||
username,
|
||||
false,
|
||||
"The account username",
|
||||
mediaLibPref,
|
||||
),
|
||||
)
|
||||
screen.addPreference(
|
||||
screen.editTextPreference(
|
||||
PASSWORD_KEY,
|
||||
"Password",
|
||||
PASSWORD_DEFAULT,
|
||||
password,
|
||||
true,
|
||||
"••••••••",
|
||||
mediaLibPref,
|
||||
),
|
||||
)
|
||||
screen.addPreference(mediaLibPref)
|
||||
|
||||
MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_EP_DETAILS_KEY
|
||||
title = "Additional details for episodes"
|
||||
summary = "Show additional details about an episode in the scanlator field"
|
||||
entries = PREF_EP_DETAILS
|
||||
entryValues = PREF_EP_DETAILS
|
||||
setDefaultValue(PREF_EP_DETAILS_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SUB_KEY
|
||||
title = "Preferred sub language"
|
||||
entries = JellyfinConstants.PREF_ENTRIES
|
||||
entryValues = JellyfinConstants.PREF_VALUES
|
||||
setDefaultValue(PREF_SUB_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_AUDIO_KEY
|
||||
title = "Preferred audio language"
|
||||
entries = JellyfinConstants.PREF_ENTRIES
|
||||
entryValues = JellyfinConstants.PREF_VALUES
|
||||
setDefaultValue(PREF_AUDIO_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_INFO_TYPE
|
||||
title = "Retrieve metadata from series"
|
||||
summary = """Enable this to retrieve metadata from series instead of season when applicable.""".trimMargin()
|
||||
setDefaultValue(PREF_INFO_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_TRUST_CERT_KEY
|
||||
title = "Trust all certificates"
|
||||
summary = "Requires app restart to take effect."
|
||||
setDefaultValue(PREF_TRUST_CERT_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_SPLIT_COLLECTIONS_KEY
|
||||
title = "Split collections"
|
||||
summary = "Split each item in a collection into its own entry"
|
||||
setDefaultValue(PREF_SPLIT_COLLECTIONS_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_SORT_EPISODES_KEY
|
||||
title = "Sort episodes by release date"
|
||||
summary = "Useful for collections, otherwise items in a collection are grouped by name."
|
||||
setDefaultValue(PREF_SORT_EPISODES_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private val SharedPreferences.getApiKey
|
||||
get() = getString(APIKEY_KEY, null)
|
||||
|
||||
private val SharedPreferences.getUserId
|
||||
get() = getString(USERID_KEY, null)
|
||||
|
||||
private val SharedPreferences.getUserName
|
||||
get() = getString(USERNAME_KEY, USERNAME_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getPassword
|
||||
get() = getString(PASSWORD_KEY, PASSWORD_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getMediaLibId
|
||||
get() = getString(MEDIALIB_KEY, MEDIALIB_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getEpDetails
|
||||
get() = getStringSet(PREF_EP_DETAILS_KEY, PREF_EP_DETAILS_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getSubPref
|
||||
get() = getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getAudioPref
|
||||
get() = getString(PREF_AUDIO_KEY, PREF_AUDIO_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.useSeriesData
|
||||
get() = getBoolean(PREF_INFO_TYPE, PREF_INFO_DEFAULT)
|
||||
|
||||
private val SharedPreferences.getTrustCert
|
||||
get() = getBoolean(PREF_TRUST_CERT_KEY, PREF_TRUST_CERT_DEFAULT)
|
||||
|
||||
private val SharedPreferences.getSplitCol
|
||||
get() = getBoolean(PREF_SPLIT_COLLECTIONS_KEY, PREF_SPLIT_COLLECTIONS_DEFAULT)
|
||||
|
||||
private val SharedPreferences.sortEp
|
||||
get() = getBoolean(PREF_SORT_EPISODES_KEY, PREF_SORT_EPISODES_DEFAULT)
|
||||
|
||||
private abstract class MediaLibPreference(context: Context) : ListPreference(context) {
|
||||
abstract fun reload()
|
||||
}
|
||||
|
||||
private fun medialibPreference(screen: PreferenceScreen) =
|
||||
object : MediaLibPreference(screen.context) {
|
||||
override fun reload() {
|
||||
this.apply {
|
||||
key = MEDIALIB_KEY
|
||||
title = "Select Media Library"
|
||||
summary = "%s"
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val mediaLibsResponse = client.newCall(
|
||||
GET("$baseUrl/Users/$userId/Items?api_key=$apiKey"),
|
||||
).execute()
|
||||
val mediaJson = mediaLibsResponse.parseAs<ItemsDto>().items
|
||||
|
||||
val entriesArray = mediaJson.map { it.name }
|
||||
val entriesValueArray = mediaJson.map { it.id }
|
||||
|
||||
entries = entriesArray.toTypedArray()
|
||||
entryValues = entriesValueArray.toTypedArray()
|
||||
} catch (ex: Exception) {
|
||||
entries = emptyArray()
|
||||
entryValues = emptyArray()
|
||||
}
|
||||
}.start()
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
parentId = entry
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply { reload() }
|
||||
|
||||
private fun getSummary(isPassword: Boolean, value: String, placeholder: String) = when {
|
||||
isPassword && value.isNotEmpty() || !isPassword && value.isEmpty() -> placeholder
|
||||
else -> value
|
||||
}
|
||||
|
||||
private fun PreferenceScreen.editTextPreference(key: String, title: String, default: String, value: String, isPassword: Boolean = false, placeholder: String, mediaLibPref: MediaLibPreference): EditTextPreference {
|
||||
return EditTextPreference(context).apply {
|
||||
this.key = key
|
||||
this.title = title
|
||||
summary = getSummary(isPassword, value, placeholder)
|
||||
this.setDefaultValue(default)
|
||||
dialogTitle = title
|
||||
|
||||
setOnBindEditTextListener {
|
||||
it.inputType = if (isPassword) {
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
} else {
|
||||
InputType.TYPE_CLASS_TEXT
|
||||
}
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val newValueString = newValue as String
|
||||
val res = preferences.edit().putString(key, newValueString).commit()
|
||||
summary = getSummary(isPassword, newValueString, placeholder)
|
||||
val loginRes = login(true, context)
|
||||
if (loginRes == true) {
|
||||
mediaLibPref.reload()
|
||||
}
|
||||
res
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
import eu.kanade.tachiyomi.animeextension.all.jellyfin.Jellyfin.Companion.APIKEY_KEY
|
||||
import eu.kanade.tachiyomi.animeextension.all.jellyfin.Jellyfin.Companion.USERID_KEY
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class JellyfinAuthenticator(
|
||||
private val preferences: SharedPreferences,
|
||||
private val baseUrl: String,
|
||||
private val client: OkHttpClient,
|
||||
) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun login(username: String, password: String): Pair<String?, String?> {
|
||||
return runCatching {
|
||||
val authResult = authenticateWithPassword(username, password)
|
||||
val key = authResult.accessToken
|
||||
val userId = authResult.sessionInfo.userId
|
||||
saveLogin(key, userId)
|
||||
Pair(key, userId)
|
||||
}.getOrElse {
|
||||
Log.e(LOG_TAG, it.stackTraceToString())
|
||||
Pair(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticateWithPassword(username: String, password: String): LoginDto {
|
||||
var deviceId = getPrefDeviceId()
|
||||
if (deviceId.isNullOrEmpty()) {
|
||||
deviceId = getRandomString()
|
||||
setPrefDeviceId(deviceId)
|
||||
}
|
||||
val aniyomiVersion = AppInfo.getVersionName()
|
||||
val androidVersion = Build.VERSION.RELEASE
|
||||
val authHeader = Headers.headersOf(
|
||||
"X-Emby-Authorization",
|
||||
"MediaBrowser Client=\"$CLIENT\", Device=\"Android $androidVersion\", DeviceId=\"$deviceId\", Version=\"$aniyomiVersion\"",
|
||||
)
|
||||
val body = json.encodeToString(
|
||||
buildJsonObject {
|
||||
put("Username", username)
|
||||
put("Pw", password)
|
||||
},
|
||||
).toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val request = POST("$baseUrl/Users/authenticatebyname", headers = authHeader, body = body)
|
||||
return client.newCall(request).execute().parseAs()
|
||||
}
|
||||
|
||||
private fun getRandomString(): String {
|
||||
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||
return (1..172)
|
||||
.map { allowedChars.random() }
|
||||
.joinToString("")
|
||||
}
|
||||
|
||||
private fun saveLogin(key: String, userId: String) {
|
||||
preferences.edit()
|
||||
.putString(APIKEY_KEY, key)
|
||||
.putString(USERID_KEY, userId)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun getPrefDeviceId(): String? = preferences.getString(
|
||||
DEVICEID_KEY,
|
||||
null,
|
||||
)
|
||||
|
||||
private fun setPrefDeviceId(value: String) = preferences.edit().putString(
|
||||
DEVICEID_KEY,
|
||||
value,
|
||||
).apply()
|
||||
|
||||
companion object {
|
||||
private const val DEVICEID_KEY = "device_id"
|
||||
private const val CLIENT = "Aniyomi"
|
||||
private const val LOG_TAG = "JellyfinAuthenticator"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||
|
||||
object JellyfinConstants {
|
||||
val QUALITIES_LIST = arrayOf(
|
||||
Quality(480, 360, 292000, 128000, "360p - 420 kbps"),
|
||||
Quality(854, 480, 528000, 192000, "480p - 720 kbps"),
|
||||
Quality(854, 480, 1308000, 192000, "480p - 1.5 Mbps"),
|
||||
Quality(854, 480, 2808000, 192000, "480p - 3 Mbps"),
|
||||
Quality(1280, 720, 3808000, 192000, "720p - 4 Mbps"),
|
||||
Quality(1280, 720, 5808000, 192000, "720p - 6 Mbps"),
|
||||
Quality(1280, 720, 7808000, 192000, "720p - 8 Mbps"),
|
||||
Quality(1920, 1080, 9808000, 192000, "1080p - 10 Mbps"),
|
||||
Quality(1920, 1080, 14808000, 192000, "1080p - 15 Mbps"),
|
||||
Quality(1920, 1080, 19808000, 192000, "1080p - 20 Mbps"),
|
||||
Quality(1920, 1080, 39808000, 192000, "1080p - 40 Mbps"),
|
||||
Quality(1920, 1080, 59808000, 192000, "1080p - 60 Mbps"),
|
||||
Quality(3840, 2160, 80000000, 192000, "4K - 80 Mbps"),
|
||||
Quality(3840, 2160, 120000000, 192000, "4K - 120 Mbps"),
|
||||
)
|
||||
|
||||
data class Quality(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val videoBitrate: Int,
|
||||
val audioBitrate: Int,
|
||||
val description: String,
|
||||
)
|
||||
|
||||
val PREF_VALUES = arrayOf(
|
||||
"aar", "abk", "ace", "ach", "ada", "ady", "afh", "afr", "ain", "aka", "akk", "ale", "alt", "amh", "ang", "anp", "apa",
|
||||
"ara", "arc", "arg", "arn", "arp", "arw", "asm", "ast", "ath", "ava", "ave", "awa", "aym", "aze", "bai", "bak", "bal",
|
||||
"bam", "ban", "bas", "bej", "bel", "bem", "ben", "ber", "bho", "bik", "bin", "bis", "bla", "bod", "bos", "bra", "bre",
|
||||
"bua", "bug", "bul", "byn", "cad", "car", "cat", "ceb", "ces", "cha", "chb", "che", "chg", "chk", "chm", "chn", "cho",
|
||||
"chp", "chr", "chu", "chv", "chy", "cnr", "cop", "cor", "cos", "cre", "crh", "csb", "cym", "dak", "dan", "dar", "del",
|
||||
"den", "deu", "dgr", "din", "div", "doi", "dsb", "dua", "dum", "dyu", "dzo", "efi", "egy", "eka", "ell", "elx", "eng",
|
||||
"enm", "epo", "est", "eus", "ewe", "ewo", "fan", "fao", "fas", "fat", "fij", "fil", "fin", "fiu", "fon", "fra", "frm",
|
||||
"fro", "frr", "frs", "fry", "ful", "fur", "gaa", "gay", "gba", "gez", "gil", "gla", "gle", "glg", "glv", "gmh", "goh",
|
||||
"gon", "gor", "got", "grb", "grc", "grn", "gsw", "guj", "gwi", "hai", "hat", "hau", "haw", "heb", "her", "hil", "hin",
|
||||
"hit", "hmn", "hmo", "hrv", "hsb", "hun", "hup", "hye", "iba", "ibo", "ido", "iii", "ijo", "iku", "ile", "ilo", "ina",
|
||||
"inc", "ind", "inh", "ipk", "isl", "ita", "jav", "jbo", "jpn", "jpr", "jrb", "kaa", "kab", "kac", "kal", "kam", "kan",
|
||||
"kar", "kas", "kat", "kau", "kaw", "kaz", "kbd", "kha", "khm", "kho", "kik", "kin", "kir", "kmb", "kok", "kom", "kon",
|
||||
"kor", "kos", "kpe", "krc", "krl", "kru", "kua", "kum", "kur", "kut", "lad", "lah", "lam", "lao", "lat", "lav", "lez",
|
||||
"lim", "lin", "lit", "lol", "loz", "ltz", "lua", "lub", "lug", "lui", "lun", "luo", "lus", "mad", "mag", "mah", "mai",
|
||||
"mak", "mal", "man", "mar", "mas", "mdf", "mdr", "men", "mga", "mic", "min", "mkd", "mkh", "mlg", "mlt", "mnc", "mni",
|
||||
"moh", "mon", "mos", "mri", "msa", "mus", "mwl", "mwr", "mya", "myv", "nah", "nap", "nau", "nav", "nbl", "nde", "ndo",
|
||||
"nds", "nep", "new", "nia", "nic", "niu", "nld", "nno", "nob", "nog", "non", "nor", "nqo", "nso", "nub", "nwc", "nya",
|
||||
"nym", "nyn", "nyo", "nzi", "oci", "oji", "ori", "orm", "osa", "oss", "ota", "oto", "pag", "pal", "pam", "pan", "pap",
|
||||
"pau", "peo", "phn", "pli", "pol", "pon", "por", "pro", "pus", "que", "raj", "rap", "rar", "roh", "rom", "ron", "run",
|
||||
"rup", "rus", "sad", "sag", "sah", "sam", "san", "sas", "sat", "scn", "sco", "sel", "sga", "shn", "sid", "sin", "slk",
|
||||
"slv", "sma", "sme", "smj", "smn", "smo", "sms", "sna", "snd", "snk", "sog", "som", "son", "sot", "spa", "sqi", "srd",
|
||||
"srn", "srp", "srr", "ssw", "suk", "sun", "sus", "sux", "swa", "swe", "syc", "syr", "tah", "tai", "tam", "tat", "tel",
|
||||
"tem", "ter", "tet", "tgk", "tgl", "tha", "tig", "tir", "tiv", "tkl", "tlh", "tli", "tmh", "tog", "ton", "tpi", "tsi",
|
||||
"tsn", "tso", "tuk", "tum", "tup", "tur", "tvl", "twi", "tyv", "udm", "uga", "uig", "ukr", "umb", "urd", "uzb", "vai",
|
||||
"ven", "vie", "vol", "vot", "wal", "war", "was", "wen", "wln", "wol", "xal", "xho", "yao", "yap", "yid", "yor", "zap",
|
||||
"zbl", "zen", "zgh", "zha", "zho", "zul", "zun", "zza",
|
||||
)
|
||||
|
||||
val PREF_ENTRIES = arrayOf(
|
||||
"Qafaraf; ’Afar Af; Afaraf; Qafar af", "Аҧсуа бызшәа Aƥsua bızšwa; Аҧсшәа Aƥsua", "بهسا اچيه", "Lwo", "Dangme",
|
||||
"Адыгабзэ; Кӏахыбзэ", "El-Afrihili", "Afrikaans", "アイヌ・イタㇰ Ainu-itak", "Akan", "𒀝𒅗𒁺𒌑", "Уна́ӈам тунуу́; Унаӈан умсуу",
|
||||
"Алтай тили", "አማርኛ Amârıñâ", "Ænglisc; Anglisc; Englisc", "Angika", "Apache languages", "العَرَبِيَّة al'Arabiyyeẗ",
|
||||
"Official Aramaic (700–300 BCE); Imperial Aramaic (700–300 BCE)", "aragonés", "Mapudungun; Mapuche", "Hinónoʼeitíít",
|
||||
"Lokono", "অসমীয়া", "Asturianu; Llïonés", "Athapascan languages", "Магӏарул мацӏ; Авар мацӏ", "Avestan", "अवधी",
|
||||
"Aymar aru", "Azərbaycan dili; آذربایجان دیلی; Азәрбајҹан дили", "Bamiléké", "Башҡорт теле; Başqort tele",
|
||||
"بلوچی", "ߓߊߡߊߣߊߣߞߊߣ", "ᬪᬵᬱᬩᬮᬶ; ᬩᬲᬩᬮᬶ; Basa Bali", "Mbene; Ɓasaá", "Bidhaawyeet", "Беларуская мова Belaruskaâ mova",
|
||||
"Chibemba", "বাংলা Bāŋlā", "Tamaziɣt; Tamazight; ⵜⴰⵎⴰⵣⵉⵖⵜ; ⵝⴰⵎⴰⵣⵉⵗⵝ; ⵜⴰⵎⴰⵣⵉⵗⵜ", "भोजपुरी", "Bikol", "Ẹ̀dó",
|
||||
"Bislama", "ᓱᖽᐧᖿ", "བོད་སྐད་ Bodskad; ལྷ་སའི་སྐད་ Lhas'iskad", "bosanski", "Braj", "Brezhoneg", "буряад хэлэн",
|
||||
"ᨅᨔ ᨕᨘᨁᨗ", "български език bălgarski ezik", "ብሊና; ብሊን", "Hasí:nay", "Kari'nja", "català,valencià", "Sinugbuanong Binisayâ",
|
||||
"čeština; český jazyk", "Finu' Chamoru", "Muysccubun", "Нохчийн мотт; نَاخچیین موٓتت; ნახჩიე მუოთთ", "جغتای",
|
||||
"Chuukese", "марий йылме", "chinuk wawa; wawa; chinook lelang; lelang", "Chahta'", "ᑌᓀᓱᒼᕄᓀ (Dënesųłiné)",
|
||||
"ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ Tsalagi gawonihisdi", "Славе́нскїй ѧ҆зы́къ", "Чӑвашла", "Tsėhésenėstsestȯtse", "crnogorski / црногорски",
|
||||
"ϯⲙⲉⲑⲣⲉⲙⲛ̀ⲭⲏⲙⲓ; ⲧⲙⲛ̄ⲧⲣⲙ̄ⲛ̄ⲕⲏⲙⲉ", "Kernowek", "Corsu; Lingua corsa", "Cree", "Къырымтатарджа; Къырымтатар тили; Ҡырымтатарҗа; Ҡырымтатар тили",
|
||||
"Kaszëbsczi jãzëk", "Cymraeg; y Gymraeg", "Dakhótiyapi; Dakȟótiyapi", "dansk", "дарган мез", "Delaware", "Dene K'e",
|
||||
"Deutsch", "Dogrib", "Thuɔŋjäŋ", "ދިވެހި; ދިވެހިބަސް Divehi", "𑠖𑠵𑠌𑠤𑠮; डोगरी; ڈوگرى", "Dolnoserbski; Dolnoserbšćina",
|
||||
"Duala", "Dutch, Middle (ca. 1050–1350)", "Julakan", "རྫོང་ཁ་ Ĵoŋkha", "Efik", "Egyptian (Ancient)", "Ekajuk",
|
||||
"Νέα Ελληνικά Néa Ellêniká", "Elamite", "English", "English, Middle (1100–1500)", "Esperanto", "eesti keel",
|
||||
"euskara", "Èʋegbe", "Ewondo", "Fang", "føroyskt", "فارسی Fārsiy", "Mfantse; Fante; Fanti", "Na Vosa Vakaviti",
|
||||
"Wikang Filipino", "suomen kieli", "Finno-Ugrian languages", "Fon gbè", "français", "françois; franceis", "Franceis; François; Romanz",
|
||||
"Frasch; Fresk; Freesk; Friisk", "Oostfreesk; Plattdüütsk", "Frysk", "Fulfulde; Pulaar; Pular", "Furlan",
|
||||
"Gã", "Basa Gayo", "Gbaya", "ግዕዝ", "Taetae ni Kiribati", "Gàidhlig", "Gaeilge", "galego", "Gaelg; Gailck", "Diutsch",
|
||||
"Diutisk", "Gondi", "Bahasa Hulontalo", "Gothic", "Grebo", "Ἑλληνική", "Avañe'ẽ", "Schwiizerdütsch", "ગુજરાતી Gujarātī",
|
||||
"Dinjii Zhu’ Ginjik", "X̱aat Kíl; X̱aadas Kíl; X̱aayda Kil; Xaad kil", "kreyòl ayisyen", "Harshen Hausa; هَرْشَن",
|
||||
"ʻŌlelo Hawaiʻi", "עברית 'Ivriyþ", "Otjiherero", "Ilonggo", "हिन्दी Hindī", "𒉈𒅆𒇷", "lus Hmoob; lug Moob; lol Hmongb; 𖬇𖬰𖬞 𖬌𖬣𖬵",
|
||||
"Hiri Motu", "hrvatski", "hornjoserbšćina", "magyar nyelv", "Na:tinixwe Mixine:whe'", "Հայերէն Hayerèn; Հայերեն Hayeren",
|
||||
"Jaku Iban", "Asụsụ Igbo", "Ido", "ꆈꌠꉙ Nuosuhxop", "Ịjọ", "ᐃᓄᒃᑎᑐᑦ Inuktitut", "Interlingue; Occidental", "Pagsasao nga Ilokano; Ilokano",
|
||||
"Interlingua (International Auxiliary Language Association)", "Indo-Aryan languages", "bahasa Indonesia",
|
||||
"ГӀалгӀай мотт", "Iñupiaq", "íslenska", "italiano; lingua italiana", "ꦧꦱꦗꦮ / Basa Jawa", "la .lojban.", "日本語 Nihongo",
|
||||
"Dzhidi", "عربية يهودية / ערבית יהודית", "Qaraqalpaq tili; Қарақалпақ тили", "Tamaziɣt Taqbaylit; Tazwawt",
|
||||
"Jingpho", "Kalaallisut; Greenlandic", "Kamba", "ಕನ್ನಡ Kannađa", "Karen languages", "कॉशुर / كأشُر", "ქართული Kharthuli",
|
||||
"Kanuri", "ꦧꦱꦗꦮ", "қазақ тілі qazaq tili; қазақша qazaqşa", "Адыгэбзэ (Къэбэрдейбзэ) Adıgăbză (Qăbărdeĭbză)",
|
||||
"কা কতিয়েন খাশি", "ភាសាខ្មែរ Phiəsaakhmær", "Khotanese; Sakan", "Gĩkũyũ", "Ikinyarwanda", "кыргызча kırgızça; кыргыз тили kırgız tili",
|
||||
"Kimbundu", "कोंकणी", "Коми кыв", "Kongo", "한국어 Han'gug'ô", "Kosraean", "Kpɛlɛwoo", "Къарачай-Малкъар тил; Таулу тил",
|
||||
"karjal; kariela; karjala", "कुड़ुख़", "Kuanyama; Kwanyama", "къумукъ тил/qumuq til", "kurdî / کوردی", "Kutenai",
|
||||
"Judeo-español", "بھارت کا", "Lamba", "ພາສາລາວ Phasalaw", "Lingua latīna", "Latviešu valoda", "Лезги чӏал",
|
||||
"Lèmburgs", "Lingala", "lietuvių kalba", "Lomongo", "Lozi", "Lëtzebuergesch", "Cilubà / Tshiluba", "Kiluba",
|
||||
"Luganda", "Cham'teela", "Chilunda", "Dholuo", "Mizo ṭawng", "Madhura", "मगही", "Kajin M̧ajeļ", "मैथिली; মৈথিলী",
|
||||
"Basa Mangkasara' / ᨅᨔ ᨆᨀᨔᨑ", "മലയാളം Malayāļã", "Mandi'nka kango", "मराठी Marāţhī", "ɔl", "мокшень кяль",
|
||||
"Mandar", "Mɛnde yia", "Gaoidhealg", "Míkmawísimk", "Baso Minang", "македонски јазик makedonski jazik", "Mon-Khmer languages",
|
||||
"Malagasy", "Malti", "ᠮᠠᠨᠵᡠ ᡤᡳᠰᡠᠨ Manju gisun", "Manipuri", "Kanien’kéha", "монгол хэл mongol xel; ᠮᠣᠩᠭᠣᠯ ᠬᠡᠯᠡ",
|
||||
"Mooré", "Te Reo Māori", "Bahasa Melayu", "Mvskoke", "mirandés; lhéngua mirandesa", "मारवाड़ी", "မြန်မာစာ Mrãmācā; မြန်မာစကား Mrãmākā:",
|
||||
"эрзянь кель", "Nahuatl languages", "napulitano", "dorerin Naoero", "Diné bizaad; Naabeehó bizaad", "isiNdebele seSewula",
|
||||
"siNdebele saseNyakatho", "ndonga", "Plattdütsch; Plattdüütsch", "नेपाली भाषा Nepālī bhāśā", "नेपाल भाषा; नेवाः भाय्",
|
||||
"Li Niha", "Niger-Kordofanian languages", "ko e vagahau Niuē", "Nederlands; Vlaams", "norsk nynorsk", "norsk bokmål",
|
||||
"Ногай тили", "Dǫnsk tunga; Norrœnt mál", "norsk", "N'Ko", "Sesotho sa Leboa", "لغات نوبية", "पुलां भाय्; पुलाङु नेपाल भाय्",
|
||||
"Chichewa; Chinyanja", "Nyamwezi", "Nyankole", "Runyoro", "Nzima", "occitan; lenga d'òc", "Ojibwa", "ଓଡ଼ିଆ",
|
||||
"Afaan Oromoo", "Wazhazhe ie / 𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟", "Ирон ӕвзаг Iron ævzag", "لسان عثمانى / lisân-ı Osmânî", "Otomian languages",
|
||||
"Salitan Pangasinan", "Pārsīk; Pārsīg", "Amánung Kapampangan; Amánung Sísuan", "ਪੰਜਾਬੀ / پنجابی Pãjābī",
|
||||
"Papiamentu", "a tekoi er a Belau", "Persian, Old (ca. 600–400 B.C.)", "𐤃𐤁𐤓𐤉𐤌 𐤊𐤍𐤏𐤍𐤉𐤌 Dabariym Kana'aniym",
|
||||
"Pāli", "Język polski", "Pohnpeian", "português", "Provençal, Old (to 1500); Old Occitan (to 1500)", "پښتو Pax̌tow",
|
||||
"Runa simi; kichwa simi; Nuna shimi", "राजस्थानी", "Vananga rapa nui", "Māori Kūki 'Āirani", "Rumantsch; Rumàntsch; Romauntsch; Romontsch",
|
||||
"romani čhib", "limba română", "Ikirundi", "armãneashce; armãneashti; rrãmãneshti", "русский язык russkiĭ âzık",
|
||||
"Sandaweeki", "yângâ tî sängö", "Сахалыы", "ארמית", "संस्कृतम् Sąskŕtam; 𑌸𑌂𑌸𑍍𑌕𑍃𑌤𑌮𑍍", "Sasak", "ᱥᱟᱱᱛᱟᱲᱤ", "Sicilianu",
|
||||
"Braid Scots; Lallans", "Selkup", "Goídelc", "ၵႂၢမ်းတႆးယႂ်", "Sidaamu Afoo", "සිංහල Sĩhala", "slovenčina; slovenský jazyk",
|
||||
"slovenski jezik; slovenščina", "Åarjelsaemien gïele", "davvisámegiella", "julevsámegiella", "anarâškielâ",
|
||||
"Gagana faʻa Sāmoa", "sääʹmǩiõll", "chiShona", "سنڌي / सिन्धी / ਸਿੰਧੀ", "Sooninkanxanne", "Sogdian", "af Soomaali",
|
||||
"Songhai languages", "Sesotho [southern]", "español; castellano", "Shqip", "sardu; limba sarda; lingua sarda",
|
||||
"Sranan Tongo", "српски / srpski", "Seereer", "siSwati", "Kɪsukuma", "ᮘᮞ ᮞᮥᮔ᮪ᮓ / Basa Sunda", "Sosoxui", "𒅴𒂠",
|
||||
"Kiswahili", "svenska", "Classical Syriac", "ܠܫܢܐ ܣܘܪܝܝܐ Lešānā Suryāyā", "Reo Tahiti; Reo Mā'ohi", "ภาษาไท; ภาษาไต",
|
||||
"தமிழ் Tamił", "татар теле / tatar tele / تاتار", "తెలుగు Telugu", "KʌThemnɛ", "Terêna", "Lia-Tetun", "тоҷикӣ toçikī",
|
||||
"Wikang Tagalog", "ภาษาไทย Phasathay", "ትግረ; ትግሬ; ኻሳ; ትግራይት", "ትግርኛ", "Tiv", "Tokelau", "Klingon; tlhIngan-Hol",
|
||||
"Lingít", "Tamashek", "chiTonga", "lea faka-Tonga", "Tok Pisin", "Tsimshian", "Setswana", "Xitsonga", "Türkmençe / Түркменче / تورکمن تیلی تورکمنچ; türkmen dili / түркмен дили",
|
||||
"chiTumbuka", "Tupi languages", "Türkçe", "Te Ggana Tuuvalu; Te Gagana Tuuvalu", "Twi", "тыва дыл", "удмурт кыл",
|
||||
"Ugaritic", "ئۇيغۇرچە ; ئۇيغۇر تىلى", "Українська мова; Українська", "Úmbúndú", "اُردُو Urduw", "Oʻzbekcha / Ózbekça / ўзбекча / ئوزبېچه; oʻzbek tili / ўзбек тили / ئوبېک تیلی",
|
||||
"ꕙꔤ", "Tshivenḓa", "Tiếng Việt", "Volapük", "vađđa ceeli", "Wolaitta; Wolaytta", "Winaray; Samareño; Lineyte-Samarnon; Binisayâ nga Winaray; Binisayâ nga Samar-Leyte; “Binisayâ nga Waray”",
|
||||
"wá:šiw ʔítlu", "Serbsce / Serbski", "Walon", "Wolof", "Хальмг келн / Xaľmg keln", "isiXhosa", "Yao", "Yapese",
|
||||
"ייִדיש; יידיש; אידיש Yidiš", "èdè Yorùbá", "Diidxazá/Dizhsa", "Blissymbols; Blissymbolics; Bliss", "Tuḍḍungiyya",
|
||||
"ⵜⴰⵎⴰⵣⵉⵖⵜ ⵜⴰⵏⴰⵡⴰⵢⵜ", "Vahcuengh / 話僮", "中文 Zhōngwén; 汉语; 漢語 Hànyǔ", "isiZulu", "Shiwi'ma", "kirmanckî; dimilkî; kirdkî; zazakî",
|
||||
)
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.all.jellyfin.Jellyfin.EpisodeType
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class LoginDto(
|
||||
@SerialName("AccessToken") val accessToken: String,
|
||||
@SerialName("SessionInfo") val sessionInfo: LoginSessionDto,
|
||||
) {
|
||||
@Serializable
|
||||
data class LoginSessionDto(
|
||||
@SerialName("UserId") val userId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ItemsDto(
|
||||
@SerialName("Items") val items: List<ItemDto>,
|
||||
@SerialName("TotalRecordCount") val itemCount: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ItemDto(
|
||||
@SerialName("Name") val name: String,
|
||||
@SerialName("Type") val type: String,
|
||||
@SerialName("Id") val id: String,
|
||||
@SerialName("LocationType") val locationType: String,
|
||||
@SerialName("ImageTags") val imageTags: ImageDto,
|
||||
@SerialName("SeriesId") val seriesId: String? = null,
|
||||
@SerialName("SeriesName") val seriesName: String? = null,
|
||||
|
||||
// Details
|
||||
@SerialName("Overview") val overview: String? = null,
|
||||
@SerialName("Genres") val genres: List<String>? = null,
|
||||
@SerialName("Studios") val studios: List<StudioDto>? = null,
|
||||
|
||||
// Only for series, not season
|
||||
@SerialName("Status") val seriesStatus: String? = null,
|
||||
@SerialName("SeasonName") val seasonName: String? = null,
|
||||
|
||||
// Episode
|
||||
@SerialName("PremiereDate") val premiereData: String? = null,
|
||||
@SerialName("RunTimeTicks") val runTime: Long? = null,
|
||||
@SerialName("MediaSources") val mediaSources: List<MediaDto>? = null,
|
||||
@SerialName("IndexNumber") val indexNumber: Int? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class ImageDto(
|
||||
@SerialName("Primary") val primary: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StudioDto(
|
||||
@SerialName("Name") val name: String,
|
||||
)
|
||||
|
||||
fun toSAnime(baseUrl: String, userId: String, apiKey: String): SAnime = SAnime.create().apply {
|
||||
val httpUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("Users")
|
||||
addPathSegment(userId)
|
||||
addPathSegment("Items")
|
||||
addPathSegment(id)
|
||||
addQueryParameter("api_key", apiKey)
|
||||
}
|
||||
|
||||
thumbnail_url = "$baseUrl/Items/$id/Images/Primary?api_key=$apiKey"
|
||||
|
||||
when (type) {
|
||||
"Season" -> {
|
||||
// To prevent one extra GET request when fetching episodes
|
||||
httpUrl.fragment("seriesId,${seriesId!!}")
|
||||
|
||||
if (locationType == "Virtual") {
|
||||
title = seriesName!!
|
||||
thumbnail_url = "$baseUrl/Items/$seriesId/Images/Primary?api_key=$apiKey"
|
||||
} else {
|
||||
title = "$seriesName $name"
|
||||
}
|
||||
|
||||
// Use series as fallback
|
||||
if (imageTags.primary == null) {
|
||||
thumbnail_url = "$baseUrl/Items/$seriesId/Images/Primary?api_key=$apiKey"
|
||||
}
|
||||
}
|
||||
"Movie" -> {
|
||||
httpUrl.fragment("movie")
|
||||
title = name
|
||||
}
|
||||
"BoxSet" -> {
|
||||
httpUrl.fragment("boxSet")
|
||||
title = name
|
||||
}
|
||||
"Series" -> {
|
||||
httpUrl.fragment("series")
|
||||
title = name
|
||||
}
|
||||
}
|
||||
|
||||
url = httpUrl.build().toString()
|
||||
|
||||
// Details
|
||||
description = overview?.let {
|
||||
Jsoup.parseBodyFragment(
|
||||
it.replace("<br>", "br2n"),
|
||||
).text().replace("br2n", "\n")
|
||||
}
|
||||
genre = genres?.joinToString(", ")
|
||||
author = studios?.joinToString(", ") { it.name }
|
||||
|
||||
if (type == "Movie") {
|
||||
status = SAnime.COMPLETED
|
||||
} else {
|
||||
status = seriesStatus.parseStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.parseStatus(): Int = when (this) {
|
||||
"Ended" -> SAnime.COMPLETED
|
||||
"Continuing" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
fun toSEpisode(
|
||||
baseUrl: String,
|
||||
userId: String,
|
||||
apiKey: String,
|
||||
epDetails: Set<String>,
|
||||
epType: EpisodeType,
|
||||
prefix: String,
|
||||
): SEpisode = SEpisode.create().apply {
|
||||
when (epType) {
|
||||
EpisodeType.MOVIE -> {
|
||||
episode_number = 1F
|
||||
name = "${prefix}Movie"
|
||||
}
|
||||
EpisodeType.EPISODE -> {
|
||||
episode_number = indexNumber?.toFloat() ?: 1F
|
||||
name = "${prefix}Ep. $indexNumber - ${this@ItemDto.name}"
|
||||
}
|
||||
}
|
||||
|
||||
val extraInfo = buildList {
|
||||
if (epDetails.contains("Overview") && overview != null && epType == EpisodeType.EPISODE) {
|
||||
add(overview)
|
||||
}
|
||||
|
||||
if (epDetails.contains("Size") && mediaSources != null) {
|
||||
mediaSources.first().size?.also {
|
||||
add(it.formatBytes())
|
||||
}
|
||||
}
|
||||
|
||||
if (epDetails.contains("Runtime") && runTime != null) {
|
||||
add(runTime.formatTicks())
|
||||
}
|
||||
}
|
||||
|
||||
scanlator = extraInfo.joinToString(" • ")
|
||||
premiereData?.also {
|
||||
date_upload = parseDate(it.removeSuffix("Z"))
|
||||
}
|
||||
url = "$baseUrl/Users/$userId/Items/$id?api_key=$apiKey"
|
||||
}
|
||||
|
||||
private fun Long.formatBytes(): String = when {
|
||||
this >= 1_000_000_000 -> "%.2f GB".format(this / 1_000_000_000.0)
|
||||
this >= 1_000_000 -> "%.2f MB".format(this / 1_000_000.0)
|
||||
this >= 1_000 -> "%.2f KB".format(this / 1_000.0)
|
||||
this > 1 -> "$this bytes"
|
||||
this == 1L -> "$this byte"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
private fun Long.formatTicks(): String {
|
||||
val seconds = this / 10_000_000
|
||||
val minutes = seconds / 60
|
||||
val hours = minutes / 60
|
||||
|
||||
val remainingSeconds = seconds % 60
|
||||
val remainingMinutes = minutes % 60
|
||||
|
||||
val formattedHours = if (hours > 0) "${hours}h " else ""
|
||||
val formattedMinutes = if (remainingMinutes > 0) "${remainingMinutes}m " else ""
|
||||
val formattedSeconds = "${remainingSeconds}s"
|
||||
|
||||
return "$formattedHours$formattedMinutes$formattedSeconds".trim()
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SessionDto(
|
||||
@SerialName("MediaSources") val mediaSources: List<MediaDto>,
|
||||
@SerialName("PlaySessionId") val playSessionId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaDto(
|
||||
@SerialName("Size") val size: Long? = null,
|
||||
@SerialName("MediaStreams") val mediaStreams: List<MediaStreamDto>,
|
||||
) {
|
||||
@Serializable
|
||||
data class MediaStreamDto(
|
||||
@SerialName("Codec") val codec: String,
|
||||
@SerialName("Index") val index: Int,
|
||||
@SerialName("Type") val type: String,
|
||||
@SerialName("SupportsExternalStream") val supportsExternalStream: Boolean,
|
||||
@SerialName("IsExternal") val isExternal: Boolean,
|
||||
@SerialName("Language") val lang: String? = null,
|
||||
@SerialName("DisplayTitle") val displayTitle: String? = null,
|
||||
@SerialName("Height") val height: Int? = null,
|
||||
@SerialName("Width") val width: Int? = null,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||
|
||||
class JellyfinFactory : AnimeSourceFactory {
|
||||
override fun createSources(): List<AnimeSource> {
|
||||
val firstJelly = Jellyfin("1")
|
||||
val extraCount = firstJelly.preferences.getString(Jellyfin.EXTRA_SOURCES_COUNT_KEY, Jellyfin.EXTRA_SOURCES_COUNT_DEFAULT)!!.toInt()
|
||||
|
||||
return buildList(extraCount) {
|
||||
add(firstJelly)
|
||||
for (i in 2..extraCount) {
|
||||
add(Jellyfin("$i"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue