fix crunchyroll parser to work with the latest api changes
This commit is contained in:
parent
097383a082
commit
8b7fb3ac5f
@ -31,6 +31,7 @@ import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.SerializationException
|
||||
@ -245,7 +246,7 @@ object Crunchyroll {
|
||||
|
||||
val account: Account = try {
|
||||
requestGet(indexEndpoint)
|
||||
} catch (ex: SerializationException) {
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "SerializationException in account(). This is bad!", ex)
|
||||
NoneAccount
|
||||
}
|
||||
@ -275,7 +276,7 @@ object Crunchyroll {
|
||||
): BrowseResult {
|
||||
val browseEndpoint = "/content/v1/browse"
|
||||
val parameters = mutableListOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"sort_by" to sortBy.str,
|
||||
"start" to start,
|
||||
"n" to n
|
||||
@ -298,7 +299,7 @@ object Crunchyroll {
|
||||
Log.d(TAG, "browse result not cached, fetching: $parameters")
|
||||
val browseResult: BrowseResult = try {
|
||||
requestGet(browseEndpoint, parameters)
|
||||
}catch (ex: SerializationException) {
|
||||
}catch (ex: Exception) {
|
||||
Log.e(TAG, "SerializationException in browse().", ex)
|
||||
NoneBrowseResult
|
||||
}
|
||||
@ -328,7 +329,7 @@ object Crunchyroll {
|
||||
suspend fun search(query: String, n: Int = 10): SearchResult {
|
||||
val searchEndpoint = "/content/v1/search"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"q" to query,
|
||||
"n" to n,
|
||||
"type" to "series"
|
||||
@ -339,8 +340,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(searchEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in search(), with query = \"$query\".", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in search(), with query = \"$query\".", ex)
|
||||
NoneSearchResult
|
||||
}
|
||||
}
|
||||
@ -355,7 +356,7 @@ object Crunchyroll {
|
||||
suspend fun objects(objects: List<String>): Collection<Item> {
|
||||
val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
@ -363,8 +364,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(episodesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in objects().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in objects().", ex)
|
||||
NoneCollection
|
||||
}
|
||||
}
|
||||
@ -375,12 +376,12 @@ object Crunchyroll {
|
||||
@Suppress("unused")
|
||||
suspend fun seasonList(): DiscSeasonList {
|
||||
val seasonListEndpoint = "/content/v1/season_list"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
return try {
|
||||
requestGet(seasonListEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in seasonList().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in seasonList().", ex)
|
||||
NoneDiscSeasonList
|
||||
}
|
||||
}
|
||||
@ -395,7 +396,7 @@ object Crunchyroll {
|
||||
suspend fun series(seriesId: String): Series {
|
||||
val seriesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/series/$seriesId"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"Signature" to signature,
|
||||
"Policy" to policy,
|
||||
"Key-Pair-Id" to keyPairID
|
||||
@ -403,8 +404,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(seriesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in series().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in series().", ex)
|
||||
NoneSeries
|
||||
}
|
||||
}
|
||||
@ -415,18 +416,21 @@ object Crunchyroll {
|
||||
* @param seriesId The series id for which to call up next
|
||||
* @return A **[UpNextSeriesItem]** with a Panel representing the up next episode
|
||||
*/
|
||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesItem {
|
||||
val upNextSeriesEndpoint = "/content/v1/up_next_series"
|
||||
suspend fun upNextSeries(seriesId: String): UpNextSeriesList {
|
||||
val upNextSeriesEndpoint = "/content/v2/discover/up_next/$seriesId"
|
||||
val parameters = listOf(
|
||||
"series_id" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag()
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(upNextSeriesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in upNextSeries().", ex)
|
||||
NoneUpNextSeriesItem
|
||||
} catch (ex: JsonConvertException) {
|
||||
Log.e(TAG, "JsonConvertException in upNextSeries() with seriesId=$seriesId", ex)
|
||||
NoneUpNextSeriesList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in upNextSeries().", ex)
|
||||
NoneUpNextSeriesList
|
||||
}
|
||||
}
|
||||
|
||||
@ -439,14 +443,14 @@ object Crunchyroll {
|
||||
suspend fun seasons(seriesId: String): Seasons {
|
||||
val seasonsEndpoint = "/content/v2/cms/series/$seriesId/seasons"
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag()
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(seasonsEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in seasons().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in seasons().", ex)
|
||||
NoneSeasons
|
||||
}
|
||||
}
|
||||
@ -460,14 +464,14 @@ object Crunchyroll {
|
||||
suspend fun episodes(seasonId: String): Episodes {
|
||||
val episodesEndpoint = "/content/v2/cms/seasons/$seasonId/episodes"
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag()
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(episodesEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in episodes().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in episodes().", ex)
|
||||
NoneEpisodes
|
||||
}
|
||||
}
|
||||
@ -480,18 +484,23 @@ object Crunchyroll {
|
||||
*/
|
||||
suspend fun streams(url: String): Streams {
|
||||
val parameters = listOf(
|
||||
"preferred_audio_language" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag()
|
||||
"preferred_audio_language" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(url, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in streams().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in streams().", ex)
|
||||
NoneStreams
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun streamsFromMediaGUID(mediaGUID: String): Streams {
|
||||
val streamsEndpoint = "/content/v2/cms/videos/$mediaGUID/streams"
|
||||
return streams(streamsEndpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional media functions: watchlist (series), playhead, similar to
|
||||
*/
|
||||
@ -504,13 +513,13 @@ object Crunchyroll {
|
||||
*/
|
||||
suspend fun isWatchlist(seriesId: String): Boolean {
|
||||
val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
return try {
|
||||
(requestGet(watchlistSeriesEndpoint, parameters) as JsonObject)
|
||||
.containsKey(seriesId)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in isWatchlist() with seriesId = $seriesId", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in isWatchlist() with seriesId = $seriesId", ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -522,13 +531,18 @@ object Crunchyroll {
|
||||
*/
|
||||
suspend fun postWatchlist(seriesId: String) {
|
||||
val watchlistPostEndpoint = "/content/v1/watchlist/$accountID"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
val json = buildJsonObject {
|
||||
put("content_id", seriesId)
|
||||
}
|
||||
|
||||
requestPost(watchlistPostEndpoint, parameters, json)
|
||||
try {
|
||||
requestPost(watchlistPostEndpoint, parameters, json)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in postWatchlist() with seriesId = $seriesId", ex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -538,9 +552,14 @@ object Crunchyroll {
|
||||
*/
|
||||
suspend fun deleteWatchlist(seriesId: String) {
|
||||
val watchlistDeleteEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
try {
|
||||
requestDelete(watchlistDeleteEndpoint, parameters)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in deleteWatchlist() with seriesId = $seriesId", ex)
|
||||
}
|
||||
|
||||
requestDelete(watchlistDeleteEndpoint, parameters)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -553,7 +572,7 @@ object Crunchyroll {
|
||||
*/
|
||||
suspend fun playheads(episodeIDs: List<String>): PlayheadsMap {
|
||||
val playheadsEndpoint = "/content/v1/playheads/$accountID/${episodeIDs.joinToString(",")}"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
return try {
|
||||
requestGet(playheadsEndpoint, parameters)
|
||||
@ -574,7 +593,7 @@ object Crunchyroll {
|
||||
*/
|
||||
suspend fun postPlayheads(episodeId: String, playhead: Int) {
|
||||
val playheadsEndpoint = "/content/v1/playheads/$accountID"
|
||||
val parameters = listOf("locale" to Preferences.preferredLocale.toLanguageTag())
|
||||
val parameters = listOf("locale" to Preferences.preferredSubtitleLocale.toLanguageTag())
|
||||
|
||||
val json = buildJsonObject {
|
||||
put("content_id", episodeId)
|
||||
@ -583,7 +602,7 @@ object Crunchyroll {
|
||||
|
||||
try {
|
||||
requestPost(playheadsEndpoint, parameters, json)
|
||||
} catch (ex: Throwable) {
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in postPlayheads()", ex.cause)
|
||||
}
|
||||
}
|
||||
@ -603,8 +622,8 @@ object Crunchyroll {
|
||||
return try {
|
||||
val response: HttpResponse = requestGet(datalabIntroEndpoint, url = staticUrl)
|
||||
Json.decodeFromString(response.bodyAsText())
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in datalabIntro(). EpisodeId=$episodeId", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in datalabIntro(). EpisodeId=$episodeId", ex)
|
||||
NoneDatalabIntro
|
||||
}
|
||||
}
|
||||
@ -620,14 +639,14 @@ object Crunchyroll {
|
||||
val similarToEndpoint = "/content/v1/$accountID/similar_to"
|
||||
val parameters = listOf(
|
||||
"guid" to seriesId,
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(similarToEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in similarTo().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in similarTo().", ex)
|
||||
NoneSimilarToResult
|
||||
}
|
||||
}
|
||||
@ -640,23 +659,24 @@ object Crunchyroll {
|
||||
* List items present in the watchlist.
|
||||
*
|
||||
* @param n Number of items to return, defaults to 20.
|
||||
* @return A **[Watchlist]** containing up to n **[Item]**.
|
||||
* @return A **[Collection]** containing up to n **[Item]**.
|
||||
*/
|
||||
suspend fun watchlist(n: Int = 20): Watchlist {
|
||||
val watchlistEndpoint = "/content/v1/$accountID/watchlist"
|
||||
suspend fun watchlist(n: Int = 20): Collection<Item> {
|
||||
val watchlistEndpoint = "/content/v2/discover/$accountID/watchlist"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n,
|
||||
"preferred_audio_language" to Preferences.preferredAudioLocale.toLanguageTag()
|
||||
)
|
||||
|
||||
val list: ContinueWatchingList = try {
|
||||
val list: Watchlist = try {
|
||||
requestGet(watchlistEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in watchlist().", ex)
|
||||
NoneContinueWatchingList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in watchlist().", ex)
|
||||
NoneWatchlist
|
||||
}
|
||||
|
||||
val objects = list.items.map{ it.panel.episodeMetadata.seriesId }
|
||||
val objects = list.data.map{ it.panel.episodeMetadata.seriesId }
|
||||
return objects(objects)
|
||||
}
|
||||
|
||||
@ -664,27 +684,27 @@ object Crunchyroll {
|
||||
* List the next up episodes for the logged in account.
|
||||
*
|
||||
* @param n Number of items to return, defaults to 20.
|
||||
* @return A **[ContinueWatchingList]** containing up to n **[ContinueWatchingItem]**.
|
||||
* @return A **[HistoryList]** containing up to n **[UpNextAccountItem]**.
|
||||
*/
|
||||
suspend fun upNextAccount(n: Int = 20): ContinueWatchingList {
|
||||
val watchlistEndpoint = "/content/v1/$accountID/up_next_account"
|
||||
suspend fun upNextAccount(n: Int = 20): HistoryList {
|
||||
val watchlistEndpoint = "/content/v2/discover/$accountID/history"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n
|
||||
)
|
||||
|
||||
return try {
|
||||
requestGet(watchlistEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in upNextAccount().", ex)
|
||||
NoneContinueWatchingList
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in upNextAccount().", ex)
|
||||
NoneHistoryList
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun recommendations(n: Int = 20, start: Int = 0): RecommendationsList {
|
||||
val recommendationsEndpoint = "/content/v1/$accountID/recommendations"
|
||||
val parameters = listOf(
|
||||
"locale" to Preferences.preferredLocale.toLanguageTag(),
|
||||
"locale" to Preferences.preferredSubtitleLocale.toLanguageTag(),
|
||||
"n" to n,
|
||||
"start" to start,
|
||||
"variant_id" to 0
|
||||
@ -692,8 +712,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(recommendationsEndpoint, parameters)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in recommendations().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in recommendations().", ex)
|
||||
NoneRecommendationsList
|
||||
}
|
||||
}
|
||||
@ -712,8 +732,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(profileEndpoint)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in profile().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in profile().", ex)
|
||||
NoneProfile
|
||||
}
|
||||
}
|
||||
@ -742,8 +762,8 @@ object Crunchyroll {
|
||||
|
||||
return try {
|
||||
requestGet(profileEndpoint)
|
||||
} catch (ex: SerializationException) {
|
||||
Log.e(TAG, "SerializationException in benefits().", ex)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Exception in benefits().", ex)
|
||||
NoneBenefits
|
||||
}
|
||||
}
|
||||
|
@ -117,24 +117,23 @@ data class Collection<T>(
|
||||
@SerialName("items") val items: List<T>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Collection2<T>(
|
||||
@SerialName("total") val total: Int,
|
||||
@SerialName("data") val data: List<T>
|
||||
)
|
||||
|
||||
typealias SearchResult = Collection<SearchCollection>
|
||||
typealias SearchCollection = Collection<Item>
|
||||
typealias BrowseResult = Collection<Item>
|
||||
typealias SimilarToResult = Collection<Item>
|
||||
typealias DiscSeasonList = Collection<SeasonListItem>
|
||||
typealias Watchlist = Collection<Item>
|
||||
typealias ContinueWatchingList = Collection<ContinueWatchingItem>
|
||||
typealias Watchlist = Collection2<WatchlistItem>
|
||||
typealias HistoryList = Collection2<UpNextAccountItem>
|
||||
typealias UpNextSeriesList = Collection2<UpNextSeriesItem>
|
||||
typealias RecommendationsList = Collection<Item>
|
||||
typealias Benefits = Collection<Benefit>
|
||||
|
||||
@Serializable
|
||||
data class UpNextSeriesItem(
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
@SerialName("fully_watched") val fullyWatched: Boolean,
|
||||
@SerialName("never_watched") val neverWatched: Boolean,
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
)
|
||||
|
||||
/**
|
||||
* panel data classes
|
||||
*/
|
||||
@ -178,18 +177,32 @@ data class SeasonListLocalization(
|
||||
/**
|
||||
* continue_watching_item data classes
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class ContinueWatchingItem(
|
||||
data class WatchlistItem(
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
@SerialName("new") val new: Boolean,
|
||||
@SerialName("new_content") val newContent: Boolean,
|
||||
// not present in up_next_account -> continue_watching_item
|
||||
// @SerialName("is_favorite") val isFavorite: Boolean,
|
||||
// @SerialName("never_watched") val neverWatched: Boolean,
|
||||
// @SerialName("completion_status") val completionStatus: Boolean,
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
// not present in watchlist -> continue_watching_item
|
||||
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
||||
@SerialName("never_watched") val neverWatched: Boolean = false,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpNextAccountItem(
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
@SerialName("new") val new: Boolean,
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
@SerialName("fully_watched") val fullyWatched: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpNextSeriesItem(
|
||||
@SerialName("panel") val panel: EpisodePanel,
|
||||
@SerialName("playhead") val playhead: Int,
|
||||
@SerialName("fully_watched") val fullyWatched: Boolean,
|
||||
@SerialName("never_watched") val neverWatched: Boolean,
|
||||
|
||||
)
|
||||
|
||||
// EpisodePanel is used in ContinueWatchingItem and UpNextSeriesItem
|
||||
@ -202,7 +215,7 @@ data class EpisodePanel(
|
||||
@SerialName("description") val description: String,
|
||||
@SerialName("episode_metadata") val episodeMetadata: EpisodeMetadata,
|
||||
@SerialName("images") val images: Thumbnail,
|
||||
@SerialName("playback") val playback: String,
|
||||
// @SerialName("streams_link") val streamsLink: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -218,24 +231,19 @@ data class EpisodeMetadata(
|
||||
|
||||
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
|
||||
val NoneEpisodeMetadata = EpisodeMetadata(0, 0, "", 0, "", "", "")
|
||||
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf()), "")
|
||||
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf())) //, "")
|
||||
|
||||
val NoneCollection = Collection<Item>(0, emptyList())
|
||||
val NoneSearchResult = SearchResult(0, emptyList())
|
||||
val NoneBrowseResult = BrowseResult(0, emptyList())
|
||||
val NoneSimilarToResult = SimilarToResult(0, emptyList())
|
||||
val NoneDiscSeasonList = DiscSeasonList(0, emptyList())
|
||||
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
|
||||
val NoneWatchlist = Watchlist(0, emptyList())
|
||||
val NoneHistoryList = HistoryList(0, emptyList())
|
||||
val NoneUpNextSeriesList = UpNextSeriesList(0, emptyList())
|
||||
val NoneRecommendationsList = RecommendationsList(0, emptyList())
|
||||
val NoneBenefits = Benefits(0, emptyList())
|
||||
|
||||
val NoneUpNextSeriesItem = UpNextSeriesItem(
|
||||
playhead = 0,
|
||||
fullyWatched = false,
|
||||
neverWatched = false,
|
||||
panel = NoneEpisodePanel
|
||||
)
|
||||
|
||||
/**
|
||||
* series data class
|
||||
*/
|
||||
@ -309,7 +317,7 @@ data class Episode(
|
||||
@SerialName("is_dubbed") val isDubbed: Boolean,
|
||||
@SerialName("images") val images: Thumbnail,
|
||||
@SerialName("duration_ms") val durationMs: Int,
|
||||
@SerialName("playback") val playback: String,
|
||||
@SerialName("versions") val versions: List<Version>,
|
||||
@SerialName("streams_link") val streamsLink: String,
|
||||
)
|
||||
|
||||
@ -318,6 +326,17 @@ data class Thumbnail(
|
||||
@SerialName("thumbnail") val thumbnail: List<List<Poster>>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Version(
|
||||
@SerialName("audio_locale") val audioLocale: String,
|
||||
@SerialName("guid") val guid: String,
|
||||
@SerialName("is_premium_only") val isPremiumOnly: Boolean,
|
||||
@SerialName("media_guid") val mediaGUID: String,
|
||||
@SerialName("original") val original: Boolean,
|
||||
@SerialName("season_guid") val seasonGUID: String,
|
||||
@SerialName("variant") val variant: String,
|
||||
)
|
||||
|
||||
val NoneEpisodes = Episodes(0, listOf())
|
||||
val NoneEpisode = Episode(
|
||||
id = "",
|
||||
@ -335,10 +354,20 @@ val NoneEpisode = Episode(
|
||||
isDubbed = false,
|
||||
images = Thumbnail(listOf()),
|
||||
durationMs = 0,
|
||||
playback = "",
|
||||
versions = emptyList(),
|
||||
streamsLink = ""
|
||||
)
|
||||
|
||||
val NoneVersion = Version(
|
||||
audioLocale = "",
|
||||
guid = "",
|
||||
isPremiumOnly = false,
|
||||
mediaGUID = "",
|
||||
original = true,
|
||||
seasonGUID = "",
|
||||
variant = ""
|
||||
)
|
||||
|
||||
typealias PlayheadsMap = Map<String, PlayheadObject>
|
||||
|
||||
@Serializable
|
||||
@ -412,6 +441,7 @@ data class Profile(
|
||||
@SerialName("avatar") val avatar: String,
|
||||
@SerialName("email") val email: String,
|
||||
@SerialName("maturity_rating") val maturityRating: String,
|
||||
@SerialName("preferred_content_audio_language") val preferredContentAudioLanguage: String,
|
||||
@SerialName("preferred_content_subtitle_language") val preferredContentSubtitleLanguage: String,
|
||||
@SerialName("username") val username: String,
|
||||
)
|
||||
@ -419,6 +449,7 @@ val NoneProfile = Profile(
|
||||
avatar = "",
|
||||
email = "",
|
||||
maturityRating = "",
|
||||
preferredContentAudioLanguage = "",
|
||||
preferredContentSubtitleLanguage = "",
|
||||
username = ""
|
||||
)
|
||||
|
@ -8,7 +8,9 @@ import java.util.*
|
||||
|
||||
object Preferences {
|
||||
|
||||
var preferredLocale: Locale = Locale.forLanguageTag("en-US") // TODO this should be saved (potential offline usage) but fetched on start
|
||||
var preferredAudioLocale: Locale = Locale.forLanguageTag("en-US")
|
||||
internal set
|
||||
var preferredSubtitleLocale: Locale = Locale.forLanguageTag("en-US")
|
||||
internal set
|
||||
var preferSubbed = false
|
||||
internal set
|
||||
@ -30,13 +32,22 @@ object Preferences {
|
||||
)
|
||||
}
|
||||
|
||||
fun savePreferredLocal(context: Context, preferredLocale: Locale) {
|
||||
fun savePreferredAudioLocal(context: Context, preferredLocale: Locale) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag())
|
||||
apply()
|
||||
}
|
||||
|
||||
this.preferredLocale = preferredLocale
|
||||
this.preferredAudioLocale = preferredLocale
|
||||
}
|
||||
|
||||
fun savePreferredSubtitleLocal(context: Context, preferredLocale: Locale) {
|
||||
with(getSharedPref(context).edit()) {
|
||||
putString(context.getString(R.string.save_key_preferred_local), preferredLocale.toLanguageTag())
|
||||
apply()
|
||||
}
|
||||
|
||||
this.preferredSubtitleLocale = preferredLocale
|
||||
}
|
||||
|
||||
fun savePreferSecondary(context: Context, preferSubbed: Boolean) {
|
||||
@ -90,7 +101,12 @@ object Preferences {
|
||||
fun load(context: Context) {
|
||||
val sharedPref = getSharedPref(context)
|
||||
|
||||
preferredLocale = Locale.forLanguageTag(
|
||||
preferredAudioLocale = Locale.forLanguageTag(
|
||||
sharedPref.getString(
|
||||
context.getString(R.string.save_key_preferred_audio_local), "en-US"
|
||||
) ?: "en-US"
|
||||
)
|
||||
preferredSubtitleLocale = Locale.forLanguageTag(
|
||||
sharedPref.getString(
|
||||
context.getString(R.string.save_key_preferred_local), "en-US"
|
||||
) ?: "en-US"
|
||||
|
@ -169,9 +169,12 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||
scope.launch { Crunchyroll.account() },
|
||||
scope.launch {
|
||||
// update the local preferred content language, since it may have changed
|
||||
val locale = Locale.forLanguageTag(Crunchyroll.profile().preferredContentSubtitleLanguage)
|
||||
Preferences.savePreferredLocal(this@MainActivity, locale)
|
||||
val profile = Crunchyroll.profile()
|
||||
|
||||
val audioLocale = Locale.forLanguageTag(profile.preferredContentAudioLanguage)
|
||||
val subtitleLocale = Locale.forLanguageTag(profile.preferredContentSubtitleLanguage)
|
||||
Preferences.savePreferredAudioLocal(this@MainActivity, audioLocale)
|
||||
Preferences.savePreferredSubtitleLocal(this@MainActivity, subtitleLocale)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ class AccountFragment : Fragment() {
|
||||
|
||||
}.invokeOnCompletion {
|
||||
// update the local preferred content language
|
||||
Preferences.savePreferredLocal(requireContext(), preferredLocale)
|
||||
Preferences.savePreferredSubtitleLocal(requireContext(), preferredLocale)
|
||||
|
||||
// update profile since the language selection might have changed
|
||||
profile = lifecycleScope.async { Crunchyroll.profile() }
|
||||
|
@ -20,7 +20,7 @@ import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.FragmentMediaBinding
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesItem
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesList
|
||||
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
|
||||
import org.mosad.teapod.util.playerIntent
|
||||
import org.mosad.teapod.util.tmdb.TMDBApiController
|
||||
@ -104,8 +104,8 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
}
|
||||
binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull()
|
||||
|
||||
binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesItem) {
|
||||
upNextSeries.panel.title
|
||||
binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesList) {
|
||||
upNextSeries.data.first().panel.title
|
||||
} else seriesCrunchy.title
|
||||
binding.textOverview.text = seriesCrunchy.description
|
||||
|
||||
@ -174,8 +174,9 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
|
||||
private fun initActions() = with(model) {
|
||||
binding.buttonPlay.setOnClickListener {
|
||||
if (upNextSeries != NoneUpNextSeriesItem) {
|
||||
playEpisode(upNextSeries.panel.episodeMetadata.seasonId, upNextSeries.panel.id)
|
||||
if (upNextSeries != NoneUpNextSeriesList) {
|
||||
val panel = upNextSeries.data.first().panel
|
||||
playEpisode(panel.episodeMetadata.seasonId, panel.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,8 +200,8 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
|
||||
private fun playerFinishedCallback() = lifecycleScope.launch {
|
||||
model.updateOnResume()
|
||||
|
||||
if (model.upNextSeries != NoneUpNextSeriesItem) {
|
||||
binding.textTitle.text = model.upNextSeries.panel.title
|
||||
if (model.upNextSeries != NoneUpNextSeriesList) {
|
||||
binding.textTitle.text = model.upNextSeries.data.first().panel.title
|
||||
}
|
||||
|
||||
// needs to be called after model.updateOnResume()
|
||||
|
@ -44,7 +44,7 @@ class MyListsFragment : Fragment() {
|
||||
}.attach()
|
||||
|
||||
lifecycleScope.launch {
|
||||
val items = Crunchyroll.watchlist(50).items
|
||||
val items = Crunchyroll.watchlist(50)
|
||||
|
||||
MediaFragmentSimilar(items.toItemMediaList()).also {
|
||||
fragments.add(it)
|
||||
|
@ -26,7 +26,8 @@ import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.parser.crunchyroll.*
|
||||
import kotlin.random.Random
|
||||
@ -40,7 +41,7 @@ class HomeViewModel : ViewModel() {
|
||||
sealed class UiState {
|
||||
object Loading : UiState()
|
||||
data class Normal(
|
||||
val upNextItems: List<ContinueWatchingItem>,
|
||||
val upNextItems: List<UpNextAccountItem>,
|
||||
val watchlistItems: List<Item>,
|
||||
val recommendationsItems: List<Item>,
|
||||
val recentlyAddedItems: List<Item>,
|
||||
@ -65,7 +66,7 @@ class HomeViewModel : ViewModel() {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
// run the loading in parallel to speed up the process
|
||||
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().items }
|
||||
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().data }
|
||||
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(WATCHLIST_LENGTH).items }
|
||||
val recommendationsJob = viewModelScope.async {
|
||||
Crunchyroll.recommendations(20).items
|
||||
@ -81,7 +82,7 @@ class HomeViewModel : ViewModel() {
|
||||
// FIXME crashes on newTitles.items.size == 0
|
||||
val highlightItem = recentlyAddedItems[Random.nextInt(recentlyAddedItems.size)]
|
||||
val highlightItemUpNextJob = viewModelScope.async {
|
||||
Crunchyroll.upNextSeries(highlightItem.id)
|
||||
Crunchyroll.upNextSeries(highlightItem.id).data.first()
|
||||
}
|
||||
val highlightItemIsWatchlistJob = viewModelScope.async {
|
||||
Crunchyroll.isWatchlist(highlightItem.id)
|
||||
@ -132,7 +133,7 @@ class HomeViewModel : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
uiState.update { currentUiState ->
|
||||
if (currentUiState is UiState.Normal) {
|
||||
val upNextItems = Crunchyroll.upNextAccount().items
|
||||
val upNextItems = Crunchyroll.upNextAccount().data
|
||||
currentUiState.copy(upNextItems = upNextItems)
|
||||
} else {
|
||||
currentUiState
|
||||
|
@ -30,7 +30,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
val currentPlayheads: MutableMap<String, PlayheadObject> = mutableMapOf()
|
||||
var isWatchlist = false
|
||||
internal set
|
||||
var upNextSeries = NoneUpNextSeriesItem
|
||||
var upNextSeries = NoneUpNextSeriesList
|
||||
internal set
|
||||
var similarTo = NoneSimilarToResult
|
||||
internal set
|
||||
@ -60,8 +60,8 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||
// load the preferred season:
|
||||
// next episode > preferred language (language per season, not per stream)
|
||||
currentSeasonCrunchy = seasonsCrunchy.data.firstOrNull{ season ->
|
||||
season.id == upNextSeries.panel.episodeMetadata.seasonId
|
||||
} ?: seasonsCrunchy.getPreferredSeasonByLocal(Preferences.preferredLocale)
|
||||
season.id == upNextSeries.data.first().panel.episodeMetadata.seasonId
|
||||
} ?: seasonsCrunchy.getPreferredSeasonByLocal(Preferences.preferredSubtitleLocale)
|
||||
// Note: if we need to query metaDB, do it now
|
||||
|
||||
// load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes)
|
||||
|
@ -75,10 +75,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
internal set
|
||||
var currentEpisode = NoneEpisode
|
||||
internal set
|
||||
var currentVersion = NoneVersion
|
||||
internal set
|
||||
var currentStreams = NoneStreams
|
||||
internal set
|
||||
|
||||
// current playback settings
|
||||
var currentLanguage: Locale = Preferences.preferredLocale
|
||||
var currentAudioLocale: Locale = Preferences.preferredAudioLocale
|
||||
internal set
|
||||
var currentSubtitleLocale: Locale = Preferences.preferredSubtitleLocale
|
||||
internal set
|
||||
|
||||
init {
|
||||
@ -146,9 +151,31 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
playCurrentMedia(currentPlayhead)
|
||||
}
|
||||
|
||||
fun setLanguage(language: Locale) {
|
||||
currentLanguage = language
|
||||
playCurrentMedia(player.currentPosition)
|
||||
fun setLanguage(newAudioLocale: Locale, newSubtitleLocale: Locale) {
|
||||
// TODO if the audio locale has changes update the streams, if only the subtitle locale has changed load the new stream
|
||||
if (newAudioLocale != currentAudioLocale) {
|
||||
currentAudioLocale = newAudioLocale
|
||||
|
||||
currentVersion = currentEpisode.versions.firstOrNull {
|
||||
it.audioLocale == currentAudioLocale.toLanguageTag()
|
||||
} ?: currentEpisode.versions.first()
|
||||
|
||||
viewModelScope.launch {
|
||||
currentStreams = Crunchyroll.streamsFromMediaGUID(currentVersion.mediaGUID)
|
||||
Log.d(classTag, currentVersion.toString())
|
||||
|
||||
playCurrentMedia(player.currentPosition)
|
||||
}
|
||||
} else if (newSubtitleLocale != currentSubtitleLocale) {
|
||||
currentSubtitleLocale = newSubtitleLocale
|
||||
playCurrentMedia(player.currentPosition)
|
||||
}
|
||||
|
||||
println(newSubtitleLocale != currentSubtitleLocale)
|
||||
println("currentSubtitleLocale: $currentSubtitleLocale")
|
||||
println("newSubtitleLocale: $newSubtitleLocale")
|
||||
|
||||
// else nothing has changed so no need do do anything
|
||||
}
|
||||
|
||||
// player actions
|
||||
@ -198,8 +225,17 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
|
||||
joinAll(
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
currentStreams = Crunchyroll.streams(currentEpisode.streamsLink)
|
||||
println("stream: $Streams")
|
||||
currentVersion = if (Preferences.preferSubbed) {
|
||||
currentEpisode.versions.first { it.original }
|
||||
} else {
|
||||
currentEpisode.versions
|
||||
.firstOrNull { it.audioLocale == currentAudioLocale.toLanguageTag() }
|
||||
?: currentEpisode.versions.first()
|
||||
}
|
||||
|
||||
currentStreams = Crunchyroll.streamsFromMediaGUID(currentVersion.mediaGUID)
|
||||
Log.d(classTag, currentVersion.toString())
|
||||
println("stream: $currentStreams")
|
||||
},
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Crunchyroll.playheads(listOf(currentEpisode.id))[currentEpisode.id]?.let {
|
||||
@ -215,7 +251,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
currentIntroMetadata = NoneDatalabIntro //Crunchyroll.datalabIntro(currentEpisode.id)
|
||||
}
|
||||
)
|
||||
Log.d(classTag, "playback: ${currentEpisode.streamsLink}")
|
||||
Log.d(classTag, "streams: ${currentEpisode.streamsLink}")
|
||||
|
||||
if (startPlayback) {
|
||||
playCurrentMedia()
|
||||
@ -223,25 +259,25 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the current media from currentPlaybackCr.
|
||||
* Play the current media from currentStreams.
|
||||
*
|
||||
* @param seekPosition The seek position for the episode (default = 0).
|
||||
* @param seekPosition The seek position for the media (default = 0).
|
||||
*/
|
||||
fun playCurrentMedia(seekPosition: Long = 0) {
|
||||
// get preferred stream url, set current language if it differs from the preferred one
|
||||
val preferredLocale = currentLanguage
|
||||
val preferredLocale = currentSubtitleLocale
|
||||
val fallbackLocal = Locale.US
|
||||
val url = when {
|
||||
currentStreams.data[0].adaptive_hls.containsKey(preferredLocale.toLanguageTag()) -> {
|
||||
currentStreams.data[0].adaptive_hls[preferredLocale.toLanguageTag()]?.url
|
||||
}
|
||||
currentStreams.data[0].adaptive_hls.containsKey(fallbackLocal.toLanguageTag()) -> {
|
||||
currentLanguage = fallbackLocal
|
||||
currentSubtitleLocale = fallbackLocal
|
||||
currentStreams.data[0].adaptive_hls[fallbackLocal.toLanguageTag()]?.url
|
||||
}
|
||||
else -> {
|
||||
// if no language tag is present use the first entry
|
||||
currentLanguage = Locale.ROOT
|
||||
currentSubtitleLocale = Locale.ROOT
|
||||
currentStreams.data[0].adaptive_hls.entries.first().value.url
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.DialogFragment
|
||||
@ -24,8 +25,8 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
private lateinit var model: PlayerViewModel
|
||||
private lateinit var binding: PlayerLanguageSettingsBinding
|
||||
|
||||
private var selectedLocale = Locale.ROOT
|
||||
private var selectedView: View? = null
|
||||
private var selectedSubtitleLocale = Locale.ROOT
|
||||
private var selectedAudioLocale = Locale.ROOT
|
||||
|
||||
companion object {
|
||||
const val TAG = "LanguageSettingsDialogFragment"
|
||||
@ -35,7 +36,7 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
|
||||
model = ViewModelProvider(requireActivity())[PlayerViewModel::class.java]
|
||||
selectedLocale = model.currentLanguage
|
||||
selectedSubtitleLocale = model.currentSubtitleLocale
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
@ -46,18 +47,41 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
var selectedSubtitleView: TextView? = null
|
||||
model.currentStreams.data[0].adaptive_hls.keys.forEach { languageTag ->
|
||||
val locale = Locale.forLanguageTag(languageTag)
|
||||
addLanguage(locale, locale == model.currentLanguage) { v ->
|
||||
selectedLocale = locale
|
||||
updateSelectedLanguage(v as TextView)
|
||||
val subtitleView = addLanguage(binding.linearSubtitleLanguages, locale) { v ->
|
||||
selectedSubtitleLocale = locale
|
||||
updateSelectedLanguage(binding.linearSubtitleLanguages, v as TextView)
|
||||
}
|
||||
|
||||
// if the view is the currently selected one, highlight it
|
||||
if (locale == model.currentSubtitleLocale) {
|
||||
selectedSubtitleView = subtitleView
|
||||
updateSelectedLanguage(binding.linearSubtitleLanguages, subtitleView)
|
||||
}
|
||||
}
|
||||
|
||||
val currentAudioLocal = Locale.forLanguageTag(model.currentVersion.audioLocale)
|
||||
var selectedAudioView: TextView? = null
|
||||
model.currentEpisode.versions.forEach { version ->
|
||||
val locale = Locale.forLanguageTag(version.audioLocale)
|
||||
val audioView = addLanguage(binding.linearAudioLanguages, locale) { v ->
|
||||
selectedAudioLocale = locale
|
||||
updateSelectedLanguage(binding.linearAudioLanguages, v as TextView)
|
||||
}
|
||||
|
||||
// if the view is the currently selected one, highlight it
|
||||
if (locale == currentAudioLocal) {
|
||||
selectedAudioView = audioView
|
||||
updateSelectedLanguage(binding.linearAudioLanguages, audioView)
|
||||
}
|
||||
}
|
||||
|
||||
binding.buttonCloseLanguageSettings.setOnClickListener { dismiss() }
|
||||
binding.buttonCancel.setOnClickListener { dismiss() }
|
||||
binding.buttonSelect.setOnClickListener {
|
||||
model.setLanguage(selectedLocale)
|
||||
model.setLanguage(selectedAudioLocale, selectedSubtitleLocale)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@ -65,8 +89,12 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
hideBars(requireDialog().window, binding.root)
|
||||
|
||||
// scroll to the position of the view, if it's the selected language
|
||||
binding.scrollLanguages.post {
|
||||
binding.scrollLanguages.scrollTo(0, selectedView?.top ?: 0)
|
||||
binding.scrollSubtitleLanguages.post {
|
||||
binding.scrollSubtitleLanguages.scrollTo(0, selectedSubtitleView?.top ?: 0)
|
||||
}
|
||||
|
||||
binding.scrollAudioLanguages.post {
|
||||
binding.scrollSubtitleLanguages.scrollTo(0, selectedAudioView?.top ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,35 +103,32 @@ class LanguageSettingsDialogFragment : DialogFragment() {
|
||||
model.player.play()
|
||||
}
|
||||
|
||||
private fun addLanguage(locale: Locale, isSelected: Boolean, onClick: View.OnClickListener) {
|
||||
private fun addLanguage(linear: LinearLayout, locale: Locale, onClick: View.OnClickListener): TextView {
|
||||
val text = TextView(context).apply {
|
||||
height = 96
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
text = if (locale == Locale.ROOT) context.getString(R.string.no_subtitles) else locale.displayLanguage
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||
|
||||
if (isSelected) {
|
||||
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_baseline_check_24, 0, 0, 0)
|
||||
compoundDrawablesRelative.getOrNull(0)?.setTint(Color.WHITE)
|
||||
compoundDrawablePadding = 12
|
||||
|
||||
selectedView = this
|
||||
} else {
|
||||
setTextColor(context.resources.getColor(R.color.textSecondaryDark, context.theme))
|
||||
setPadding(75, 0, 0, 0)
|
||||
}
|
||||
setTextColor(context.resources.getColor(R.color.textSecondaryDark, context.theme))
|
||||
setPadding(75, 0, 0, 0)
|
||||
|
||||
setOnClickListener(onClick)
|
||||
}
|
||||
|
||||
binding.linearLanguages.addView(text)
|
||||
linear.addView(text)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private fun updateSelectedLanguage(selected: TextView) {
|
||||
/**
|
||||
* Highlights the selected audio/subtitle language
|
||||
*
|
||||
* @param languageLayout The audio/subtitle Layout to update
|
||||
* @param selected The newly selected language TextView
|
||||
*/
|
||||
private fun updateSelectedLanguage(languageLayout: LinearLayout, selected: TextView) {
|
||||
// rest all tf to not selected style
|
||||
binding.linearLanguages.children.forEach { child ->
|
||||
languageLayout.children.forEach { child ->
|
||||
if (child is TextView) {
|
||||
child.apply {
|
||||
setTextColor(context.resources.getColor(R.color.textPrimaryDark, context.theme))
|
||||
|
@ -10,7 +10,6 @@ import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.parser.crunchyroll.Collection
|
||||
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
|
||||
import org.mosad.teapod.parser.crunchyroll.Item
|
||||
import org.mosad.teapod.ui.activity.player.PlayerActivity
|
||||
import java.util.*
|
||||
@ -48,19 +47,6 @@ fun List<Item>.toItemMediaList(): List<ItemMedia> {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("toItemMediaListContinueWatchingItem")
|
||||
fun Collection<ContinueWatchingItem>.toItemMediaList(): List<ItemMedia> {
|
||||
return items.map {
|
||||
ItemMedia(it.panel.episodeMetadata.seriesId, it.panel.title, it.panel.images.thumbnail[0][0].source)
|
||||
}
|
||||
}
|
||||
|
||||
fun List<ContinueWatchingItem>.toItemMediaList(): List<ItemMedia> {
|
||||
return this.map {
|
||||
ItemMedia(it.panel.episodeMetadata.seriesId, it.panel.title, it.panel.images.thumbnail[0][0].source)
|
||||
}
|
||||
}
|
||||
|
||||
fun Locale.toDisplayString(fallback: String): String {
|
||||
return if (this.displayLanguage.isNotEmpty() && this.displayCountry.isNotEmpty()) {
|
||||
"${this.displayLanguage} (${this.displayCountry})"
|
||||
|
@ -9,9 +9,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
|
||||
import org.mosad.teapod.parser.crunchyroll.UpNextAccountItem
|
||||
|
||||
class MediaEpisodeListAdapter(private val onClickListener: OnClickListener, private val itemOffset: Int = 0) : ListAdapter<ContinueWatchingItem, MediaEpisodeListAdapter.MediaViewHolder>(DiffCallback) {
|
||||
class MediaEpisodeListAdapter(private val onClickListener: OnClickListener, private val itemOffset: Int = 0) : ListAdapter<UpNextAccountItem, MediaEpisodeListAdapter.MediaViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
||||
val binding = ItemMediaBinding.inflate(
|
||||
@ -37,7 +37,7 @@ class MediaEpisodeListAdapter(private val onClickListener: OnClickListener, priv
|
||||
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: ContinueWatchingItem) {
|
||||
fun bind(item: UpNextAccountItem) {
|
||||
val metadata = item.panel.episodeMetadata
|
||||
|
||||
binding.textTitle.text = binding.root.context.getString(R.string.season_episode_title,
|
||||
@ -57,17 +57,17 @@ class MediaEpisodeListAdapter(private val onClickListener: OnClickListener, priv
|
||||
}
|
||||
}
|
||||
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<ContinueWatchingItem>() {
|
||||
override fun areItemsTheSame(oldItem: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<UpNextAccountItem>() {
|
||||
override fun areItemsTheSame(oldItem: UpNextAccountItem, newItem: UpNextAccountItem): Boolean {
|
||||
return oldItem.panel.id == newItem.panel.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
|
||||
override fun areContentsTheSame(oldItem: UpNextAccountItem, newItem: UpNextAccountItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class OnClickListener(val clickListener: (item: ContinueWatchingItem) -> Unit) {
|
||||
fun onClick(item: ContinueWatchingItem) = clickListener(item)
|
||||
class OnClickListener(val clickListener: (item: UpNextAccountItem) -> Unit) {
|
||||
fun onClick(item: UpNextAccountItem) = clickListener(item)
|
||||
}
|
||||
}
|
@ -67,7 +67,7 @@ class TMDBApiController {
|
||||
): T = coroutineScope {
|
||||
val path = "$apiUrl$endpoint"
|
||||
val params = concatenate(
|
||||
listOf("api_key" to apiKey, "language" to Preferences.preferredLocale.language),
|
||||
listOf("api_key" to apiKey, "language" to Preferences.preferredSubtitleLocale.language),
|
||||
parameters
|
||||
)
|
||||
|
||||
|
@ -131,7 +131,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:text="@string/subtitles"
|
||||
android:text="@string/language"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_baseline_subtitles_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -36,7 +36,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="44dp"
|
||||
android:text="@string/subtitles"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/player_white"
|
||||
android:textSize="18sp"
|
||||
@ -44,24 +43,80 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_languages"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/language"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linear_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linear_top">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_languages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="56dp"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:orientation="vertical" />
|
||||
</ScrollView>
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_audio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/audio"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/player_white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_audio_languages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/audio">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_audio_languages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="56dp"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:orientation="vertical" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subtitles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/subtitles"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/player_white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_subtitle_languages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/subtitles">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_subtitle_languages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="56dp"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:orientation="vertical" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_bottom"
|
||||
|
@ -86,6 +86,7 @@
|
||||
<string name="next_episode">Nächste Folge</string>
|
||||
<string name="skip_opening">Intro überspringen</string>
|
||||
<string name="language">Sprache</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="subtitles">Untertitel</string>
|
||||
<string name="episodes">Folgen</string>
|
||||
<string name="episode">Folge</string>
|
||||
|
@ -112,6 +112,7 @@
|
||||
<string name="time_min_sec" translatable="false">%1$02d:%2$02d</string>
|
||||
<string name="time_hour_min_sec" translatable="false">%1$d:%2$02d:%3$02d</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="subtitles">Subtitles</string>
|
||||
<string name="episodes">Episodes</string>
|
||||
<string name="episode">Episode</string>
|
||||
@ -150,6 +151,7 @@
|
||||
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
||||
<!-- for legacy reasons the prefer subbed key is called prefer_secondary-->
|
||||
<string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string>
|
||||
<string name="save_key_preferred_audio_local" translatable="false">org.mosad.teapod.preferred_audio_local</string>
|
||||
<string name="save_key_preferred_local" translatable="false">org.mosad.teapod.preferred_local</string>
|
||||
<string name="save_key_autoplay" translatable="false">org.mosad.teapod.autoplay</string>
|
||||
<string name="save_key_dev_settings" translatable="false">org.mosad.teapod.dev.settings</string>
|
||||
|
@ -8,7 +8,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.0'
|
||||
classpath 'com.android.tools.build:gradle:7.4.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
Loading…
Reference in New Issue
Block a user