5 Commits

Author SHA1 Message Date
7fbf639a70 add metadb support for crunchyroll
also remove gson snice it's unused now
2022-03-29 22:39:16 +02:00
ff63b3d7a4 update gradle wrapper & core-splashscreen
* wrapper 7.3.3 -> 7.4.1
* core-splashscreen 1.0.0-beta01 -> 1.0.0-beta02
2022-03-29 22:39:02 +02:00
7d32cecd89 hide unused dev settings 2022-03-20 12:56:01 +01:00
72280f29d8 add option to disable playhead updates/reporting 2022-03-20 12:38:49 +01:00
cd4cfb7a0c update libraries & targetSdk; use core-splashscreen for splashscreen
* targetSdk 30 -> 31
* core-ktx 1.6.0 -> 1.7.0
* appcompat 1.3.1 -> 1.4.1
* constraintlayout 2.1.0 -> 2.1.3
* navigation-fragment-ktx 2.3.5 -> 2.4.1
* navigation-ui-ktx 2.3.5 -> 2.4.1
* lifecycle-runtime-ktx 2.3.5 -> 2.4.1
* lifecycle-viewmodel-ktx 2.3.5 -> 2.4.1
* material 1.4.0 -> 1.5.0
2022-03-19 22:09:47 +01:00
32 changed files with 352 additions and 275 deletions

View File

@ -6,13 +6,13 @@ plugins {
} }
android { android {
compileSdkVersion 30 compileSdkVersion 31
buildToolsVersion "30.0.3" buildToolsVersion "30.0.3"
defaultConfig { defaultConfig {
applicationId "org.mosad.teapod" applicationId "org.mosad.teapod"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 31
versionCode 9000 //00.09.000 versionCode 9000 //00.09.000
versionName "1.0.0-beta1" versionName "1.0.0-beta1"
@ -48,18 +48,18 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1'
implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0' implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'com.google.android.material:material:1.4.0' 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-core:2.15.0'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.15.0' implementation 'com.google.android.exoplayer:exoplayer-hls:2.15.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.15.0' implementation 'com.google.android.exoplayer:exoplayer-dash:2.15.0'

View File

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

View File

@ -11,11 +11,10 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme.Dark"> android:theme="@style/Theme.App.Starting">
<activity <activity
android:name="org.mosad.teapod.ui.activity.SplashActivity" android:exported="true"
android:label="@string/app_name" android:name="org.mosad.teapod.ui.activity.main.MainActivity"
android:theme="@style/SplashTheme"
android:screenOrientation="portrait"> android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -23,22 +22,17 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:exported="false"
android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity" android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
android:label="@string/app_name"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:launchMode="singleTop" android:launchMode="singleTop"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">
</activity> </activity>
<activity <activity
android:name="org.mosad.teapod.ui.activity.main.MainActivity" android:exported="false"
android:label="@string/app_name"
android:screenOrientation="portrait">
</activity>
<activity
android:name="org.mosad.teapod.ui.activity.player.PlayerActivity" android:name="org.mosad.teapod.ui.activity.player.PlayerActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
android:autoRemoveFromRecents="true" android:autoRemoveFromRecents="true"
android:label="@string/app_name"
android:launchMode="singleTask" android:launchMode="singleTask"
android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity" android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"

View File

@ -500,6 +500,12 @@ object Crunchyroll {
} }
} }
/**
* Post the playhead to crunchy (playhead position,watched state)
*
* @param episodeId A episode ID as strings.
* @param playhead The episodes playhead in seconds.
*/
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.preferredLocale.toLanguageTag())

View File

@ -19,6 +19,10 @@ object Preferences {
var theme = DataTypes.Theme.DARK var theme = DataTypes.Theme.DARK
internal set internal set
// dev settings
var updatePlayhead = true
internal set
private fun getSharedPref(context: Context): SharedPreferences { private fun getSharedPref(context: Context): SharedPreferences {
return context.getSharedPreferences( return context.getSharedPreferences(
context.getString(R.string.preference_file_key), context.getString(R.string.preference_file_key),
@ -71,6 +75,15 @@ object Preferences {
this.theme = theme this.theme = theme
} }
fun saveUpdatePlayhead(context: Context, updatePlayhead: Boolean) {
with(getSharedPref(context).edit()) {
putBoolean(context.getString(R.string.save_key_update_playhead), updatePlayhead)
apply()
}
this.updatePlayhead = updatePlayhead
}
/** /**
* initially load the stored values * initially load the stored values
*/ */
@ -96,6 +109,11 @@ object Preferences {
context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString() context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString()
) ?: DataTypes.Theme.DARK.toString() ) ?: DataTypes.Theme.DARK.toString()
) )
// dev settings
updatePlayhead = sharedPref.getBoolean(
context.getString(R.string.save_key_update_playhead), true
)
} }

