Implement media fragment for tv shows

This commit is contained in:
2021-12-20 22:14:58 +01:00
parent a46fd4c6d2
commit 236ca9a6c9
16 changed files with 419 additions and 132 deletions

View File

@ -11,12 +11,13 @@ import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.util.*
private val json = Json { ignoreUnknownKeys = true }
object Crunchyroll {
private val baseUrl = "https://beta-api.crunchyroll.com"
private const val baseUrl = "https://beta-api.crunchyroll.com"
private var accessToken = ""
private var tokenType = ""
@ -25,9 +26,14 @@ object Crunchyroll {
private var signature = ""
private var keyPairID = ""
// TODO temp helper vary
var locale = "${Locale.GERMANY.language}-${Locale.GERMANY.country}"
var country = Locale.GERMANY.country
val browsingCache = arrayListOf<Item>()
fun login(username: String, password: String): Boolean = runBlocking {
val tokenEndpoint = "/auth/v1/token"
val formData = listOf(
"username" to username,
"password" to password,
@ -63,9 +69,15 @@ object Crunchyroll {
}
// TODO get/post difference
private suspend fun request(endpoint: String, params: Parameters = listOf()): Result<FuelJson, FuelError> = coroutineScope {
private suspend fun request(
endpoint: String,
params: Parameters = listOf(),
url: String = ""
): Result<FuelJson, FuelError> = coroutineScope {
val path = if (url.isEmpty()) "$baseUrl$endpoint" else url
return@coroutineScope (Dispatchers.IO) {
val (request, response, result) = Fuel.get("$baseUrl$endpoint", params)
val (request, response, result) = Fuel.get(path, params)
.header("Authorization", "$tokenType $accessToken")
.responseJson()
@ -77,42 +89,6 @@ object Crunchyroll {
}
}
// TESTING
// TODO sort_by, default alphabetical, n, locale de-DE, categories
/**
* Browse the media available on crunchyroll.
*
* @param sortBy
* @param n Number of items to return, defaults to 10
*
* @return A **[BrowseResult]** object is returned.
*/
suspend fun browse(sortBy: SortBy = SortBy.ALPHABETICAL, n: Int = 10): BrowseResult {
val browseEndpoint = "/content/v1/browse"
val parameters = listOf("sort_by" to sortBy.str, "n" to n)
val result = request(browseEndpoint, parameters)
// val browseResult = json.decodeFromString<BrowseResult>(result.component1()?.obj()?.toString()!!)
// println(browseResult.items.size)
return json.decodeFromString(result.component1()?.obj()?.toString()!!)
}
// TODO
suspend fun search() {
val searchEndpoint = "/content/v1/search"
val result = request(searchEndpoint)
println("${result.component1()?.obj()?.get("total")}")
val test = json.decodeFromString<BrowseResult>(result.component1()?.obj()?.toString()!!)
println(test.items.size)
}
/**
* Retrieve the identifiers necessary for streaming. If the identifiers are
* retrieved, set the corresponding global var. The identifiers are valid for 24h.
@ -132,4 +108,108 @@ object Crunchyroll {
println("keyPairID: $keyPairID")
}
// TODO locale de-DE, categories
/**
* Browse the media available on crunchyroll.
*
* @param sortBy
* @param n Number of items to return, defaults to 10
*
* @return A **[BrowseResult]** object is returned.
*/
suspend fun browse(sortBy: SortBy = SortBy.ALPHABETICAL, n: Int = 10): BrowseResult {
val browseEndpoint = "/content/v1/browse"
val parameters = listOf("sort_by" to sortBy.str, "n" to n)
val result = request(browseEndpoint, parameters)
val browseResult = result.component1()?.obj()?.let {
json.decodeFromString(it.toString())
} ?: NoneBrowseResult
// add results to cache TODO improve
browsingCache.clear()
browsingCache.addAll(browseResult.items)
return browseResult
}
// // TODO locale de-DE, type
suspend fun search(query: String, n: Int = 10) {
val searchEndpoint = "/content/v1/search"
val parameters = listOf("q" to query, "n" to n)
val result = request(searchEndpoint, parameters)
println("${result.component1()?.obj()?.get("total")}")
val test = json.decodeFromString<BrowseResult>(result.component1()?.obj()?.toString()!!)
println(test.items.size)
// TODO return
}
/**
* series id == crunchyroll id?
*/
suspend fun series(seriesId: String): Series {
val seriesEndpoint = "/cms/v2/$country/M3/crunchyroll/series/$seriesId"
val parameters = listOf(
"locale" to locale,
"Signature" to signature,
"Policy" to policy,
"Key-Pair-Id" to keyPairID
)
val result = request(seriesEndpoint, parameters)
return result.component1()?.obj()?.let {
json.decodeFromString(it.toString())
} ?: NoneSeries
}
suspend fun seasons(seriesId: String): Seasons {
val episodesEndpoint = "/cms/v2/$country/M3/crunchyroll/seasons"
val parameters = listOf(
"series_id" to seriesId,
"locale" to locale,
"Signature" to signature,
"Policy" to policy,
"Key-Pair-Id" to keyPairID
)
val result = request(episodesEndpoint, parameters)
return result.component1()?.obj()?.let {
println(it)
json.decodeFromString(it.toString())
} ?: NoneSeasons
}
suspend fun episodes(seasonId: String): Episodes {
val episodesEndpoint = "/cms/v2/$country/M3/crunchyroll/episodes"
val parameters = listOf(
"season_id" to seasonId,
"locale" to locale,
"Signature" to signature,
"Policy" to policy,
"Key-Pair-Id" to keyPairID
)
val result = request(episodesEndpoint, parameters)
return result.component1()?.obj()?.let {
println(it)
json.decodeFromString(it.toString())
} ?: NoneEpisodes
}
suspend fun playback(url: String): Playback {
val result = request("", url = url)
return result.component1()?.obj()?.let {
println(it)
json.decodeFromString(it.toString())
} ?: NonePlayback
}
}

View File

@ -1,5 +1,6 @@
package org.mosad.teapod.parser.crunchyroll
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
@ -26,9 +27,141 @@ data class Item(
// TODO metadata etc.
)
val NoneItem = Item("", "", "", "", "", Images(listOf(), listOf()))
val NoneBrowseResult = BrowseResult(0, listOf())
@Serializable
data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<List<Poster>>)
// crunchyroll why?
@Serializable
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
/**
* Series return type
*/
@Serializable
data class Series(
val id: String,
val title: String,
val description: String,
val images: Images
)
val NoneSeries = Series("", "", "", Images(listOf(), listOf()))
/**
* Seasons data type
*/
@Serializable
data class Seasons(val total: Int, val items: List<Season>)
@Serializable
data class Season(
val id: String,
val title: String,
val series_id: String,
val season_number: Int
)
val NoneSeasons = Seasons(0, listOf())
/**
* Episodes data type
*/
@Serializable
data class Episodes(val total: Int, val items: List<Episode>)
@Serializable
data class Episode(
@SerialName("id") val id: String,
@SerialName("title") val title: String,
@SerialName("series_id") val seriesId: String,
@SerialName("season_title") val seasonTitle: String,
@SerialName("season_id") val seasonId: String,
@SerialName("season_number") val seasonNumber: Int,
@SerialName("episode") val episode: String,
@SerialName("episode_number") val episodeNumber: Int,
@SerialName("description") val description: String,
@SerialName("next_episode_id") val nextEpisodeId: String = "", // use default value since the field is optional
@SerialName("next_episode_title") val nextEpisodeTitle: String = "", // use default value since the field is optional
@SerialName("is_subbed") val isSubbed: Boolean,
@SerialName("is_dubbed") val isDubbed: Boolean,
@SerialName("images") val images: Thumbnail,
@SerialName("duration_ms") val durationMs: Int,
@SerialName("playback") val playback: String,
)
@Serializable
data class Thumbnail(
@SerialName("thumbnail") val thumbnail: List<List<Poster>>
)
val NoneEpisodes = Episodes(0, listOf())
val NoneEpisode = Episode(
id = "",
title = "",
seriesId = "",
seasonId = "",
seasonTitle = "",
seasonNumber = 0,
episode = "",
episodeNumber = 0,
description = "",
nextEpisodeId = "",
nextEpisodeTitle = "",
isSubbed = false,
isDubbed = false,
images = Thumbnail(listOf()),
durationMs = 0,
playback = ""
)
/**
* Playback/stream data type
*/
@Serializable
data class Playback(
@SerialName("audio_locale") val audioLocale: String,
@SerialName("subtitles") val subtitles: Map<String, Subtitle>,
@SerialName("streams") val streams: Streams,
)
@Serializable
data class Subtitle(
@SerialName("locale") val locale: String,
@SerialName("url") val url: String,
@SerialName("format") val format: String,
)
@Serializable
data class Streams(
@SerialName("adaptive_dash") val adaptive_dash: Map<String, Stream>,
@SerialName("adaptive_hls") val adaptive_hls: Map<String, Stream>,
@SerialName("download_hls") val download_hls: Map<String, Stream>,
@SerialName("drm_adaptive_dash") val drm_adaptive_dash: Map<String, Stream>,
@SerialName("drm_adaptive_hls") val drm_adaptive_hls: Map<String, Stream>,
@SerialName("drm_download_hls") val drm_download_hls: Map<String, Stream>,
@SerialName("trailer_dash") val trailer_dash: Map<String, Stream>,
@SerialName("trailer_hls") val trailer_hls: Map<String, Stream>,
@SerialName("vo_adaptive_dash") val vo_adaptive_dash: Map<String, Stream>,
@SerialName("vo_adaptive_hls") val vo_adaptive_hls: Map<String, Stream>,
@SerialName("vo_drm_adaptive_dash") val vo_drm_adaptive_dash: Map<String, Stream>,
@SerialName("vo_drm_adaptive_hls") val vo_drm_adaptive_hls: Map<String, Stream>,
)
@Serializable
data class Stream(
@SerialName("hardsub_locale") val hardsubLocale: String,
@SerialName("url") val url: String,
@SerialName("vcodec") val vcodec: String,
)
val NonePlayback = Playback(
"",
mapOf(),
Streams(
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
)
)