parent
44f99295e9
commit
c66c725ee3
|
@ -46,6 +46,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.MetaDBController
|
||||||
import org.mosad.teapod.util.StorageController
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.util.exitAndRemoveTask
|
import org.mosad.teapod.util.exitAndRemoveTask
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
@ -137,8 +138,12 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
*/
|
*/
|
||||||
private fun load() {
|
private fun load() {
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
|
// start the initial loading
|
||||||
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||||
.async { AoDParser.initialLoading() } // start the initial loading
|
.async {
|
||||||
|
launch { AoDParser.initialLoading() }
|
||||||
|
launch { MetaDBController.list() }
|
||||||
|
}
|
||||||
|
|
||||||
// load all saved stuff here
|
// load all saved stuff here
|
||||||
Preferences.load(this)
|
Preferences.load(this)
|
||||||
|
|
|
@ -64,7 +64,6 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
|
||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
model.load(mediaId) // load the streams and tmdb for the selected media
|
model.load(mediaId) // load the streams and tmdb for the selected media
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ class MediaFragmentEpisodes : Fragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes)
|
adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes, model.tmdbTVSeason?.episodes)
|
||||||
binding.recyclerEpisodes.adapter = adapterRecEpisodes
|
binding.recyclerEpisodes.adapter = adapterRecEpisodes
|
||||||
|
|
||||||
// set onItemClick only in adapter is initialized
|
// set onItemClick only in adapter is initialized
|
||||||
|
|
|
@ -9,9 +9,11 @@ import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
import org.mosad.teapod.util.tmdb.Movie
|
import org.mosad.teapod.util.tmdb.Movie
|
||||||
import org.mosad.teapod.util.tmdb.TMDBApiController
|
import org.mosad.teapod.util.tmdb.TMDBApiController
|
||||||
import org.mosad.teapod.util.tmdb.TMDBResult
|
import org.mosad.teapod.util.tmdb.TMDBResult
|
||||||
|
import org.mosad.teapod.util.tmdb.TVSeason
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle media, next ep and tmdb
|
* handle media, next ep and tmdb
|
||||||
|
* TODO this lives in activity, is this correct?
|
||||||
*/
|
*/
|
||||||
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
@ -21,16 +23,35 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
internal set
|
internal set
|
||||||
lateinit var tmdbResult: TMDBResult // TODO rename
|
lateinit var tmdbResult: TMDBResult // TODO rename
|
||||||
internal set
|
internal set
|
||||||
|
var tmdbTVSeason: TVSeason? =null
|
||||||
|
internal set
|
||||||
|
var mediaMeta: Meta? = null
|
||||||
|
internal set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set media, tmdb and nextEpisode
|
* set media, tmdb and nextEpisode
|
||||||
|
* TODO run aod and tmdb load parallel
|
||||||
*/
|
*/
|
||||||
suspend fun load(mediaId: Int) {
|
suspend fun load(mediaId: Int) {
|
||||||
|
val tmdbApiController = TMDBApiController()
|
||||||
media = AoDParser.getMediaById(mediaId)
|
media = AoDParser.getMediaById(mediaId)
|
||||||
|
|
||||||
val tmdbApiController = TMDBApiController()
|
// check if metaDB knows the title
|
||||||
val searchTitle = stripTitleInfo(media.info.title)
|
val tmdbId: Int = if (MetaDBController.mediaList.media.contains(media.id)) {
|
||||||
val tmdbId = tmdbApiController.search(searchTitle, media.type)
|
// load media info from metaDB
|
||||||
|
val metaDB = MetaDBController()
|
||||||
|
mediaMeta = when (media.type) {
|
||||||
|
MediaType.MOVIE -> metaDB.getMovieMetadata(media.id)
|
||||||
|
MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.id)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaMeta?.tmdbId ?: -1
|
||||||
|
} else {
|
||||||
|
// use tmdb search to get media info
|
||||||
|
mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media
|
||||||
|
tmdbApiController.search(stripTitleInfo(media.info.title), media.type)
|
||||||
|
}
|
||||||
|
|
||||||
tmdbResult = when (media.type) {
|
tmdbResult = when (media.type) {
|
||||||
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)
|
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)
|
||||||
|
@ -39,16 +60,30 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
|
||||||
}
|
}
|
||||||
println(tmdbResult) // TODO
|
println(tmdbResult) // TODO
|
||||||
|
|
||||||
// TESTING
|
// get season info, if metaDB knows the tv show
|
||||||
if (media.type == MediaType.TVSHOW) {
|
tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) {
|
||||||
val seasonNumber = guessSeasonFromTitle(media.info.title)
|
val tvShowMeta = mediaMeta as TVShowMeta
|
||||||
Log.d("test", "season number: $seasonNumber")
|
tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
|
||||||
|
} else {
|
||||||
// TODO Important: only use tmdb info if media title and episode number match exactly
|
null
|
||||||
val tmdbTVSeason = tmdbApiController.getTVSeasonDetails(tmdbId, seasonNumber)
|
|
||||||
Log.d("test", "Season Info: $tmdbTVSeason.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TESTING
|
||||||
|
// if (media.type == MediaType.TVSHOW) {
|
||||||
|
// if (mediaMeta != null) {
|
||||||
|
// val tvShowMeta = mediaMeta as TVShowMeta
|
||||||
|
// val tmdbTVSeason = tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
|
||||||
|
// } else {
|
||||||
|
// // for tv shows not in metaDB, try to guess/search
|
||||||
|
//
|
||||||
|
// val seasonNumber = guessSeasonFromTitle(media.info.title)
|
||||||
|
// Log.d("test", "season number: $seasonNumber")
|
||||||
|
//
|
||||||
|
// val tmdbTVSeason = tmdbApiController.getTVSeasonDetails(tmdbId, seasonNumber)
|
||||||
|
// Log.d("test", "Season Info: $tmdbTVSeason.")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// TESTING END
|
// TESTING END
|
||||||
|
|
||||||
if (media.type == MediaType.TVSHOW) {
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
|
|
@ -19,9 +19,7 @@ import kotlinx.coroutines.runBlocking
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.util.DataTypes
|
import org.mosad.teapod.util.*
|
||||||
import org.mosad.teapod.util.Episode
|
|
||||||
import org.mosad.teapod.util.Media
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
@ -45,6 +43,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
internal set
|
internal set
|
||||||
var nextEpisode: Episode? = null
|
var nextEpisode: Episode? = null
|
||||||
internal set
|
internal set
|
||||||
|
var mediaMeta: Meta? = null
|
||||||
|
internal set
|
||||||
var currentLanguage: Locale = Locale.ROOT
|
var currentLanguage: Locale = Locale.ROOT
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
@ -75,6 +75,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
fun loadMedia(mediaId: Int, episodeId: Int) {
|
fun loadMedia(mediaId: Int, episodeId: Int) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
media = AoDParser.getMediaById(mediaId)
|
media = AoDParser.getMediaById(mediaId)
|
||||||
|
mediaMeta = loadMediaMeta(media.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentEpisode = media.getEpisodeById(episodeId)
|
currentEpisode = media.getEpisodeById(episodeId)
|
||||||
|
@ -159,6 +160,14 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadMediaMeta(aodId: Int): Meta? {
|
||||||
|
return if (media.type == DataTypes.MediaType.TVSHOW) {
|
||||||
|
MetaDBController().getTVShowMetadata(aodId)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on the current episodeId, get the next episode. If there is no next
|
* Based on the current episodeId, get the next episode. If there is no next
|
||||||
* episode, return null
|
* episode, return null
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
package org.mosad.teapod.util
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
Thread.sleep(5000)
|
||||||
|
|
||||||
|
mediaList = Gson().fromJson(json, MediaList::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getMovieMetadata(aodId: Int): MovieMeta? {
|
||||||
|
return metaCacheList.firstOrNull {
|
||||||
|
it.aodId == aodId
|
||||||
|
} as MovieMeta? ?: getMovieMetadata2(aodId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTVShowMetadata(aodId: Int): TVShowMeta? {
|
||||||
|
return metaCacheList.firstOrNull {
|
||||||
|
it.aodId == aodId
|
||||||
|
} as TVShowMeta? ?: getTVShowMetadata2(aodId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun getMovieMetadata2(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) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun getTVShowMetadata2(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) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO move data classes
|
||||||
|
data class MediaList(
|
||||||
|
val media: List<Int>
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract class Meta {
|
||||||
|
abstract val id: Int
|
||||||
|
abstract val aodId: Int
|
||||||
|
abstract val tmdbId: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MovieMeta(
|
||||||
|
override val id: Int,
|
||||||
|
@SerializedName("aod_id")
|
||||||
|
override val aodId: Int,
|
||||||
|
@SerializedName("tmdb_id")
|
||||||
|
override val tmdbId: Int
|
||||||
|
): Meta()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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: Int,
|
||||||
|
@SerializedName("opening_duration")
|
||||||
|
val openingDuration: Int,
|
||||||
|
@SerializedName("ending_start")
|
||||||
|
val endingStart: Int,
|
||||||
|
@SerializedName("ending_duration")
|
||||||
|
val endingDuration: Int
|
||||||
|
)
|
|
@ -12,8 +12,9 @@ import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
import org.mosad.teapod.databinding.ItemEpisodeBinding
|
||||||
import org.mosad.teapod.util.Episode
|
import org.mosad.teapod.util.Episode
|
||||||
|
import org.mosad.teapod.util.tmdb.TVEpisode
|
||||||
|
|
||||||
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
class EpisodeItemAdapter(private val episodes: List<Episode>, private val tmdbEpisodes: List<TVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
|
||||||
|
|
||||||
var onImageClick: ((String, Int) -> Unit)? = null
|
var onImageClick: ((String, Int) -> Unit)? = null
|
||||||
|
|
||||||
|
@ -32,7 +33,13 @@ class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Ada
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.binding.textEpisodeTitle.text = titleText
|
holder.binding.textEpisodeTitle.text = titleText
|
||||||
holder.binding.textEpisodeDesc.text = ep.shortDesc
|
holder.binding.textEpisodeDesc.text = if (ep.shortDesc.isNotEmpty()) {
|
||||||
|
ep.shortDesc
|
||||||
|
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
|
||||||
|
tmdbEpisodes[position].overview
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
if (episodes[position].posterUrl.isNotEmpty()) {
|
if (episodes[position].posterUrl.isNotEmpty()) {
|
||||||
Glide.with(context).load(ep.posterUrl)
|
Glide.with(context).load(ep.posterUrl)
|
||||||
|
|
|
@ -99,14 +99,14 @@ class TMDBApiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TVSeason = withContext(Dispatchers.IO) {
|
suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TVSeason? = withContext(Dispatchers.IO) {
|
||||||
val url = URL("$detailsTVUrl/$tvId/season/$seasonNumber?api_key=$apiKey&language=$language")
|
val url = URL("$detailsTVUrl/$tvId/season/$seasonNumber?api_key=$apiKey&language=$language")
|
||||||
|
|
||||||
val response = try {
|
val response = try {
|
||||||
JsonParser.parseString(url.readText()).asJsonObject
|
JsonParser.parseString(url.readText()).asJsonObject
|
||||||
} catch (ex: FileNotFoundException) {
|
} catch (ex: FileNotFoundException) {
|
||||||
Log.w(javaClass.name, "The resource you requested could not be found")
|
Log.w(javaClass.name, "The resource you requested could not be found")
|
||||||
return@withContext TVSeason(-1)
|
return@withContext null
|
||||||
}
|
}
|
||||||
// println(response)
|
// println(response)
|
||||||
|
|
||||||
|
@ -114,25 +114,25 @@ class TMDBApiController {
|
||||||
val episodes = response.get("episodes").asJsonArray.map {
|
val episodes = response.get("episodes").asJsonArray.map {
|
||||||
TVEpisode(
|
TVEpisode(
|
||||||
id = it.asJsonObject.get("id").asInt,
|
id = it.asJsonObject.get("id").asInt,
|
||||||
name = it.asJsonObject.get("name")?.asString,
|
name = it.asJsonObject.get("name")?.asString ?: "",
|
||||||
overview = it.asJsonObject.get("overview")?.asString,
|
overview = it.asJsonObject.get("overview")?.asString ?: "",
|
||||||
airDate = it.asJsonObject.get("air_date")?.asString,
|
airDate = it.asJsonObject.get("air_date")?.asString ?: "",
|
||||||
episodeNumber = it.asJsonObject.get("episode_number")?.asInt
|
episodeNumber = it.asJsonObject.get("episode_number")?.asInt ?: -1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TVSeason(
|
TVSeason(
|
||||||
id = response.get("id").asInt,
|
id = response.get("id").asInt,
|
||||||
name = response.asJsonObject.get("name")?.asString,
|
name = response.asJsonObject.get("name")?.asString ?: "",
|
||||||
overview = response.asJsonObject.get("overview")?.asString,
|
overview = response.asJsonObject.get("overview")?.asString ?: "",
|
||||||
posterPath = response.asJsonObject.get("poster_path")?.asString,
|
posterPath = response.asJsonObject.get("poster_path")?.asString ?: "",
|
||||||
airDate = response.asJsonObject.get("air_date")?.asString,
|
airDate = response.asJsonObject.get("air_date")?.asString ?: "",
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
seasonNumber = response.get("season_number")?.asInt
|
seasonNumber = response.get("season_number")?.asInt ?: -1
|
||||||
)
|
)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Log.w(javaClass.name, "Error", ex)
|
Log.w(javaClass.name, "Error", ex)
|
||||||
TVSeason(-1)
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,19 +32,19 @@ data class TVShow(
|
||||||
|
|
||||||
data class TVSeason(
|
data class TVSeason(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String? = null,
|
val name: String,
|
||||||
val overview: String? = null,
|
val overview: String,
|
||||||
val posterPath: String? = null,
|
val posterPath: String,
|
||||||
val airDate: String? = null,
|
val airDate: String,
|
||||||
val episodes: List<TVEpisode>? = null,
|
val episodes: List<TVEpisode>,
|
||||||
val seasonNumber: Int? = null
|
val seasonNumber: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO decide whether to use nullable or not
|
// TODO decide whether to use nullable or not
|
||||||
data class TVEpisode(
|
data class TVEpisode(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String? = null,
|
val name: String,
|
||||||
val overview: String? = null,
|
val overview: String,
|
||||||
val airDate: String? = null,
|
val airDate: String,
|
||||||
val episodeNumber: Int? = null
|
val episodeNumber: Int
|
||||||
)
|
)
|
Loading…
Reference in New Issue