Use Kotlinx Serialization with stream data.

This commit is contained in:
Isira Seneviratne 2023-01-18 07:01:06 +05:30
parent b04cef0e88
commit 219a7d7cfe
13 changed files with 91 additions and 93 deletions

View File

@ -3,40 +3,51 @@ package com.github.libretube.api
import com.github.libretube.constants.PIPED_API_URL
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.util.PreferenceHelper
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.create
object RetrofitInstance {
lateinit var url: String
lateinit var authUrl: String
val lazyMgr = resettableManager()
val jacksonConverterFactory = JacksonConverterFactory.create()
private val jacksonConverterFactory = JacksonConverterFactory.create()
private val json = Json {
ignoreUnknownKeys = true
}
private val kotlinxConverterFactory = json.asConverterFactory("application/json".toMediaType())
val api: PipedApi by resettableLazy(lazyMgr) {
val api by resettableLazy(lazyMgr) {
Retrofit.Builder()
.baseUrl(url)
.callFactory(CronetHelper.callFactory)
.addConverterFactory(kotlinxConverterFactory)
.addConverterFactory(jacksonConverterFactory)
.build()
.create(PipedApi::class.java)
.create<PipedApi>()
}
val authApi: PipedApi by resettableLazy(lazyMgr) {
val authApi by resettableLazy(lazyMgr) {
Retrofit.Builder()
.baseUrl(authUrl)
.callFactory(CronetHelper.callFactory)
.addConverterFactory(kotlinxConverterFactory)
.addConverterFactory(jacksonConverterFactory)
.build()
.create(PipedApi::class.java)
.create<PipedApi>()
}
val externalApi: ExternalApi by resettableLazy(lazyMgr) {
val externalApi by resettableLazy(lazyMgr) {
Retrofit.Builder()
.baseUrl(url)
.callFactory(CronetHelper.callFactory)
.addConverterFactory(kotlinxConverterFactory)
.addConverterFactory(jacksonConverterFactory)
.build()
.create(ExternalApi::class.java)
.create<ExternalApi>()
}
/**

View File

@ -1,8 +1,8 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class ChapterSegment(
var title: String? = null,
var image: String? = null,

View File

@ -1,8 +1,8 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class PipedStream(
var url: String? = null,
var format: String? = null,

View File

@ -1,8 +1,8 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class PreviewFrames(
val urls: List<String>? = null,
val frameWidth: Int? = null,

View File

@ -1,8 +1,8 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class StreamItem(
var url: String? = null,
val type: String? = null,

View File

@ -1,31 +1,32 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class Streams(
val title: String? = null,
val description: String? = null,
val uploadDate: String? = null,
val uploader: String? = null,
val uploaderUrl: String? = null,
val uploaderAvatar: String? = null,
val thumbnailUrl: String? = null,
val title: String,
val description: String,
val uploadDate: LocalDate,
val uploader: String,
val uploaderUrl: String,
val uploaderAvatar: String,
val thumbnailUrl: String,
val hls: String? = null,
val dash: String? = null,
val lbryId: String? = null,
val uploaderVerified: Boolean? = null,
val duration: Long? = null,
val views: Long? = null,
val likes: Long? = null,
val dislikes: Long? = null,
val audioStreams: List<PipedStream>? = null,
val videoStreams: List<PipedStream>? = null,
val relatedStreams: List<StreamItem>? = null,
val subtitles: List<Subtitle>? = null,
val livestream: Boolean? = null,
val uploaderVerified: Boolean,
val duration: Long,
val views: Long = 0,
val likes: Long = 0,
val dislikes: Long = 0,
val audioStreams: List<PipedStream> = emptyList(),
val videoStreams: List<PipedStream> = emptyList(),
val relatedStreams: List<StreamItem> = emptyList(),
val subtitles: List<Subtitle> = emptyList(),
val livestream: Boolean = false,
val proxyUrl: String? = null,
val chapters: List<ChapterSegment>? = null,
val uploaderSubscriberCount: Long? = null,
val previewFrames: List<PreviewFrames>? = null
val chapters: List<ChapterSegment> = emptyList(),
val uploaderSubscriberCount: Long = 0,
val previewFrames: List<PreviewFrames> = emptyList()
)

View File

@ -1,8 +1,8 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class Subtitle(
val url: String? = null,
val mimeType: String? = null,

View File

@ -16,9 +16,9 @@ object DatabaseHelper {
val watchHistoryItem = WatchHistoryItem(
videoId,
streams.title,
streams.uploadDate,
streams.uploadDate.toString(),
streams.uploader,
streams.uploaderUrl!!.toID(),
streams.uploaderUrl.toID(),
streams.uploaderAvatar,
streams.thumbnailUrl,
streams.duration

View File

@ -13,7 +13,7 @@ fun Streams.toStreamItem(videoId: String): StreamItem {
uploaderName = uploader,
uploaderUrl = uploaderUrl,
uploaderAvatar = uploaderAvatar,
uploadedDate = uploadDate,
uploadedDate = uploadDate.toString(),
uploaded = null,
duration = duration,
views = views,

View File

@ -102,21 +102,19 @@ class DownloadService : Service() {
Database.downloadDao().insertDownload(
Download(
videoId = videoId,
title = streams.title ?: "",
title = streams.title,
thumbnailPath = thumbnailTargetFile.absolutePath,
description = streams.description ?: "",
uploadDate = streams.uploadDate,
uploader = streams.uploader ?: ""
description = streams.description,
uploadDate = streams.uploadDate.toString(),
uploader = streams.uploader
)
)
}
streams.thumbnailUrl?.let { url ->
ImageHelper.downloadImage(
this@DownloadService,
url,
streams.thumbnailUrl,
thumbnailTargetFile.absolutePath
)
}
val downloadItems = streams.toDownloadItems(
videoId,

View File

@ -74,7 +74,7 @@ class DownloadDialog(
}
private fun initDownloadOptions(streams: Streams) {
binding.fileName.setText(streams.title.toString())
binding.fileName.setText(streams.title)
val vidName = arrayListOf<String>()
@ -82,7 +82,7 @@ class DownloadDialog(
vidName.add(getString(R.string.no_video))
// add all available video streams
for (vid in streams.videoStreams!!) {
for (vid in streams.videoStreams) {
if (vid.url != null) {
val name = vid.quality + " " + vid.format
vidName.add(name)
@ -95,7 +95,7 @@ class DownloadDialog(
audioName.add(getString(R.string.no_audio))
// add all available audio streams
for (audio in streams.audioStreams!!) {
for (audio in streams.audioStreams) {
if (audio.url != null) {
val name = audio.quality + " " + audio.format
audioName.add(name)
@ -108,7 +108,7 @@ class DownloadDialog(
subtitleName.add(getString(R.string.no_subtitle))
// add all available subtitles
for (subtitle in streams.subtitles!!) {
for (subtitle in streams.subtitles) {
if (subtitle.url != null) {
subtitleName.add(subtitle.name.toString())
}

View File

@ -116,6 +116,7 @@ import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import org.chromium.net.CronetEngine
import retrofit2.HttpException
@ -682,9 +683,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
} else {
PlayingQueue.updateCurrent(streams.toStreamItem(videoId!!))
if (PlayerHelper.autoInsertRelatedVideos) {
PlayingQueue.add(
*streams.relatedStreams.orEmpty().toTypedArray()
)
PlayingQueue.add(*streams.relatedStreams.toTypedArray())
}
}
}
@ -773,8 +772,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
playerBinding.liveDiff.text = "-$diffText"
}
// call the function again after 100ms
handler
.postDelayed(this@PlayerFragment::refreshLiveStatus, 100)
handler.postDelayed(this@PlayerFragment::refreshLiveStatus, 100)
}
// seek to saved watch position if available
@ -794,7 +792,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
return
}
// position is almost the end of the video => don't seek, start from beginning
if (position != null && position < streams.duration!! * 1000 * 0.9) {
if (position != null && position < streams.duration * 1000 * 0.9) {
exoPlayer.seekTo(position)
}
}
@ -828,7 +826,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
playerBinding.exoProgress.setPlayer(exoPlayer)
}
private fun localizedDate(date: String?): String? {
private fun localizedDate(date: LocalDate): String {
val locale = ConfigurationCompat.getLocales(resources.configuration)[0]!!
return TextUtils.localizeDate(date, locale)
}
@ -870,12 +868,12 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
playerChannelSubCount.text = context?.getString(
R.string.subscribers,
streams.uploaderSubscriberCount?.formatShort()
streams.uploaderSubscriberCount.formatShort()
)
}
// duration that's not greater than 0 indicates that the video is live
if (streams.duration!! <= 0) {
if (streams.duration <= 0) {
isLive = true
handleLiveVideo()
}
@ -883,10 +881,8 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
playerBinding.exoTitle.text = streams.title
// init the chapters recyclerview
if (streams.chapters != null) {
chapters = streams.chapters.orEmpty()
chapters = streams.chapters
initializeChapters()
}
// Listener for play and pause icon change
exoPlayer.addListener(object : Player.Listener {
@ -972,7 +968,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
})
binding.relPlayerDownload.setOnClickListener {
if (streams.duration!! <= 0) {
if (streams.duration <= 0) {
Toast.makeText(context, R.string.cannotDownload, Toast.LENGTH_SHORT).show()
} else if (!DownloadService.IS_DOWNLOAD_RUNNING) {
val newFragment = DownloadDialog(videoId!!)
@ -995,7 +991,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
}
initializeRelatedVideos(streams.relatedStreams)
// set video description
val description = streams.description!!
val description = streams.description
// detect whether the description is html formatted
if (description.contains("<") && description.contains(">")) {
@ -1016,7 +1012,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
// update the subscribed state
binding.playerSubscribe.setupSubscriptionButton(
this.streams.uploaderUrl?.toID(),
this.streams.uploaderUrl.toID(),
this.streams.uploader
)
@ -1228,9 +1224,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
private fun setResolutionAndSubtitles() {
// create a list of subtitles
subtitles = mutableListOf()
val subtitlesNamesList = mutableListOf(context?.getString(R.string.none)!!)
val subtitlesNamesList = mutableListOf(getString(R.string.none))
val subtitleCodesList = mutableListOf("")
streams.subtitles.orEmpty().forEach {
streams.subtitles.forEach {
subtitles.add(
SubtitleConfiguration.Builder(it.url!!.toUri())
.setMimeType(it.mimeType!!) // The correct MIME type (required).
@ -1260,7 +1256,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
if (defaultResolution != "") setPlayerResolution(defaultResolution.toInt())
if (!PreferenceHelper.getBoolean(PreferenceKeys.USE_HLS_OVER_DASH, false) &&
streams.videoStreams.orEmpty().isNotEmpty()
streams.videoStreams.isNotEmpty()
) {
val uri = let {
streams.dash?.toUri()
@ -1355,16 +1351,14 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
}
override fun onCaptionsClicked() {
if (!this@PlayerFragment::streams.isInitialized ||
streams.subtitles.isNullOrEmpty()
) {
if (!this@PlayerFragment::streams.isInitialized || streams.subtitles.isEmpty()) {
Toast.makeText(context, R.string.no_subtitles_available, Toast.LENGTH_SHORT).show()
return
}
val subtitlesNamesList = mutableListOf(context?.getString(R.string.none)!!)
val subtitlesNamesList = mutableListOf(getString(R.string.none))
val subtitleCodesList = mutableListOf("")
streams.subtitles!!.forEach {
streams.subtitles.forEach {
subtitlesNamesList += it.name!!
subtitleCodesList += it.code!!
}
@ -1493,9 +1487,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
playerBinding.seekbarPreview.visibility = View.GONE
playerBinding.exoProgress.addListener(
SeekbarPreviewListener(
streams.previewFrames.orEmpty(),
streams.previewFrames,
playerBinding.seekbarPreview,
streams.duration!! * 1000
streams.duration * 1000
)
)
}

View File

@ -1,10 +1,11 @@
package com.github.libretube.util
import java.net.URL
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlinx.datetime.LocalDate
import kotlinx.datetime.toJavaLocalDate
object TextUtils {
/**
@ -41,15 +42,8 @@ object TextUtils {
* @param locale The locale to use, otherwise uses system default
* return Localized date string
*/
fun localizeDate(date: String?, locale: Locale): String? {
date ?: return null
// relative time span
if (!date.contains("-")) return date
val dateObj = LocalDate.parse(date)
fun localizeDate(date: LocalDate, locale: Locale): String {
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale)
return dateObj.format(formatter)
return date.toJavaLocalDate().format(formatter)
}
}