2020-10-19 19:59:53 +02:00
/ * *
* Teapod
*
2021-01-01 12:15:17 +01:00
* Copyright 2020 - 2021 < seil0 @mosad . xyz >
2020-10-19 19:59:53 +02:00
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin Street , Fifth Floor , Boston ,
* MA 02110 - 1301 , USA .
*
* /
2020-10-08 22:20:20 +02:00
package org.mosad.teapod.parser
2020-10-11 23:16:47 +02:00
import android.util.Log
2020-10-20 20:07:59 +02:00
import com.google.gson.JsonParser
2020-10-09 15:18:52 +02:00
import kotlinx.coroutines.*
2020-10-08 22:20:20 +02:00
import org.jsoup.Connection
import org.jsoup.Jsoup
2020-10-11 13:18:20 +02:00
import org.mosad.teapod.preferences.EncryptedPreferences
2020-10-20 20:07:59 +02:00
import org.mosad.teapod.util.*
2020-10-11 23:16:47 +02:00
import org.mosad.teapod.util.DataTypes.MediaType
2020-10-15 16:23:52 +02:00
import java.io.IOException
2021-09-05 13:43:27 +02:00
import java.net.CookieStore
2020-10-12 17:52:24 +02:00
import java.util.*
2020-12-18 17:23:04 +01:00
import kotlin.random.Random
2021-09-05 13:43:27 +02:00
import kotlin.reflect.jvm.jvmName
2020-10-08 22:20:20 +02:00
2020-10-19 19:59:53 +02:00
object AoDParser {
2020-10-08 22:20:20 +02:00
2020-10-19 19:59:53 +02:00
private const val baseUrl = " https://www.anime-on-demand.de "
private const val loginPath = " /users/sign_in "
private const val libraryPath = " /animes "
2021-04-17 20:59:37 +02:00
private const val subscriptionPath = " /mypools "
2020-10-08 22:20:20 +02:00
2021-02-06 19:02:12 +01:00
private const val userAgent = " Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0 "
2020-10-15 18:51:29 +02:00
2021-09-05 13:43:27 +02:00
private lateinit var cookieStore : CookieStore
2020-10-19 19:59:53 +02:00
private var csrfToken : String = " "
private var loginSuccess = false
2020-10-11 10:02:00 +02:00
2021-08-31 19:47:18 +02:00
private val aodMediaList = arrayListOf < AoDMedia > ( ) // actual media (data)
2021-07-25 19:15:31 +02:00
// gui media
val guiMediaList = arrayListOf < ItemMedia > ( )
2020-12-06 15:18:15 +01:00
val highlightsList = arrayListOf < ItemMedia > ( )
2020-10-19 19:59:53 +02:00
val newEpisodesList = arrayListOf < ItemMedia > ( )
2020-12-11 10:54:40 +01:00
val newSimulcastsList = arrayListOf < ItemMedia > ( )
val newTitlesList = arrayListOf < ItemMedia > ( )
2021-01-21 18:22:53 +01:00
val topTenList = arrayListOf < ItemMedia > ( )
2020-10-08 22:20:20 +02:00
2020-10-12 23:26:32 +02:00
fun login ( ) : Boolean = runBlocking {
2020-10-08 22:20:20 +02:00
2021-01-16 00:16:47 +01:00
withContext ( Dispatchers . IO ) {
2021-09-05 13:43:27 +02:00
// get the authenticity token and cookies
val conAuth = Jsoup . connect ( baseUrl + loginPath )
2020-10-08 22:20:20 +02:00
. header ( " User-Agent " , userAgent )
2021-09-05 13:43:27 +02:00
cookieStore = conAuth . cookieStore ( )
csrfToken = conAuth . execute ( ) . parse ( ) . select ( " meta[name=csrf-token] " ) . attr ( " content " )
2020-10-08 22:20:20 +02:00
2021-09-05 13:43:27 +02:00
Log . d ( AoDParser :: class . jvmName , " Received authenticity token: $csrfToken " )
Log . d ( AoDParser :: class . jvmName , " Received authenticity cookies: $cookieStore " )
2020-10-08 22:20:20 +02:00
val data = mapOf (
2020-10-11 13:18:20 +02:00
Pair ( " user[login] " , EncryptedPreferences . login ) ,
Pair ( " user[password] " , EncryptedPreferences . password ) ,
2020-10-08 22:20:20 +02:00
Pair ( " user[remember_me] " , " 1 " ) ,
Pair ( " commit " , " Einloggen " ) ,
2021-09-05 13:43:27 +02:00
Pair ( " authenticity_token " , csrfToken )
2020-10-08 22:20:20 +02:00
)
2020-10-12 20:30:45 +02:00
val resLogin = Jsoup . connect ( baseUrl + loginPath )
2020-10-08 22:20:20 +02:00
. method ( Connection . Method . POST )
2021-01-13 20:57:00 +01:00
. timeout ( 60000 ) // login can take some time default is 60000 (60 sec)
2020-10-08 22:20:20 +02:00
. data ( data )
. postDataCharset ( " UTF-8 " )
2021-09-05 13:43:27 +02:00
. cookieStore ( cookieStore )
2020-10-08 22:20:20 +02:00
. execute ( )
//println(resLogin.body())
2020-10-16 18:24:34 +02:00
2020-10-09 15:18:52 +02:00
loginSuccess = resLogin . body ( ) . contains ( " Hallo, du bist jetzt angemeldet. " )
2021-09-05 13:43:27 +02:00
Log . i ( AoDParser :: class . jvmName , " Status: ${resLogin.statusCode()} ( ${resLogin.statusMessage()} ), login successful: $loginSuccess " )
2020-10-12 23:26:32 +02:00
loginSuccess
2020-10-08 22:20:20 +02:00
}
}
2020-10-11 23:16:47 +02:00
/ * *
2020-10-19 19:59:53 +02:00
* initially load all media and home screen data
* /
2021-06-06 17:54:19 +02:00
suspend fun initialLoading ( ) {
coroutineScope {
launch { loadHome ( ) }
launch { listAnimes ( ) }
}
}
2020-10-19 19:59:53 +02:00
2021-08-31 19:47:18 +02:00
/ * *
* get a media by it ' s ID ( int )
* @param aodId The AoD ID of the requested media
* @return returns a AoDMedia of type Movie or TVShow if found , else return AoDMediaNone
* /
2021-09-04 13:33:46 +02:00
suspend fun getMediaById ( aodId : Int ) : AoDMedia {
2021-08-31 19:47:18 +02:00
return aodMediaList . firstOrNull { it . aodId == aodId } ?:
try {
loadMediaAsync ( aodId ) . await ( ) . apply {
aodMediaList . add ( this )
}
} catch ( exn : NullPointerException ) {
2021-09-05 13:43:27 +02:00
Log . e ( AoDParser :: class . jvmName , " Error while loading media $aodId " , exn )
2021-08-31 19:47:18 +02:00
AoDMediaNone
}
}
2021-04-17 20:59:37 +02:00
/ * *
* get subscription info from aod website , remove " Anime-Abo " Prefix and trim
* /
2021-06-06 17:54:19 +02:00
suspend fun getSubscriptionInfoAsync ( ) : Deferred < String > {
return coroutineScope {
async ( Dispatchers . IO ) {
val res = Jsoup . connect ( baseUrl + subscriptionPath )
2021-09-05 13:43:27 +02:00
. cookieStore ( cookieStore )
2021-06-06 17:54:19 +02:00
. get ( )
return @async res . select ( " a:contains(Anime-Abo) " ) . text ( )
. removePrefix ( " Anime-Abo " ) . trim ( )
}
2021-04-17 20:59:37 +02:00
}
}
fun getSubscriptionUrl ( ) : String {
return baseUrl + subscriptionPath
}
2021-09-04 13:33:46 +02:00
suspend fun markAsWatched ( aodId : Int , episodeId : Int ) {
val episode = getMediaById ( aodId ) . getEpisodeById ( episodeId )
2021-01-01 13:08:25 +01:00
episode . watched = true
sendCallback ( episode . watchedCallback )
2021-09-05 13:43:27 +02:00
Log . d ( AoDParser :: class . jvmName , " Marked episode ${episode.mediaId} as watched " )
2021-01-01 13:08:25 +01:00
}
2020-10-19 19:59:53 +02:00
// TODO don't use jsoup here
2021-06-06 17:54:19 +02:00
private suspend fun sendCallback ( callbackPath : String ) = coroutineScope {
launch ( Dispatchers . IO ) {
val headers = mutableMapOf (
Pair ( " Accept " , " application/json, text/javascript, */*; q=0.01 " ) ,
Pair ( " Accept-Language " , " de,en-US;q=0.7,en;q=0.3 " ) ,
Pair ( " Accept-Encoding " , " gzip, deflate, br " ) ,
Pair ( " X-CSRF-Token " , csrfToken ) ,
Pair ( " X-Requested-With " , " XMLHttpRequest " ) ,
)
2020-10-19 19:59:53 +02:00
2021-06-06 17:54:19 +02:00
try {
Jsoup . connect ( baseUrl + callbackPath )
. ignoreContentType ( true )
2021-09-05 13:43:27 +02:00
. cookieStore ( cookieStore )
2021-06-06 17:54:19 +02:00
. headers ( headers )
. execute ( )
} catch ( ex : IOException ) {
2021-09-05 13:43:27 +02:00
Log . e ( AoDParser :: class . jvmName , " Callback for $callbackPath failed. " , ex )
2021-06-06 17:54:19 +02:00
}
}
2020-10-19 19:59:53 +02:00
}
/ * *
* load all media from aod into itemMediaList and mediaList
2021-06-06 17:54:19 +02:00
* TODO private suspend fun listAnimes ( ) = withContext ( Dispatchers . IO ) should also work , maybe a bug in android studio ?
2020-10-19 19:59:53 +02:00
* /
2021-06-06 17:54:19 +02:00
private suspend fun listAnimes ( ) = withContext ( Dispatchers . IO ) {
launch ( Dispatchers . IO ) {
val resAnimes = Jsoup . connect ( baseUrl + libraryPath ) . get ( )
//println(resAnimes)
2021-07-25 19:15:31 +02:00
guiMediaList . clear ( )
val animes = resAnimes . select ( " div.animebox " )
guiMediaList . addAll (
animes . map {
ItemMedia (
id = it . select ( " p.animebox-link " ) . select ( " a " )
. attr ( " href " ) . substringAfterLast ( " / " ) . toInt ( ) ,
title = it . select ( " h3.animebox-title " ) . text ( ) ,
posterUrl = it . select ( " p.animebox-image " ) . select ( " img " )
. attr ( " src " )
)
}
)
2021-09-05 13:43:27 +02:00
Log . i ( AoDParser :: class . jvmName , " Total library size is: ${guiMediaList.size} " )
2021-06-06 17:54:19 +02:00
}
2020-10-09 15:18:52 +02:00
}
2020-10-08 22:20:20 +02:00
2020-10-19 19:59:53 +02:00
/ * *
2020-12-11 10:54:40 +01:00
* load new episodes , titles and highlights
2020-10-19 19:59:53 +02:00
* /
2021-06-06 17:54:19 +02:00
private suspend fun loadHome ( ) = withContext ( Dispatchers . IO ) {
launch ( Dispatchers . IO ) {
val resHome = Jsoup . connect ( baseUrl ) . get ( )
// get highlights from AoD
highlightsList . clear ( )
resHome . select ( " #aod-highlights " ) . select ( " div.news-item " ) . forEach {
val mediaId = it . select ( " div.news-item-text " ) . select ( " a.serienlink " )
. attr ( " href " ) . substringAfterLast ( " / " ) . toIntOrNull ( )
val mediaTitle = it . select ( " div.news-title " ) . select ( " h2 " ) . text ( )
val mediaImage = it . select ( " img " ) . attr ( " src " )
if ( mediaId != null ) {
highlightsList . add ( ItemMedia ( mediaId , mediaTitle , mediaImage ) )
}
2020-12-06 15:18:15 +01:00
}
2021-06-06 17:54:19 +02:00
// get all new episodes from AoD
newEpisodesList . clear ( )
resHome . select ( " h2:contains(Neue Episoden) " ) . next ( ) . select ( " li " ) . forEach {
val mediaId = it . select ( " a.thumbs " ) . attr ( " href " )
. substringAfterLast ( " / " ) . toIntOrNull ( )
val mediaImage = it . select ( " a.thumbs > img " ) . attr ( " src " )
val mediaTitle = " ${it.select("a").text()} - ${it.select("span.neweps").text()} "
2020-12-11 10:54:40 +01:00
2021-06-06 17:54:19 +02:00
if ( mediaId != null ) {
newEpisodesList . add ( ItemMedia ( mediaId , mediaTitle , mediaImage ) )
}
2020-12-11 10:54:40 +01:00
}
2021-06-06 17:54:19 +02:00
// get new simulcasts from AoD
newSimulcastsList . clear ( )
resHome . select ( " h2:contains(Neue Simulcasts) " ) . next ( ) . select ( " li " ) . forEach {
val mediaId = it . select ( " a.thumbs " ) . attr ( " href " )
. substringAfterLast ( " / " ) . toIntOrNull ( )
val mediaImage = it . select ( " a.thumbs > img " ) . attr ( " src " )
val mediaTitle = it . select ( " a " ) . text ( )
2020-12-11 10:54:40 +01:00
2021-06-06 17:54:19 +02:00
if ( mediaId != null ) {
newSimulcastsList . add ( ItemMedia ( mediaId , mediaTitle , mediaImage ) )
}
2020-12-11 10:54:40 +01:00
}
2021-06-06 17:54:19 +02:00
// get new titles from AoD
newTitlesList . clear ( )
resHome . select ( " h2:contains(Neue Anime-Titel) " ) . next ( ) . select ( " li " ) . forEach {
val mediaId = it . select ( " a.thumbs " ) . attr ( " href " )
. substringAfterLast ( " / " ) . toIntOrNull ( )
val mediaImage = it . select ( " a.thumbs > img " ) . attr ( " src " )
val mediaTitle = it . select ( " a " ) . text ( )
2020-12-11 10:54:40 +01:00
2021-06-06 17:54:19 +02:00
if ( mediaId != null ) {
newTitlesList . add ( ItemMedia ( mediaId , mediaTitle , mediaImage ) )
}
2020-12-11 10:54:40 +01:00
}
2021-06-06 17:54:19 +02:00
// get top ten from AoD
topTenList . clear ( )
resHome . select ( " h2:contains(Anime Top 10) " ) . next ( ) . select ( " li " ) . forEach {
val mediaId = it . select ( " a.thumbs " ) . attr ( " href " )
. substringAfterLast ( " / " ) . toIntOrNull ( )
val mediaImage = it . select ( " a.thumbs > img " ) . attr ( " src " )
val mediaTitle = it . select ( " a " ) . text ( )
2021-01-21 18:22:53 +01:00
2021-06-06 17:54:19 +02:00
if ( mediaId != null ) {
topTenList . add ( ItemMedia ( mediaId , mediaTitle , mediaImage ) )
}
2021-01-21 18:22:53 +01:00
}
2021-06-06 17:54:19 +02:00
// if highlights is empty, add a random new title
if ( highlightsList . isEmpty ( ) ) {
if ( newTitlesList . isNotEmpty ( ) ) {
highlightsList . add ( newTitlesList [ Random . nextInt ( 0 , newTitlesList . size ) ] )
} else {
highlightsList . add ( ItemMedia ( 0 , " " , " " ) )
}
2020-12-18 17:23:04 +01:00
}
2021-06-06 17:54:19 +02:00
2021-09-05 13:43:27 +02:00
Log . i ( AoDParser :: class . jvmName , " loaded home " )
2020-10-19 17:34:41 +02:00
}
}
2020-10-11 10:02:00 +02:00
/ * *
2021-04-17 20:59:37 +02:00
* TODO catch SocketTimeoutException from loading to show a waring dialog
2021-09-04 13:33:46 +02:00
* Load media async . Every media has a playlist .
* @param aodId The AoD ID of the requested media
2020-10-11 10:02:00 +02:00
* /
2021-08-31 19:47:18 +02:00
private suspend fun loadMediaAsync ( aodId : Int ) : Deferred < AoDMedia > = coroutineScope {
return @coroutineScope async ( Dispatchers . IO ) {
2021-09-05 13:43:27 +02:00
if ( cookieStore . cookies . isEmpty ( ) ) login ( ) // TODO is this needed?
2021-08-31 19:47:18 +02:00
// return none object, if login wasn't successful
if ( ! loginSuccess ) {
2021-09-05 13:43:27 +02:00
Log . w ( AoDParser :: class . jvmName , " Login was not successful " )
2021-08-31 19:47:18 +02:00
return @async AoDMediaNone
}
// get the media page
val res = Jsoup . connect ( " $baseUrl /anime/ $aodId " )
2021-09-05 13:43:27 +02:00
. cookieStore ( cookieStore )
2021-08-31 19:47:18 +02:00
. get ( )
// println(res)
if ( csrfToken . isEmpty ( ) ) {
csrfToken = res . select ( " meta[name=csrf-token] " ) . attr ( " content " )
2021-09-05 13:43:27 +02:00
Log . d ( AoDParser :: class . jvmName , " New csrf token is $csrfToken " )
2021-08-31 19:47:18 +02:00
}
2021-09-04 13:33:46 +02:00
// playlist parsing TODO can this be async to the general info parsing?
2021-09-05 13:43:27 +02:00
val besides = res . select ( " div.besides " ) . first ( ) !!
2021-08-31 19:47:18 +02:00
val aodPlaylists = besides . select ( " input.streamstarter_html5 " ) . map { streamstarter ->
parsePlaylistAsync (
streamstarter . attr ( " data-playlist " ) ,
streamstarter . attr ( " data-lang " )
)
}
/ * *
* generic aod media data
* /
val title = res . select ( " h1[itemprop=name] " ) . text ( )
val description = res . select ( " div[itemprop=description] " ) . text ( )
val posterURL = res . select ( " img.fullwidth-image " ) . attr ( " src " )
val type = when {
posterURL . contains ( " films " ) -> MediaType . MOVIE
posterURL . contains ( " series " ) -> MediaType . TVSHOW
else -> MediaType . OTHER
}
var year = 0
var age = 0
res . select ( " table.vertical-table " ) . select ( " tr " ) . forEach { row ->
when ( row . select ( " th " ) . text ( ) . lowercase ( Locale . ROOT ) ) {
" produktionsjahr " -> year = row . select ( " td " ) . text ( ) . toInt ( )
" fsk " -> age = row . select ( " td " ) . text ( ) . toInt ( )
}
}
// similar titles from media page
val similar = res . select ( " h2:contains(Ähnliche Animes) " ) . next ( ) . select ( " li " ) . mapNotNull {
val mediaId = it . select ( " a.thumbs " ) . attr ( " href " )
. substringAfterLast ( " / " ) . toIntOrNull ( )
val mediaImage = it . select ( " a.thumbs > img " ) . attr ( " src " )
val mediaTitle = it . select ( " a " ) . text ( )
if ( mediaId != null ) {
ItemMedia ( mediaId , mediaTitle , mediaImage )
} else {
2021-09-05 13:43:27 +02:00
Log . i ( AoDParser :: class . jvmName , " MediaId for similar to $aodId was null " )
2021-08-31 19:47:18 +02:00
null
}
}
/ * *
* additional information for episodes :
* description : a short description of the episode
* watched : indicates if the episodes has been watched
* watched callback : url to set watched in aod
* /
val episodesInfo : Map < Int , AoDEpisodeInfo > = if ( type == MediaType . TVSHOW ) {
res . select ( " div.three-box-container > div.episodebox " ) . mapNotNull { episodeBox ->
// make sure the episode has a streaming link
if ( episodeBox . select ( " input.streamstarter_html5 " ) . isNotEmpty ( ) ) {
val mediaId = episodeBox . select ( " div.flip-front " ) . attr ( " id " ) . substringAfter ( " - " ) . toInt ( )
val episodeShortDesc = episodeBox . select ( " p.episodebox-shorttext " ) . text ( )
val episodeWatched = episodeBox . select ( " div.episodebox-icons > div " ) . hasClass ( " status-icon-orange " )
val episodeWatchedCallback = episodeBox . select ( " input.streamstarter_html5 " ) . eachAttr ( " data-playlist " ) . first ( )
AoDEpisodeInfo ( mediaId , episodeShortDesc , episodeWatched , episodeWatchedCallback )
} else {
2021-09-05 13:43:27 +02:00
Log . i ( AoDParser :: class . jvmName , " Episode info for $aodId has empty streamstarter_html5 " )
2021-08-31 19:47:18 +02:00
null
}
} . associateBy { it . aodMediaId }
} else {
mapOf ( )
}
2021-09-04 13:33:46 +02:00
// map the aod api playlist to a teapod playlist
2021-08-31 19:47:18 +02:00
val playlist : List < AoDEpisode > = aodPlaylists . awaitAll ( ) . flatMap { aodPlaylist ->
aodPlaylist . list . mapIndexed { index , episode ->
AoDEpisode (
mediaId = episode . mediaid ,
title = episode . title ,
description = episode . description ,
shortDesc = episodesInfo [ episode . mediaid ] ?. shortDesc ?: " " ,
imageURL = episode . image ,
2021-09-05 00:08:03 +02:00
numberStr = episode . title . substringAfter ( " , Ep. " , " " ) , // TODO move to parsePalylist
index = index ,
2021-08-31 19:47:18 +02:00
watched = episodesInfo [ episode . mediaid ] ?. watched ?: false ,
watchedCallback = episodesInfo [ episode . mediaid ] ?. watchedCallback ?: " " ,
streams = mutableListOf ( Stream ( episode . sources . first ( ) . file , aodPlaylist . language ) )
)
}
} . groupingBy { it . mediaId } . reduce { _ , accumulator , element ->
accumulator . copy ( ) . also {
it . streams . addAll ( element . streams )
}
} . values . toList ( )
return @async AoDMedia (
aodId = aodId ,
type = type ,
title = title ,
shortText = description ,
posterURL = posterURL ,
year = year ,
age = age ,
similar = similar ,
playlist = playlist
)
}
}
2020-10-20 20:07:59 +02:00
/ * *
* don ' t use Gson ( ) . fromJson ( ) as we don ' t have any control over the api and it may change
* /
2021-07-25 19:15:31 +02:00
private fun parsePlaylistAsync ( playlistPath : String , language : String ) : Deferred < AoDPlaylist > {
2020-10-19 21:57:02 +02:00
if ( playlistPath == " [] " ) {
2021-07-25 19:30:25 +02:00
return CompletableDeferred ( AoDPlaylist ( listOf ( ) , Locale . ROOT ) )
2020-10-19 21:57:02 +02:00
}
2021-06-06 17:54:19 +02:00
return CoroutineScope ( Dispatchers . IO ) . async ( Dispatchers . IO ) {
2020-10-09 15:18:52 +02:00
val headers = mutableMapOf (
Pair ( " Accept " , " application/json, text/javascript, */*; q=0.01 " ) ,
Pair ( " Accept-Language " , " de,en-US;q=0.7,en;q=0.3 " ) ,
Pair ( " Accept-Encoding " , " gzip, deflate, br " ) ,
Pair ( " X-CSRF-Token " , csrfToken ) ,
Pair ( " X-Requested-With " , " XMLHttpRequest " ) ,
)
2020-10-16 18:24:34 +02:00
//println("loading streaminfo with cstf: $csrfToken")
2020-10-13 23:47:48 +02:00
2020-10-12 20:30:45 +02:00
val res = Jsoup . connect ( baseUrl + playlistPath )
2020-10-09 15:18:52 +02:00
. ignoreContentType ( true )
2021-09-05 13:43:27 +02:00
. cookieStore ( cookieStore )
2020-10-09 15:18:52 +02:00
. headers ( headers )
2021-03-13 22:08:53 +01:00
. timeout ( 120000 ) // loading the playlist can take some time
2020-10-09 15:18:52 +02:00
. execute ( )
2020-10-20 20:07:59 +02:00
//Gson().fromJson(res.body(), AoDObject::class.java)
2021-07-25 19:15:31 +02:00
return @async AoDPlaylist ( JsonParser . parseString ( res . body ( ) ) . asJsonObject
2020-10-20 20:07:59 +02:00
. get ( " playlist " ) . asJsonArray . map {
Playlist (
sources = it . asJsonObject . get ( " sources " ) . asJsonArray . map { source ->
Source ( source . asJsonObject . get ( " file " ) . asString )
} ,
image = it . asJsonObject . get ( " image " ) . asString ,
title = it . asJsonObject . get ( " title " ) . asString ,
description = it . asJsonObject . get ( " description " ) . asString ,
mediaid = it . asJsonObject . get ( " mediaid " ) . asInt
)
2020-12-26 14:39:35 +01:00
} ,
2021-07-25 19:30:25 +02:00
// TODO improve language handling (via display language etc.)
language = when ( language ) {
" ger " -> Locale . GERMAN
" jap " -> Locale . JAPANESE
else -> Locale . ROOT
}
2020-12-26 14:39:35 +01:00
)
2020-10-08 22:20:20 +02:00
}
}
2020-10-11 10:02:00 +02:00
}