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.PIPED_API_URL
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.util.PreferenceHelper 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.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.create
object RetrofitInstance { object RetrofitInstance {
lateinit var url: String lateinit var url: String
lateinit var authUrl: String lateinit var authUrl: String
val lazyMgr = resettableManager() 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() Retrofit.Builder()
.baseUrl(url) .baseUrl(url)
.callFactory(CronetHelper.callFactory) .callFactory(CronetHelper.callFactory)
.addConverterFactory(kotlinxConverterFactory)
.addConverterFactory(jacksonConverterFactory) .addConverterFactory(jacksonConverterFactory)
.build() .build()
.create(PipedApi::class.java) .create<PipedApi>()
} }
val authApi: PipedApi by resettableLazy(lazyMgr) { val authApi by resettableLazy(lazyMgr) {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(authUrl) .baseUrl(authUrl)
.callFactory(CronetHelper.callFactory) .callFactory(CronetHelper.callFactory)
.addConverterFactory(kotlinxConverterFactory)
.addConverterFactory(jacksonConverterFactory) .addConverterFactory(jacksonConverterFactory)
.build() .build()
.create(PipedApi::class.java) .create<PipedApi>()
} }
val externalApi: ExternalApi by resettableLazy(lazyMgr) { val externalApi by resettableLazy(lazyMgr) {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(url) .baseUrl(url)
.callFactory(CronetHelper.callFactory) .callFactory(CronetHelper.callFactory)
.addConverterFactory(kotlinxConverterFactory)
.addConverterFactory(jacksonConverterFactory) .addConverterFactory(jacksonConverterFactory)
.build() .build()
.create(ExternalApi::class.java) .create<ExternalApi>()
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

@ -1,31 +1,32 @@
package com.github.libretube.api.obj 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( data class Streams(
val title: String? = null, val title: String,
val description: String? = null, val description: String,
val uploadDate: String? = null, val uploadDate: LocalDate,
val uploader: String? = null, val uploader: String,
val uploaderUrl: String? = null, val uploaderUrl: String,
val uploaderAvatar: String? = null, val uploaderAvatar: String,
val thumbnailUrl: String? = null, val thumbnailUrl: String,
val hls: String? = null, val hls: String? = null,
val dash: String? = null, val dash: String? = null,
val lbryId: String? = null, val lbryId: String? = null,
val uploaderVerified: Boolean? = null, val uploaderVerified: Boolean,
val duration: Long? = null, val duration: Long,
val views: Long? = null, val views: Long = 0,
val likes: Long? = null, val likes: Long = 0,
val dislikes: Long? = null, val dislikes: Long = 0,
val audioStreams: List<PipedStream>? = null, val audioStreams: List<PipedStream> = emptyList(),
val videoStreams: List<PipedStream>? = null, val videoStreams: List<PipedStream> = emptyList(),
val relatedStreams: List<StreamItem>? = null, val relatedStreams: List<StreamItem> = emptyList(),
val subtitles: List<Subtitle>? = null, val subtitles: List<Subtitle> = emptyList(),
val livestream: Boolean? = null, val livestream: Boolean = false,
val proxyUrl: String? = null, val proxyUrl: String? = null,
val chapters: List<ChapterSegment>? = null, val chapters: List<ChapterSegment> = emptyList(),
val uploaderSubscriberCount: Long? = null, val uploaderSubscriberCount: Long = 0,
val previewFrames: List<PreviewFrames>? = null val previewFrames: List<PreviewFrames> = emptyList()
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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