View File

@ -1,18 +0,0 @@
package org.mosad.teapod.ui.activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.mosad.teapod.ui.activity.main.MainActivity
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@ -27,6 +27,7 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
@ -44,6 +45,7 @@ import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
import org.mosad.teapod.ui.activity.player.PlayerActivity import org.mosad.teapod.ui.activity.player.PlayerActivity
import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.metadb.MetaDBController
import java.util.* import java.util.*
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@ -62,6 +64,9 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// Handle the splash screen transition.
installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
load() // start the initial loading load() // start the initial loading
@ -137,6 +142,9 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
Preferences.load(this) Preferences.load(this)
EncryptedPreferences.readCredentials(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 // always initialize the api token
Crunchyroll.initBasicApiToken() Crunchyroll.initBasicApiToken()
@ -148,14 +156,17 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
) { ) {
showOnboarding() showOnboarding()
} else { } else {
runBlocking { initCrunchyroll().joinAll() } runBlocking {
initCrunchyroll().joinAll()
metaJob.join() // meta loading should be done here
}
} }
} }
Log.i(classTag, "loading in $time ms") Log.i(classTag, "loading in $time ms")
} }
private fun initCrunchyroll(): List<Job> { private fun initCrunchyroll(): List<Job> {
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope")) val scope = CoroutineScope(Dispatchers.IO + CoroutineName("InitialCrunchyLoading"))
return listOf( return listOf(
scope.launch { Crunchyroll.index() }, scope.launch { Crunchyroll.index() },
scope.launch { Crunchyroll.account() }, scope.launch { Crunchyroll.account() },
@ -168,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() { private fun showLoginDialog() {
LoginDialog(this, false).positiveButton { LoginDialog(this, false).positiveButton {
EncryptedPreferences.saveCredentials(login, password, context) EncryptedPreferences.saveCredentials(login, password, context)

View File

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

View File

@ -93,6 +93,7 @@ class AccountFragment : Fragment() {
} }
binding.linearDevSettings.isVisible = Preferences.devSettings binding.linearDevSettings.isVisible = Preferences.devSettings
binding.switchUpdatePlayhead.isChecked = Preferences.updatePlayhead
binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time)) binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
@ -130,6 +131,10 @@ class AccountFragment : Fragment() {
activity?.showFragment(AboutFragment()) activity?.showFragment(AboutFragment())
} }
binding.switchUpdatePlayhead.setOnClickListener {
Preferences.saveUpdatePlayhead(requireContext(), binding.switchUpdatePlayhead.isChecked)
}
binding.linearExportData.setOnClickListener { binding.linearExportData.setOnClickListener {
val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)

View File

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

View File

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

View File

@ -46,8 +46,10 @@ import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
import org.mosad.teapod.parser.crunchyroll.NonePlayback import org.mosad.teapod.parser.crunchyroll.NonePlayback
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.EpisodeMeta import org.mosad.teapod.util.metadb.EpisodeMeta
import org.mosad.teapod.util.tmdb.TMDBTVSeason import org.mosad.teapod.util.metadb.Meta
import org.mosad.teapod.util.metadb.MetaDBController
import org.mosad.teapod.util.metadb.TVShowMeta
import java.util.* import java.util.*
/** /**
@ -56,6 +58,7 @@ import java.util.*
* the next episode will be update and the callback is handled. * the next episode will be update and the callback is handled.
*/ */
class PlayerViewModel(application: Application) : AndroidViewModel(application) { class PlayerViewModel(application: Application) : AndroidViewModel(application) {
private val classTag = javaClass.name
val player = SimpleExoPlayer.Builder(application).build() val player = SimpleExoPlayer.Builder(application).build()
private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod")) private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod"))
@ -65,13 +68,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
private var currentPlayhead: Long = 0 private var currentPlayhead: Long = 0
// tmdb/meta data // tmdb/meta data
// TODO meta data currently not implemented for cr var mediaMeta: Meta? = null
// var mediaMeta: Meta? = null
// internal set
var tmdbTVSeason: TMDBTVSeason? =null
internal set internal set
var currentEpisodeMeta: EpisodeMeta? = null var currentEpisodeMeta: EpisodeMeta? = null
internal set internal set
// var tmdbTVSeason: TMDBTVSeason? =null
// internal set
// crunchyroll episodes/playback // crunchyroll episodes/playback
var episodes = NoneEpisodes var episodes = NoneEpisodes
@ -108,7 +110,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
mediaSession.release() mediaSession.release()
player.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 { fun loadMediaAsync(seasonId: String, episodeId: String) = viewModelScope.launch {
episodes = Crunchyroll.episodes(seasonId) episodes = Crunchyroll.episodes(seasonId)
mediaMeta = loadMediaMeta(episodes.items.first().seriesId)
Log.d(classTag, "meta: $mediaMeta")
setCurrentEpisode(episodeId) setCurrentEpisode(episodeId)
playCurrentMedia(currentPlayhead) 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) { fun setLanguage(language: Locale) {
@ -174,6 +166,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
episode.id == episodeId episode.id == episodeId
} ?: NoneEpisode } ?: 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 // update player gui (title, next ep button) after currentEpisode has changed
currentEpisodeChangedListener.forEach { it() } currentEpisodeChangedListener.forEach { it() }
@ -195,9 +196,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
} }
) )
} }
println("loaded playback ${currentEpisode.playback}") Log.i(classTag, "playback: ${currentEpisode.playback}")
// TODO update metadata and language (it should not be needed to update the language here!)
if (startPlayback) { if (startPlayback) {
playCurrentMedia() playCurrentMedia()
@ -227,7 +226,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
currentPlayback.streams.adaptive_hls.entries.first().value.url currentPlayback.streams.adaptive_hls.entries.first().value.url
} }
} }
println("stream url: $url") Log.d(classTag, "stream url: $url")
// create the media source object // create the media source object
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
@ -266,25 +265,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
return episodes.items.lastOrNull()?.id == currentEpisode.id return episodes.items.lastOrNull()?.id == currentEpisode.id
} }
// TODO reimplement for cr private suspend fun loadMediaMeta(crSeriesId: String): Meta? {
// fun getEpisodeMetaByAoDMediaId(aodMediaId: Int): EpisodeMeta? { return MetaDBController.getTVShowMetadata(crSeriesId)
// 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
// }
/** /**
* Update the playhead of the current episode, if currentPosition > 1000ms. * Update the playhead of the current episode, if currentPosition > 1000ms.
@ -292,7 +275,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
private fun updatePlayhead() { private fun updatePlayhead() {
val playhead = (player.currentPosition / 1000) val playhead = (player.currentPosition / 1000)
if (playhead > 0) { if (playhead > 0 && Preferences.updatePlayhead) {
viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) } viewModelScope.launch { Crunchyroll.postPlayheads(currentEpisode.id, playhead.toInt()) }
Log.i(javaClass.name, "Set playhead for episode ${currentEpisode.id} to $playhead sec.") Log.i(javaClass.name, "Set playhead for episode ${currentEpisode.id} to $playhead sec.")
} }

View File

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

View File

@ -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
)

View File

@ -12,7 +12,7 @@ import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
import org.mosad.teapod.parser.crunchyroll.Episodes import org.mosad.teapod.parser.crunchyroll.Episodes
import org.mosad.teapod.util.tmdb.TMDBTVEpisode 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 onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
var currentSelected: Int = -1 // -1, since position should never be < 0 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.textEpisodeTitle2.text = titleText
holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) { holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) {
ep.description ep.description
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
tmdbEpisodes[position].overview
} else { } else {
"" ""
} }

View File

@ -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
)

View File

@ -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
}
}
}

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/black"/>
<item android:gravity="center" android:width="144dp" android:height="144dp">
<bitmap
android:gravity="fill_horizontal|fill_vertical"
android:src="@drawable/ic_splash_logo"/>
</item>
</layer-list>

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.03158203"
android:scaleY="0.03158203"
android:translateX="37.83"
android:translateY="44.778053">
<path
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
android:strokeLineJoin="miter"
android:strokeWidth="0.41878"
android:fillColor="#000000"
android:strokeColor="#000000"
android:fillType="evenOdd"
android:strokeLineCap="butt"/>
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -243,6 +243,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true" android:checked="true"
android:contentDescription="@string/settings_prefer_subbed"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -304,6 +305,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true" android:checked="true"
android:contentDescription="@string/settings_autoplay"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -378,6 +380,69 @@
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
<LinearLayout
android:id="@+id/linear_update_playhead"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:padding="7dp">
<ImageView
android:id="@+id/imageView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/update_playhead"
android:minWidth="48dp"
android:minHeight="48dp"
android:padding="9dp"
android:scaleType="fitXY"
android:src="@drawable/ic_baseline_access_time_24"
app:tint="?iconColor" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/linearLayout4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_update_playhead"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/text_update_playhead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/update_playhead"
android:textSize="16sp" />
<TextView
android:id="@+id/text_update_playhead_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/update_playhead_desc"
android:textColor="?textSecondary" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_update_playhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:contentDescription="@string/update_playhead"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/linear_export_data" android:id="@+id/linear_export_data"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -385,7 +450,8 @@
android:foreground="?android:selectableItemBackground" android:foreground="?android:selectableItemBackground"
android:gravity="center" android:gravity="center"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="7dp"> android:padding="7dp"
android:visibility="gone">
<ImageView <ImageView
android:id="@+id/image_export_data" android:id="@+id/image_export_data"
@ -430,7 +496,8 @@
android:foreground="?android:selectableItemBackground" android:foreground="?android:selectableItemBackground"
android:gravity="center" android:gravity="center"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="7dp"> android:padding="7dp"
android:visibility="gone">
<ImageView <ImageView
android:id="@+id/image_import_data" android:id="@+id/image_import_data"

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_splash_background"/>
<foreground android:drawable="@drawable/ic_splash_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -50,6 +50,8 @@
<string name="theme_light">Hell</string> <string name="theme_light">Hell</string>
<string name="theme_dark">Dunkel</string> <string name="theme_dark">Dunkel</string>
<string name="dev_settings">Entwickler Einstellungen</string> <string name="dev_settings">Entwickler Einstellungen</string>
<string name="update_playhead">Playhead Updates</string>
<string name="update_playhead_desc">Fortschritt bei Episoden auf cr updaten</string>
<string name="export_data">Daten exportieren</string> <string name="export_data">Daten exportieren</string>
<string name="export_data_desc">Speichere "Meine Liste" in eine Datei</string> <string name="export_data_desc">Speichere "Meine Liste" in eine Datei</string>
<string name="import_data">Daten importieren</string> <string name="import_data">Daten importieren</string>

View File

@ -26,4 +26,5 @@
<color name="controlHighlightDark">#11ffffff</color> <color name="controlHighlightDark">#11ffffff</color>
<color name="ic_launcher_background">#ffffff</color> <color name="ic_launcher_background">#ffffff</color>
<color name="ic_splash_background">#ffffff</color>
</resources> </resources>

View File

@ -59,6 +59,8 @@
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="dev_settings">Developer Settings</string> <string name="dev_settings">Developer Settings</string>
<string name="update_playhead">Playhead updates</string>
<string name="update_playhead_desc">Update episode playhead on cr</string>
<string name="export_data">export data</string> <string name="export_data">export data</string>
<string name="export_data_desc">export "My list" to a file</string> <string name="export_data_desc">export "My list" to a file</string>
<string name="import_data">import data</string> <string name="import_data">import data</string>
@ -137,6 +139,8 @@
<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>
<string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string> <string name="save_key_theme" translatable="false">org.mosad.teapod.theme</string>
<!-- dev settings -->
<string name="save_key_update_playhead" translatable="false">org.mosad.teapod.update_playhead</string>
<!-- intents & states --> <!-- intents & states -->
<string name="intent_media_id" translatable="false">intent_media_id</string> <string name="intent_media_id" translatable="false">intent_media_id</string>

View File

@ -71,10 +71,20 @@
</style> </style>
<!-- splash theme --> <!-- splash theme -->
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar"> <style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="android:windowBackground">@drawable/bg_splash</item> <!-- Set the splash screen background, animated icon, and animation duration. -->
<item name="windowSplashScreenBackground">@android:color/black</item>
<!-- Use windowSplashScreenAnimatedIcon to add either a drawable or an -->
<!-- animated drawable. One of these is required. -->
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_splash_round</item>
<item name="windowSplashScreenAnimationDuration">200</item>
<!-- Set the theme of the Activity that directly follows your splash screen. -->
<item name="postSplashScreenTheme">@style/AppTheme.Dark</item> # Required.
</style> </style>
<!-- shapes --> <!-- shapes -->
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent"> <style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists