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