fix(all/jellyfin): Fix subtitles, pages, and some small code refactoring (#2103)

Co-authored-by: jmir1 <jhmiramon@gmail.com>
This commit is contained in:
Secozzi
2023-08-30 14:27:58 +00:00
committed by GitHub
parent 3e800631e5
commit b687d0e3aa
3 changed files with 145 additions and 139 deletions

View File

@ -5,8 +5,8 @@ apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Jellyfin' extName = 'Jellyfin'
pkgNameSuffix = 'all.jellyfin' pkgNameSuffix = 'all.jellyfin'
extClass = '.Jellyfin' extClass = '.JellyfinFactory'
extVersionCode = 8 extVersionCode = 9
libVersion = '13' libVersion = '13'
} }

View File

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.network.asObservableSuccess
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Dns import okhttp3.Dns
@ -38,9 +37,9 @@ import uy.kohesive.injekt.injectLazy
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.floor import kotlin.math.floor
class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() { class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Jellyfin" override val name = "Jellyfin$suffix"
override val lang = "all" override val lang = "all"
@ -108,23 +107,21 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
} }
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request {
if (parentId.isEmpty()) { require(parentId.isNotEmpty()) { "Select library in the extension settings." }
throw Exception("Select library in the extension settings.")
}
val startIndex = (page - 1) * 20 val startIndex = (page - 1) * 20
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder() val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder().apply {
addQueryParameter("api_key", apiKey)
url.addQueryParameter("api_key", apiKey) addQueryParameter("StartIndex", startIndex.toString())
url.addQueryParameter("StartIndex", startIndex.toString()) addQueryParameter("Limit", "20")
url.addQueryParameter("Limit", "20") addQueryParameter("Recursive", "true")
url.addQueryParameter("Recursive", "true") addQueryParameter("SortBy", "SortName")
url.addQueryParameter("SortBy", "SortName") addQueryParameter("SortOrder", "Ascending")
url.addQueryParameter("SortOrder", "Ascending") addQueryParameter("includeItemTypes", "Movie,Season,BoxSet")
url.addQueryParameter("includeItemTypes", "Movie,Series,Season,BoxSet") addQueryParameter("ImageTypeLimit", "1")
url.addQueryParameter("ImageTypeLimit", "1") addQueryParameter("ParentId", parentId)
url.addQueryParameter("ParentId", parentId) addQueryParameter("EnableImageTypes", "Primary")
url.addQueryParameter("EnableImageTypes", "Primary") }
return GET(url.toString()) return GET(url.toString())
} }
@ -150,24 +147,21 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
} }
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
if (parentId.isEmpty()) { require(parentId.isNotEmpty()) { "Select library in the extension settings." }
throw Exception("Select library in the extension settings.")
}
val startIndex = (page - 1) * 20 val startIndex = (page - 1) * 20
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder() val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder().apply {
addQueryParameter("api_key", apiKey)
url.addQueryParameter("api_key", apiKey) addQueryParameter("StartIndex", startIndex.toString())
url.addQueryParameter("StartIndex", startIndex.toString()) addQueryParameter("Limit", "20")
url.addQueryParameter("Limit", "20") addQueryParameter("Recursive", "true")
url.addQueryParameter("Recursive", "true") addQueryParameter("SortBy", "DateCreated,SortName")
url.addQueryParameter("SortBy", "DateCreated,SortName") addQueryParameter("SortOrder", "Descending")
url.addQueryParameter("SortOrder", "Descending") addQueryParameter("includeItemTypes", "Movie,Season,BoxSet")
url.addQueryParameter("includeItemTypes", "Movie,Series,Season,BoxSet") addQueryParameter("ImageTypeLimit", "1")
url.addQueryParameter("ImageTypeLimit", "1") addQueryParameter("ParentId", parentId)
url.addQueryParameter("ParentId", parentId) addQueryParameter("EnableImageTypes", "Primary")
url.addQueryParameter("EnableImageTypes", "Primary") }
return GET(url.toString()) return GET(url.toString())
} }
@ -181,51 +175,45 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
override fun searchAnimeParse(response: Response) = throw Exception("Not used") override fun searchAnimeParse(response: Response) = throw Exception("Not used")
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> { override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
if (parentId.isEmpty()) { require(parentId.isNotEmpty()) { "Select library in the extension settings." }
throw Exception("Select library in the extension settings.")
}
val animeList = mutableListOf<SAnime>()
val startIndex = (page - 1) * 5 val startIndex = (page - 1) * 5
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder() val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder().apply {
addQueryParameter("api_key", apiKey)
addQueryParameter("StartIndex", startIndex.toString())
addQueryParameter("Limit", "5")
addQueryParameter("Recursive", "true")
addQueryParameter("SortBy", "SortName")
addQueryParameter("SortOrder", "Ascending")
addQueryParameter("includeItemTypes", "Movie,Season,BoxSet")
addQueryParameter("ImageTypeLimit", "1")
addQueryParameter("EnableImageTypes", "Primary")
addQueryParameter("ParentId", parentId)
addQueryParameter("SearchTerm", query)
}
url.addQueryParameter("api_key", apiKey) val items = client.newCall(
url.addQueryParameter("StartIndex", startIndex.toString())
url.addQueryParameter("Limit", "5")
url.addQueryParameter("Recursive", "true")
url.addQueryParameter("SortBy", "SortName")
url.addQueryParameter("SortOrder", "Ascending")
url.addQueryParameter("includeItemTypes", "Movie,Series,BoxSet")
url.addQueryParameter("ImageTypeLimit", "1")
url.addQueryParameter("EnableImageTypes", "Primary")
url.addQueryParameter("ParentId", parentId)
url.addQueryParameter("SearchTerm", query)
val response = client.newCall(
GET(url.build().toString(), headers = headers), GET(url.build().toString(), headers = headers),
).execute() ).execute().parseAs<ItemsResponse>()
val items = json.decodeFromString<ItemsResponse>(response.body.string())
items.Items.forEach { val animeList = items.Items.flatMap {
animeList.addAll( getAnimeFromId(it.Id)
getAnimeFromId(it.Id),
)
} }
return Observable.just(AnimesPage(animeList, 5 * page < items.TotalRecordCount)) return Observable.just(AnimesPage(animeList, 5 * page < items.TotalRecordCount))
} }
private fun getAnimeFromId(id: String): List<SAnime> { private fun getAnimeFromId(id: String): List<SAnime> {
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder() val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder().apply {
addQueryParameter("api_key", apiKey)
url.addQueryParameter("api_key", apiKey) addQueryParameter("Recursive", "true")
url.addQueryParameter("Recursive", "true") addQueryParameter("SortBy", "SortName")
url.addQueryParameter("SortBy", "SortName") addQueryParameter("SortOrder", "Ascending")
url.addQueryParameter("SortOrder", "Ascending") addQueryParameter("includeItemTypes", "Movie,Series,Season")
url.addQueryParameter("includeItemTypes", "Movie,Series,Season") addQueryParameter("ImageTypeLimit", "1")
url.addQueryParameter("ImageTypeLimit", "1") addQueryParameter("EnableImageTypes", "Primary")
url.addQueryParameter("EnableImageTypes", "Primary") addQueryParameter("ParentId", id)
url.addQueryParameter("ParentId", id) }
val response = client.newCall( val response = client.newCall(
GET(url.build().toString()), GET(url.build().toString()),
@ -244,22 +232,22 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
mediaId.seasonId mediaId.seasonId
} }
val url = "$baseUrl/Users/$userId/Items/$infoId".toHttpUrl().newBuilder() val url = "$baseUrl/Users/$userId/Items/$infoId".toHttpUrl().newBuilder().apply {
addQueryParameter("api_key", apiKey)
url.addQueryParameter("api_key", apiKey) addQueryParameter("fields", "Studios")
url.addQueryParameter("fields", "Studios") }
return GET(url.toString()) return GET(url.toString())
} }
override fun animeDetailsParse(response: Response): SAnime { override fun animeDetailsParse(response: Response): SAnime {
val info = json.decodeFromString<ItemsResponse.Item>(response.body.string()) val info = response.parseAs<ItemsResponse.Item>()
val anime = SAnime.create() val anime = SAnime.create()
if (info.Genres != null) anime.genre = info.Genres.joinToString(", ") if (info.Genres != null) anime.genre = info.Genres.joinToString(", ")
if (info.Studios != null && info.Studios.isNotEmpty()) { if (!info.Studios.isNullOrEmpty()) {
anime.author = info.Studios.mapNotNull { it.Name }.joinToString(", ") anime.author = info.Studios.mapNotNull { it.Name }.joinToString(", ")
} else if (info.SeriesStudio != null) anime.author = info.SeriesStudio } else if (info.SeriesStudio != null) anime.author = info.SeriesStudio
@ -297,13 +285,15 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val episodeList = if (response.request.url.toString().startsWith("$baseUrl/Users/")) { val episodeList = if (response.request.url.toString().startsWith("$baseUrl/Users/")) {
val parsed = json.decodeFromString<ItemsResponse.Item>(response.body.string()) val parsed = json.decodeFromString<ItemsResponse.Item>(response.body.string())
val episode = SEpisode.create() listOf(
episode.episode_number = 1.0F SEpisode.create().apply {
episode.name = "Movie ${parsed.Name}" setUrlWithoutDomain(response.request.url.toString())
episode.setUrlWithoutDomain(response.request.url.toString().substringAfter(baseUrl)) name = "Movie ${parsed.Name}"
listOf(episode) episode_number = 1.0F
},
)
} else { } else {
val parsed = json.decodeFromString<ItemsResponse>(response.body.string()) val parsed = response.parseAs<ItemsResponse>()
parsed.Items.map { ep -> parsed.Items.map { ep ->
@ -333,14 +323,14 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
val id = json.decodeFromString<ItemsResponse.Item>(response.body.string()).Id val id = response.parseAs<ItemsResponse.Item>().Id
val sessionResponse = client.newCall( val parsed = client.newCall(
GET("$baseUrl/Items/$id/PlaybackInfo?userId=$userId&api_key=$apiKey"), GET("$baseUrl/Items/$id/PlaybackInfo?userId=$userId&api_key=$apiKey"),
).execute() ).execute().parseAs<SessionResponse>()
val parsed = json.decodeFromString<SessionResponse>(sessionResponse.body.string())
val subtitleList = mutableListOf<Track>() val subtitleList = mutableListOf<Track>()
val externalSubtitleList = mutableListOf<Track>()
val prefSub = preferences.getString(JFConstants.PREF_SUB_KEY, "eng")!! val prefSub = preferences.getString(JFConstants.PREF_SUB_KEY, "eng")!!
val prefAudio = preferences.getString(JFConstants.PREF_AUDIO_KEY, "jpn")!! val prefAudio = preferences.getString(JFConstants.PREF_AUDIO_KEY, "jpn")!!
@ -359,20 +349,23 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
if (media.Language == prefSub) { if (media.Language == prefSub) {
try { try {
if (media.IsExternal) { if (media.IsExternal) {
subtitleList.add(0, Track(subUrl, media.DisplayTitle!!)) externalSubtitleList.add(0, Track(subUrl, media.DisplayTitle!!))
} }
subtitleList.add(0, Track(subUrl, media.DisplayTitle!!))
} catch (e: Error) { } catch (e: Error) {
subIndex = media.Index subIndex = media.Index
} }
} else { } else {
if (media.IsExternal) { if (media.IsExternal) {
subtitleList.add(Track(subUrl, media.DisplayTitle!!)) externalSubtitleList.add(Track(subUrl, media.DisplayTitle!!))
} }
subtitleList.add(Track(subUrl, media.DisplayTitle!!))
} }
} else { } else {
if (media.IsExternal) { if (media.IsExternal) {
subtitleList.add(Track(subUrl, media.DisplayTitle!!)) externalSubtitleList.add(Track(subUrl, media.DisplayTitle!!))
} }
subtitleList.add(Track(subUrl, media.DisplayTitle!!))
} }
} else { } else {
if (media.Language != null && media.Language == prefSub) { if (media.Language != null && media.Language == prefSub) {
@ -396,44 +389,44 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
JFConstants.QUALITIES_LIST.forEach { quality -> JFConstants.QUALITIES_LIST.forEach { quality ->
if (width < quality.width && height < quality.height) { if (width < quality.width && height < quality.height) {
val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey" val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey"
videoList.add(Video(url, "Best", url, subtitleTracks = subtitleList)) videoList.add(Video(url, "Source", url, subtitleTracks = externalSubtitleList))
return videoList.reversed() return videoList.reversed()
} else { } else {
val url = "$baseUrl/videos/$id/main.m3u8".toHttpUrl().newBuilder() val url = "$baseUrl/videos/$id/main.m3u8".toHttpUrl().newBuilder().apply {
addQueryParameter("api_key", apiKey)
url.addQueryParameter("api_key", apiKey) addQueryParameter("VideoCodec", "h264")
url.addQueryParameter("VideoCodec", "h264") addQueryParameter("AudioCodec", "aac,mp3")
url.addQueryParameter("AudioCodec", "aac,mp3") addQueryParameter("AudioStreamIndex", audioIndex.toString())
url.addQueryParameter("AudioStreamIndex", audioIndex.toString()) subIndex?.let { addQueryParameter("SubtitleStreamIndex", it.toString()) }
subIndex?.let { url.addQueryParameter("SubtitleStreamIndex", it.toString()) } addQueryParameter("VideoCodec", "h264")
url.addQueryParameter("VideoCodec", "h264") addQueryParameter("VideoCodec", "h264")
url.addQueryParameter("VideoCodec", "h264") addQueryParameter(
url.addQueryParameter(
"VideoBitrate", "VideoBitrate",
quality.videoBitrate.toString(), quality.videoBitrate.toString(),
) )
url.addQueryParameter( addQueryParameter(
"AudioBitrate", "AudioBitrate",
quality.audioBitrate.toString(), quality.audioBitrate.toString(),
) )
url.addQueryParameter("PlaySessionId", parsed.PlaySessionId) addQueryParameter("PlaySessionId", parsed.PlaySessionId)
url.addQueryParameter("TranscodingMaxAudioChannels", "6") addQueryParameter("TranscodingMaxAudioChannels", "6")
url.addQueryParameter("RequireAvc", "false") addQueryParameter("RequireAvc", "false")
url.addQueryParameter("SegmentContainer", "ts") addQueryParameter("SegmentContainer", "ts")
url.addQueryParameter("MinSegments", "1") addQueryParameter("MinSegments", "1")
url.addQueryParameter("BreakOnNonKeyFrames", "true") addQueryParameter("BreakOnNonKeyFrames", "true")
url.addQueryParameter("h264-profile", "high,main,baseline,constrainedbaseline") addQueryParameter("h264-profile", "high,main,baseline,constrainedbaseline")
url.addQueryParameter("h264-level", "51") addQueryParameter("h264-level", "51")
url.addQueryParameter("h264-deinterlace", "true") addQueryParameter("h264-deinterlace", "true")
url.addQueryParameter("TranscodeReasons", "VideoCodecNotSupported,AudioCodecNotSupported,ContainerBitrateExceedsLimit") addQueryParameter("TranscodeReasons", "VideoCodecNotSupported,AudioCodecNotSupported,ContainerBitrateExceedsLimit")
}
videoList.add(Video(url.toString(), quality.description, url.toString(), subtitleTracks = subtitleList)) videoList.add(Video(url.toString(), quality.description, url.toString(), subtitleTracks = subtitleList))
} }
} }
val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey" val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey"
videoList.add(Video(url, "Best", url)) videoList.add(Video(url, "Source", url, subtitleTracks = externalSubtitleList))
return videoList.reversed() return videoList.reversed()
} }
@ -441,10 +434,9 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun animeParse(response: Response, page: Int): AnimesPage { private fun animeParse(response: Response, page: Int): AnimesPage {
val items = json.decodeFromString<ItemsResponse>(response.body.string()) val items = response.parseAs<ItemsResponse>()
val animesList = mutableListOf<SAnime>()
items.Items.forEach { item -> val animeList = items.Items.flatMap { item ->
val anime = SAnime.create() val anime = SAnime.create()
when (item.Type) { when (item.Type) {
@ -469,7 +461,7 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
if (item.ImageTags.Primary == null) { if (item.ImageTags.Primary == null) {
anime.thumbnail_url = "$baseUrl/Items/${item.SeriesId}/Images/Primary?api_key=$apiKey" anime.thumbnail_url = "$baseUrl/Items/${item.SeriesId}/Images/Primary?api_key=$apiKey"
} }
animesList.add(anime) listOf(anime)
} }
"Movie" -> { "Movie" -> {
anime.title = item.Name anime.title = item.Name
@ -481,38 +473,41 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
item.Id, item.Id,
).toJsonString(), ).toJsonString(),
) )
animesList.add(anime) listOf(anime)
} }
"BoxSet" -> { "BoxSet" -> {
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder() val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder().apply {
addQueryParameter("api_key", apiKey)
url.addQueryParameter("api_key", apiKey) addQueryParameter("Recursive", "true")
url.addQueryParameter("Recursive", "true") addQueryParameter("SortBy", "SortName")
url.addQueryParameter("SortBy", "SortName") addQueryParameter("SortOrder", "Ascending")
url.addQueryParameter("SortOrder", "Ascending") addQueryParameter("includeItemTypes", "Movie,Series,Season")
url.addQueryParameter("includeItemTypes", "Movie,Series,Season") addQueryParameter("ImageTypeLimit", "1")
url.addQueryParameter("ImageTypeLimit", "1") addQueryParameter("ParentId", item.Id)
url.addQueryParameter("ParentId", item.Id) addQueryParameter("EnableImageTypes", "Primary")
url.addQueryParameter("EnableImageTypes", "Primary") }
val response = client.newCall( val response = client.newCall(
GET(url.build().toString(), headers = headers), GET(url.build().toString(), headers = headers),
).execute() ).execute()
animesList.addAll(animeParse(response, page).animes) animeParse(response, page).animes
}
else -> {
return@forEach
} }
else -> emptyList()
} }
} }
return AnimesPage(animesList, 20 * page < items.TotalRecordCount) return AnimesPage(animeList, 20 * page < items.TotalRecordCount)
} }
private fun LinkData.toJsonString(): String { private fun LinkData.toJsonString(): String {
return json.encodeToString(this) return json.encodeToString(this)
} }
private inline fun <reified T> Response.parseAs(transform: (String) -> String = { it }): T {
val responseBody = use { transform(it.body.string()) }
return json.decodeFromString(responseBody)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val mediaLibPref = medialibPreference(screen) val mediaLibPref = medialibPreference(screen)
screen.addPreference( screen.addPreference(

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.animeextension.all.jellyfin
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
class JellyfinFactory : AnimeSourceFactory {
override fun createSources() = listOf(
Jellyfin(""),
Jellyfin(" (2)"),
Jellyfin(" (3)"),
)
}