Browse Source

add metadb support for crunchyroll

also remove gson snice it's unused now
pull/54/head
Jannik 3 months ago
parent
commit
7fbf639a70
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
  1. 1
      app/build.gradle
  2. 4
      app/proguard-rules.pro
  3. 16
      app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt
  4. 2
      app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt
  5. 4
      app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt
  6. 1
      app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt
  7. 69
      app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt
  8. 2
      app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt
  9. 159
      app/src/main/java/org/mosad/teapod/util/MetaDBController.kt
  10. 4
      app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt
  11. 57
      app/src/main/java/org/mosad/teapod/util/metadb/DatTypes.kt
  12. 88
      app/src/main/java/org/mosad/teapod/util/metadb/MetaDBController.kt

1
app/build.gradle

@ -60,7 +60,6 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.code.gson:gson:2.8.8' // TODO remove, still used by metadb
implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.15.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.15.0'

4
app/proguard-rules.pro vendored

@ -24,10 +24,6 @@
-keep class org.json.** { *; }
#Gson
-keepattributes Signature
-dontwarn sun.misc.**
# kotlinx.serialization
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.

16
app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt

@ -45,6 +45,7 @@ import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
import org.mosad.teapod.ui.activity.player.PlayerActivity
import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.metadb.MetaDBController
import java.util.*
import kotlin.system.measureTimeMillis
@ -141,6 +142,9 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
Preferences.load(this)
EncryptedPreferences.readCredentials(this)
// load meta db at the start, it doesn't depend on any third party
val metaJob = initMetaDB()
// always initialize the api token
Crunchyroll.initBasicApiToken()
@ -152,14 +156,17 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
) {
showOnboarding()
} else {
runBlocking { initCrunchyroll().joinAll() }
runBlocking {
initCrunchyroll().joinAll()
metaJob.join() // meta loading should be done here
}
}
}
Log.i(classTag, "loading in $time ms")
}
private fun initCrunchyroll(): List<Job> {
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
return listOf(
scope.launch { Crunchyroll.index() },
scope.launch { Crunchyroll.account() },
@ -172,6 +179,11 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
)
}
private fun initMetaDB(): Job {
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialMetaDBLoading"))
return scope.launch { MetaDBController.list() }
}
private fun showLoginDialog() {
LoginDialog(this, false).positiveButton {
EncryptedPreferences.saveCredentials(login, password, context)

2
app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt

@ -107,8 +107,6 @@ class AboutFragment : Fragment() {
"https://github.com/material-components/material-components-android", License.APACHE2),
ThirdPartyComponent("ExoPlayer", "2014 - 2020", "The Android Open Source Project",
"https://github.com/google/ExoPlayer", License.APACHE2),
ThirdPartyComponent("Gson", "2008", "Google Inc.",
"https://github.com/google/gson", License.APACHE2),
ThirdPartyComponent("Material design icons", "2020", "Google Inc.",
"https://github.com/google/material-design-icons", License.APACHE2),
ThirdPartyComponent("Material Dialogs", "", "Aidan Follestad",

4
app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt

@ -8,7 +8,7 @@ import kotlinx.coroutines.launch
import org.mosad.teapod.parser.crunchyroll.*
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.Meta
import org.mosad.teapod.util.metadb.Meta
import org.mosad.teapod.util.tmdb.*
/**
@ -59,7 +59,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
).joinAll()
// println("series: $seriesCrunchy")
// println("seasons: $seasonsCrunchy")
println(upNextSeries)
// println(upNextSeries)
// load the preferred season (preferred language, language per season, not per stream)
currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocale)

1
app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt

@ -425,7 +425,6 @@ class PlayerActivity : AppCompatActivity() {
val seekTime = (it.openingStart + it.openingDuration) - model.player.currentPosition
model.seekToOffset(seekTime)
}
}
/**

69
app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt

@ -46,8 +46,10 @@ import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
import org.mosad.teapod.parser.crunchyroll.NonePlayback
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.EpisodeMeta
import org.mosad.teapod.util.tmdb.TMDBTVSeason
import org.mosad.teapod.util.metadb.EpisodeMeta
import org.mosad.teapod.util.metadb.Meta
import org.mosad.teapod.util.metadb.MetaDBController
import org.mosad.teapod.util.metadb.TVShowMeta
import java.util.*
/**
@ -56,6 +58,7 @@ import java.util.*
* the next episode will be update and the callback is handled.
*/
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
private val classTag = javaClass.name
val player = SimpleExoPlayer.Builder(application).build()
private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
@ -65,13 +68,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
private var currentPlayhead: Long = 0
// tmdb/meta data
// TODO meta data currently not implemented for cr
// var mediaMeta: Meta? = null
// internal set
var tmdbTVSeason: TMDBTVSeason? =null
var mediaMeta: Meta? = null
internal set
var currentEpisodeMeta: EpisodeMeta? = null
internal set
// var tmdbTVSeason: TMDBTVSeason? =null
// internal set
// crunchyroll episodes/playback
var episodes = NoneEpisodes
@ -108,7 +110,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
mediaSession.release()
player.release()
Log.d(javaClass.name, "Released player")
Log.d(classTag, "Released player")
}
/**
@ -124,22 +126,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch {
episodes = Crunchyroll.episodes(seasonId)
mediaMeta = loadMediaMeta(episodes.items.first().seriesId)
Log.d(classTag, "meta: $mediaMeta")
setCurrentEpisode(episodeId)
playCurrentMedia(currentPlayhead)
// TODO reimplement for cr
// run async as it should be loaded by the time the episodes a
// viewModelScope.launch {
// // get tmdb season info, if metaDB knows the tv show
// if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) {
// val tvShowMeta = mediaMeta as TVShowMeta
// tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
// }
// }
//
// currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisodeAoD.mediaId)
// currentLanguage = currentEpisodeAoD.getPreferredStream(preferredLanguage).language
}
fun setLanguage(language: Locale) {
@ -174,6 +166,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
episode.id == episodeId
} ?: NoneEpisode
// update current episode meta
currentEpisodeMeta = if (mediaMeta is TVShowMeta && currentEpisode.episodeNumber != null) {
(mediaMeta as TVShowMeta)
.seasons[currentEpisode.seasonNumber - 1]
.episodes[currentEpisode.episodeNumber!! - 1]
} else {
null
}
// update player gui (title, next ep button) after currentEpisode has changed
currentEpisodeChangedListener.forEach { it() }
@ -195,9 +196,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
}
)
}
println("loaded playback ${currentEpisode.playback}")
// TODO update metadata and language (it should not be needed to update the language here!)
Log.i(classTag, "playback: ${currentEpisode.playback}")
if (startPlayback) {
playCurrentMedia()
@ -227,7 +226,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
currentPlayback.streams.adaptive_hls.entries.first().value.url
}
}
println("stream url: $url")
Log.d(classTag, "stream url: $url")
// create the media source object
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
@ -266,25 +265,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
return episodes.items.lastOrNull()?.id == currentEpisode.id
}
// TODO reimplement for cr
// fun getEpisodeMetaByAoDMediaId(aodMediaId: Int): EpisodeMeta? {
// val meta = mediaMeta
// return if (meta is TVShowMeta) {
// meta.episodes.firstOrNull { it.aodMediaId == aodMediaId }
// } else {
// null
// }
// }
//
// private suspend fun loadMediaMeta(aodId: Int): Meta? {
// return if (media.type == DataTypes.MediaType.TVSHOW) {
// MetaDBController().getTVShowMetadata(aodId)
// } else {
// null
// }
//
// return null
// }
private suspend fun loadMediaMeta(crSeriesId: String): Meta? {
return MetaDBController.getTVShowMetadata(crSeriesId)
}
/**
* Update the playhead of the current episode, if currentPosition > 1000ms.

2
app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt

@ -28,7 +28,7 @@ class EpisodesListPlayer @JvmOverloads constructor(
}
model?.let {
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodes, model.tmdbTVSeason?.episodes)
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodes)
adapterRecEpisodes.onImageClick = {_, episodeId ->
(this.parent as ViewGroup).removeView(this)
model.setCurrentEpisode(episodeId, startPlayback = true)

159
app/src/main/java/org/mosad/teapod/util/MetaDBController.kt

@ -1,159 +0,0 @@
/**
* Teapod
*
* Copyright 2020-2022 <seil0@mosad.xyz>
*
* 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.
*
*/
package org.mosad.teapod.util
import android.util.Log
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.*
import java.io.FileNotFoundException
import java.net.URL
/**
* TODO remove gson usage
*/
class MetaDBController {
companion object {
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/aod/"
var mediaList = MediaList(listOf())
private var metaCacheList = arrayListOf<Meta>()
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun list() = withContext(Dispatchers.IO) {
val url = URL("$repoUrl/list.json")
val json = url.readText()
mediaList = Gson().fromJson(json, MediaList::class.java)
}
}
/**
* Get the meta data for a movie from MetaDB
* @param aodId The AoD id of the media
* @return A meta movie object, or null if not found
*/
suspend fun getMovieMetadata(aodId: Int): MovieMeta? {
return metaCacheList.firstOrNull {
it.aodId == aodId
} as MovieMeta? ?: getMovieMetadataFromDB(aodId)
}
/**
* Get the meta data for a tv show from MetaDB
* @param aodId The AoD id of the media
* @return A meta tv show object, or null if not found
*/
suspend fun getTVShowMetadata(aodId: Int): TVShowMeta? {
return metaCacheList.firstOrNull {
it.aodId == aodId
} as TVShowMeta? ?: getTVShowMetadataFromDB(aodId)
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun getMovieMetadataFromDB(aodId: Int): MovieMeta? = withContext(Dispatchers.IO) {
val url = URL("$repoUrl/movie/$aodId/media.json")
return@withContext try {
val json = url.readText()
val meta = Gson().fromJson(json, MovieMeta::class.java)
metaCacheList.add(meta)
meta
} catch (ex: FileNotFoundException) {
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
null
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun getTVShowMetadataFromDB(aodId: Int): TVShowMeta? = withContext(Dispatchers.IO) {
val url = URL("$repoUrl/tv/$aodId/media.json")
return@withContext try {
val json = url.readText()
val meta = Gson().fromJson(json, TVShowMeta::class.java)
metaCacheList.add(meta)
meta
} catch (ex: FileNotFoundException) {
Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex)
null
}
}
}
// class representing the media list json object
data class MediaList(
val media: List<Int>
)
// abstract class used for meta data objects (tv, movie)
abstract class Meta {
abstract val id: Int
abstract val aodId: Int
abstract val tmdbId: Int
}
// class representing the movie json object
data class MovieMeta(
override val id: Int,
@SerializedName("aod_id")
override val aodId: Int,
@SerializedName("tmdb_id")
override val tmdbId: Int
): Meta()
// class representing the tv show json object
data class TVShowMeta(
override val id: Int,
@SerializedName("aod_id")
override val aodId: Int,
@SerializedName("tmdb_id")
override val tmdbId: Int,
@SerializedName("tmdb_season_id")
val tmdbSeasonId: Int,
@SerializedName("tmdb_season_number")
val tmdbSeasonNumber: Int,
@SerializedName("episodes")
val episodes: List<EpisodeMeta>
): Meta()
// class used in TVShowMeta, part of the tv show json object
data class EpisodeMeta(
val id: Int,
@SerializedName("aod_media_id")
val aodMediaId: Int,
@SerializedName("tmdb_id")
val tmdbId: Int,
@SerializedName("tmdb_number")
val tmdbNumber: Int,
@SerializedName("opening_start")
val openingStart: Long,
@SerializedName("opening_duration")
val openingDuration: Long,
@SerializedName("ending_start")
val endingStart: Long,
@SerializedName("ending_duration")
val endingDuration: Long
)

4
app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt

@ -12,7 +12,7 @@ import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
import org.mosad.teapod.parser.crunchyroll.Episodes
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
class PlayerEpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
class PlayerEpisodeItemAdapter(private val episodes: Episodes) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
var currentSelected: Int = -1 // -1, since position should never be < 0
@ -39,8 +39,6 @@ class PlayerEpisodeItemAdapter(private val episodes: Episodes, private val tmdbE
holder.binding.textEpisodeTitle2.text = titleText
holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) {
ep.description
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
tmdbEpisodes[position].overview
} else {
""
}

57
app/src/main/java/org/mosad/teapod/util/metadb/DatTypes.kt

@ -0,0 +1,57 @@
package org.mosad.teapod.util.metadb
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// class representing the media list json object
@Serializable
data class MediaList(
@SerialName("media") val media: List<String>
)
// abstract class used for meta data objects (tv, movie)
abstract class Meta {
abstract val id: Int
abstract val tmdbId: Int
abstract val crSeriesId: String
}
// class representing the movie json object
@Serializable
data class MovieMeta(
@SerialName("id") override val id: Int,
@SerialName("tmdb_id") override val tmdbId: Int,
@SerialName("cr_series_id") override val crSeriesId: String,
): Meta()
// class representing the tv show json object
@Serializable
data class TVShowMeta(
@SerialName("id") override val id: Int,
@SerialName("tmdb_id") override val tmdbId: Int,
@SerialName("cr_series_id") override val crSeriesId: String,
@SerialName("seasons") val seasons: List<SeasonMeta>,
): Meta()
// class used in TVShowMeta, part of the tv show json object
@Serializable
data class SeasonMeta(
@SerialName("id") val id: Int,
@SerialName("tmdb_season_id") val tmdbSeasonId: Int,
@SerialName("tmdb_season_number") val tmdbSeasonNumber: Int,
@SerialName("cr_season_ids") val crSeasonIds: List<String>,
@SerialName("episodes") val episodes: List<EpisodeMeta>,
)
// class used in TVShowMeta, part of the tv show json object
@Serializable
data class EpisodeMeta(
@SerialName("id") val id: Int,
@SerialName("tmdb_episode_id") val tmdbEpisodeId: Int,
@SerialName("tmdb_episode_number") val tmdbEpisodeNumber: Int,
@SerialName("cr_episode_ids") val crEpisodeIds: List<String>,
@SerialName("opening_start") val openingStart: Long,
@SerialName("opening_duration") val openingDuration: Long,
@SerialName("ending_start") val endingStart: Long,
@SerialName("ending_duration") val endingDuration: Long
)

88
app/src/main/java/org/mosad/teapod/util/metadb/MetaDBController.kt

@ -0,0 +1,88 @@
/**
* Teapod
*
* Copyright 2020-2022 <seil0@mosad.xyz>
*
* 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.
*
*/
package org.mosad.teapod.util.metadb
import android.util.Log
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
object MetaDBController {
private val TAG = javaClass.name
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/crunchy/"
private val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(Json)
}
}
private var mediaList = MediaList(listOf())
private var metaCacheList = arrayListOf<Meta>()
suspend fun list() = withContext(Dispatchers.IO) {
val raw: String = client.get("$repoUrl/list.json")
mediaList = Json.decodeFromString(raw)
}
/**
* Get the meta data for a movie from MetaDB
* @param crSeriesId The crunchyroll media id
* @return A meta object, or null if not found
*/
suspend fun getTVShowMetadata(crSeriesId: String): TVShowMeta? {
return if (mediaList.media.contains(crSeriesId)) {
metaCacheList.firstOrNull {
it.crSeriesId == crSeriesId
} as TVShowMeta? ?: getTVShowMetadataFromDB(crSeriesId)
} else {
null
}
}
private suspend fun getTVShowMetadataFromDB(crSeriesId: String): TVShowMeta? = withContext(Dispatchers.IO) {
return@withContext try {
val raw: String = client.get("$repoUrl/tv/$crSeriesId/media.json")
val meta: TVShowMeta = Json.decodeFromString(raw)
metaCacheList.add(meta)
meta
} catch (ex: ClientRequestException) {
when (ex.response.status) {
HttpStatusCode.NotFound -> Log.w(TAG, "The requested file was not found. Series ID: $crSeriesId", ex)
else -> Log.e(TAG, "Error while requesting meta data. Series ID: $crSeriesId", ex)
}
null // todo return none object
}
}
}
Loading…
Cancel
Save

Du besuchst diese Seite mit einem veralteten IPv4-Internetzugang. Möglicherweise treten in Zukunft Probleme mit der Erreichbarkeit und Performance auf. Bitte frage deinen Internetanbieter oder Netzwerkadministrator nach IPv6-Unterstützung.
You are visiting this site with an outdated IPv4 internet access. You may experience problems with accessibility and performance in the future. Please ask your ISP or network administrator for IPv6 support.
Weitere Infos | More Information
Klicke zum schließen | Click to close