feat(all/jellyfin): Add support for collections (#3217)

This commit is contained in:
Secozzi 2024-05-06 09:10:29 +00:00 committed by GitHub
parent 26e09c144f
commit a59119a6c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 120 additions and 15 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Jellyfin' extName = 'Jellyfin'
extClass = '.JellyfinFactory' extClass = '.JellyfinFactory'
extVersionCode = 13 extVersionCode = 14
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -142,7 +142,7 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
addQueryParameter("Recursive", "true") addQueryParameter("Recursive", "true")
addQueryParameter("SortBy", "SortName") addQueryParameter("SortBy", "SortName")
addQueryParameter("SortOrder", "Ascending") addQueryParameter("SortOrder", "Ascending")
addQueryParameter("IncludeItemTypes", "Movie,Season") addQueryParameter("IncludeItemTypes", "Movie,Season,BoxSet")
addQueryParameter("ImageTypeLimit", "1") addQueryParameter("ImageTypeLimit", "1")
addQueryParameter("ParentId", parentId) addQueryParameter("ParentId", parentId)
addQueryParameter("EnableImageTypes", "Primary") addQueryParameter("EnableImageTypes", "Primary")
@ -152,9 +152,22 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
} }
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val splitCollections = preferences.getSplitCol
val page = response.request.url.queryParameter("StartIndex")!!.toInt() / SEASONS_LIMIT + 1 val page = response.request.url.queryParameter("StartIndex")!!.toInt() / SEASONS_LIMIT + 1
val data = response.parseAs<ItemsDto>() val data = response.parseAs<ItemsDto>()
val animeList = data.items.map { it.toSAnime(baseUrl, userId!!, apiKey!!) } 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) return AnimesPage(animeList, SEASONS_LIMIT * page < data.itemCount)
} }
@ -258,6 +271,26 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
}.build() }.build()
} else if (fragment.startsWith("movie")) { } else if (fragment.startsWith("movie")) {
httpUrl.newBuilder().fragment(null).build() 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 { } else {
httpUrl httpUrl
} }
@ -266,19 +299,48 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
} }
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val epDetails = preferences.getEpDetails 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, "")
}
val episodeList = if (response.request.url.toString().startsWith("$baseUrl/Users/")) { 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>() val data = response.parseAs<ItemDto>()
listOf(data.toSEpisode(baseUrl, userId!!, apiKey!!, epDetails, EpisodeType.MOVIE)) 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 { } else {
val data = response.parseAs<ItemsDto>() val data = response.parseAs<ItemsDto>()
data.items.map { data.items.map {
it.toSEpisode(baseUrl, userId!!, apiKey!!, epDetails, EpisodeType.EPISODE) it.toSEpisode(baseUrl, userId!!, apiKey!!, epDetails, EpisodeType.EPISODE, prefix)
} }
} }.reversed()
return episodeList.reversed()
} }
enum class EpisodeType { enum class EpisodeType {
@ -441,6 +503,12 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
private const val PREF_TRUST_CERT_KEY = "preferred_trust_all_certs" private const val PREF_TRUST_CERT_KEY = "preferred_trust_all_certs"
private const val PREF_TRUST_CERT_DEFAULT = false 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 = private fun getCustomLabel(): String =
@ -592,6 +660,30 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
preferences.edit().putBoolean(key, new).commit() preferences.edit().putBoolean(key, new).commit()
} }
}.also(screen::addPreference) }.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 private val SharedPreferences.getApiKey
@ -600,9 +692,6 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
private val SharedPreferences.getUserId private val SharedPreferences.getUserId
get() = getString(USERID_KEY, null) get() = getString(USERID_KEY, null)
private val SharedPreferences.getHostUrl
get() = getString(HOSTURL_KEY, HOSTURL_DEFAULT)!!
private val SharedPreferences.getUserName private val SharedPreferences.getUserName
get() = getString(USERNAME_KEY, USERNAME_DEFAULT)!! get() = getString(USERNAME_KEY, USERNAME_DEFAULT)!!
@ -627,6 +716,12 @@ class Jellyfin(private val suffix: String) : ConfigurableAnimeSource, AnimeHttpS
private val SharedPreferences.getTrustCert private val SharedPreferences.getTrustCert
get() = getBoolean(PREF_TRUST_CERT_KEY, PREF_TRUST_CERT_DEFAULT) 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) { private abstract class MediaLibPreference(context: Context) : ListPreference(context) {
abstract fun reload() abstract fun reload()
} }

View File

@ -44,6 +44,7 @@ data class ItemDto(
// Only for series, not season // Only for series, not season
@SerialName("Status") val seriesStatus: String? = null, @SerialName("Status") val seriesStatus: String? = null,
@SerialName("SeasonName") val seasonName: String? = null,
// Episode // Episode
@SerialName("PremiereDate") val premiereData: String? = null, @SerialName("PremiereDate") val premiereData: String? = null,
@ -93,6 +94,14 @@ data class ItemDto(
httpUrl.fragment("movie") httpUrl.fragment("movie")
title = name title = name
} }
"BoxSet" -> {
httpUrl.fragment("boxSet")
title = name
}
"Series" -> {
httpUrl.fragment("series")
title = name
}
} }
url = httpUrl.build().toString() url = httpUrl.build().toString()
@ -122,15 +131,16 @@ data class ItemDto(
apiKey: String, apiKey: String,
epDetails: Set<String>, epDetails: Set<String>,
epType: EpisodeType, epType: EpisodeType,
prefix: String,
): SEpisode = SEpisode.create().apply { ): SEpisode = SEpisode.create().apply {
when (epType) { when (epType) {
EpisodeType.MOVIE -> { EpisodeType.MOVIE -> {
episode_number = 1F episode_number = 1F
name = "Movie" name = "${prefix}Movie"
} }
EpisodeType.EPISODE -> { EpisodeType.EPISODE -> {
episode_number = indexNumber?.toFloat() ?: 1F episode_number = indexNumber?.toFloat() ?: 1F
name = "Ep. $indexNumber - ${this@ItemDto.name}" name = "${prefix}Ep. $indexNumber - ${this@ItemDto.name}"
} }
} }