Merge pull request #2755 from Isira-Seneviratne/KotlinX_Serialization

Switch to Kotlinx Serialization.
This commit is contained in:
Bnyro 2023-01-21 18:59:56 +01:00 committed by GitHub
commit dd1821ada3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 589 additions and 615 deletions

View File

@ -4,6 +4,7 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'kotlinx-serialization'
} }
android { android {
@ -107,10 +108,11 @@ dependencies {
implementation libs.exoplayer.extension.mediasession implementation libs.exoplayer.extension.mediasession
implementation libs.exoplayer.dash implementation libs.exoplayer.dash
/* Retrofit and Jackson */ /* Retrofit and Kotlinx Serialization */
implementation libs.square.retrofit implementation libs.square.retrofit
implementation libs.square.retrofit.converterJackson implementation libs.kotlinx.serialization
implementation libs.jacksonAnnotations implementation libs.kotlinx.datetime
implementation libs.kotlinx.serialization.retrofit
/* Cronet and Coil */ /* Cronet and Coil */
coreLibraryDesugaring libs.desugaring coreLibraryDesugaring libs.desugaring

View File

@ -29,3 +29,26 @@
# Keep data classes used for Retrofit # Keep data classes used for Retrofit
-keep class com.github.libretube.obj.** { *; } -keep class com.github.libretube.obj.** { *; }
-keep class com.github.libretube.obj.update.** { *; } -keep class com.github.libretube.obj.update.** { *; }
# Keep rules required by Kotlinx Serialization
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

View File

@ -0,0 +1,9 @@
package com.github.libretube.api
import kotlinx.serialization.json.Json
object JsonHelper {
val json = Json {
ignoreUnknownKeys = true
}
}

View File

@ -15,6 +15,7 @@ import com.github.libretube.api.obj.SegmentData
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Streams
import com.github.libretube.api.obj.Subscribe import com.github.libretube.api.obj.Subscribe
import com.github.libretube.api.obj.Subscribed
import com.github.libretube.api.obj.Subscription import com.github.libretube.api.obj.Subscription
import com.github.libretube.api.obj.Token import com.github.libretube.api.obj.Token
import retrofit2.http.Body import retrofit2.http.Body
@ -114,7 +115,7 @@ interface PipedApi {
suspend fun isSubscribed( suspend fun isSubscribed(
@Query("channelId") channelId: String, @Query("channelId") channelId: String,
@Header("Authorization") token: String @Header("Authorization") token: String
): com.github.libretube.api.obj.Subscribed ): Subscribed
@GET("subscriptions") @GET("subscriptions")
suspend fun subscriptions(@Header("Authorization") token: String): List<Subscription> suspend fun subscriptions(@Header("Authorization") token: String): List<Subscription>

View File

@ -93,10 +93,7 @@ object PlaylistsHelper {
}.last().playlist.id.toString() }.last().playlist.id.toString()
} }
val response = try { val response = try {
RetrofitInstance.authApi.createPlaylist( RetrofitInstance.authApi.createPlaylist(token, Playlists(name = playlistName))
token,
Playlists(name = playlistName)
)
} catch (e: IOException) { } catch (e: IOException) {
appContext.toastFromMainThread(R.string.unknown_error) appContext.toastFromMainThread(R.string.unknown_error)
return null return null
@ -107,7 +104,7 @@ object PlaylistsHelper {
} }
if (response.playlistId != null) { if (response.playlistId != null) {
appContext.toastFromMainThread(R.string.playlistCreated) appContext.toastFromMainThread(R.string.playlistCreated)
return response.playlistId!! return response.playlistId
} }
return null return null
} }
@ -141,13 +138,8 @@ object PlaylistsHelper {
return true return true
} }
return RetrofitInstance.authApi.addToPlaylist( val playlist = PlaylistId(playlistId, videoIds = videos.map { it.url!!.toID() })
token, return RetrofitInstance.authApi.addToPlaylist(token, playlist).message == "ok"
PlaylistId(
playlistId = playlistId,
videoIds = videos.toList().map { it.url!!.toID() }
)
).message == "ok"
} }
suspend fun renamePlaylist(playlistId: String, newName: String): Boolean { suspend fun renamePlaylist(playlistId: String, newName: String): Boolean {
@ -164,10 +156,7 @@ object PlaylistsHelper {
return RetrofitInstance.authApi.renamePlaylist( return RetrofitInstance.authApi.renamePlaylist(
token, token,
PlaylistId( PlaylistId(playlistId, newName = newName)
playlistId = playlistId,
newName = newName
)
).playlistId != null ).playlistId != null
} }
@ -251,8 +240,8 @@ object PlaylistsHelper {
name = list.name, name = list.name,
type = "playlist", type = "playlist",
visibility = "private", visibility = "private",
videos = list.relatedStreams.orEmpty().map { videos = list.relatedStreams.map {
YOUTUBE_FRONTEND_URL + "/watch?v=" + it.url!!.toID() "$YOUTUBE_FRONTEND_URL/watch?v=${it.url!!.toID()}"
} }
) )
) )
@ -278,19 +267,13 @@ object PlaylistsHelper {
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name", appContext) val newPlaylist = createPlaylist(playlist.name ?: "Unknown name", appContext)
newPlaylist ?: return@launch newPlaylist ?: return@launch
addToPlaylist( addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
newPlaylist,
*playlist.relatedStreams.orEmpty().toTypedArray()
)
var nextPage = playlist.nextpage var nextPage = playlist.nextpage
while (nextPage != null) { while (nextPage != null) {
nextPage = try { nextPage = try {
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage).apply { RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage).apply {
addToPlaylist( addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
newPlaylist,
*relatedStreams.orEmpty().toTypedArray()
)
}.nextpage }.nextpage
} catch (e: Exception) { } catch (e: Exception) {
return@launch return@launch

View File

@ -3,40 +3,43 @@ 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 okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit import retrofit2.Retrofit
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 kotlinxConverterFactory = JsonHelper.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(jacksonConverterFactory) .addConverterFactory(kotlinxConverterFactory)
.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(jacksonConverterFactory) .addConverterFactory(kotlinxConverterFactory)
.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(jacksonConverterFactory) .addConverterFactory(kotlinxConverterFactory)
.build() .build()
.create(ExternalApi::class.java) .create<ExternalApi>()
} }
/** /**

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log import android.util.Log
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Subscribe
import com.github.libretube.api.obj.Subscription import com.github.libretube.api.obj.Subscription
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database import com.github.libretube.db.DatabaseHolder.Companion.Database
@ -25,7 +26,7 @@ object SubscriptionHelper {
try { try {
RetrofitInstance.authApi.subscribe( RetrofitInstance.authApi.subscribe(
PreferenceHelper.getToken(), PreferenceHelper.getToken(),
com.github.libretube.api.obj.Subscribe(channelId) Subscribe(channelId)
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
@ -46,7 +47,7 @@ object SubscriptionHelper {
try { try {
RetrofitInstance.authApi.unsubscribe( RetrofitInstance.authApi.unsubscribe(
PreferenceHelper.getToken(), PreferenceHelper.getToken(),
com.github.libretube.api.obj.Subscribe(channelId) Subscribe(channelId)
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())

View File

@ -1,17 +1,17 @@
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 Channel( data class Channel(
var id: String? = null, val id: String,
var name: String? = null, val name: String,
var avatarUrl: String? = null, val avatarUrl: String,
var bannerUrl: String? = null, val bannerUrl: String,
var description: String? = null, val description: String,
var nextpage: String? = null, val nextpage: String? = null,
var subscriberCount: Long = 0, val subscriberCount: Long = 0,
var verified: Boolean = false, val verified: Boolean = false,
var relatedStreams: List<StreamItem>? = listOf(), val relatedStreams: List<StreamItem> = emptyList(),
var tabs: List<ChannelTab>? = listOf() val tabs: List<ChannelTab> = emptyList()
) )

View File

@ -1,9 +1,9 @@
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 ChannelTab( data class ChannelTab(
val name: String? = null, val name: String,
val data: String? = null val data: String
) )

View File

@ -1,6 +1,9 @@
package com.github.libretube.api.obj package com.github.libretube.api.obj
import kotlinx.serialization.Serializable
@Serializable
data class ChannelTabResponse( data class ChannelTabResponse(
val content: List<ContentItem> = listOf(), val content: List<ContentItem> = emptyList(),
val nextpage: String? = null val nextpage: String? = null
) )

View File

@ -1,10 +1,10 @@
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, val title: String? = null,
var image: String? = null, val image: String? = null,
var start: Long? = null val start: Long? = null
) )

View File

@ -1,19 +1,19 @@
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 Comment( data class Comment(
val author: String? = null, val author: String,
val commentId: String? = null, val commentId: String,
val commentText: String? = null, val commentText: String,
val commentedTime: String? = null, val commentedTime: String,
val commentorUrl: String? = null, val commentorUrl: String,
val repliesPage: String? = null, val repliesPage: String? = null,
val hearted: Boolean? = null, val hearted: Boolean,
val likeCount: Long? = null, val likeCount: Long,
val pinned: Boolean? = null, val pinned: Boolean,
val thumbnail: String? = null, val thumbnail: String,
val verified: Boolean? = null, val verified: Boolean,
val replyCount: Long? = null val replyCount: Long
) )

View File

@ -1,10 +1,10 @@
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 CommentsPage( data class CommentsPage(
var comments: MutableList<Comment> = arrayListOf(), var comments: List<Comment> = emptyList(),
val disabled: Boolean? = null, val disabled: Boolean = false,
val nextpage: String? = null val nextpage: String? = null
) )

View File

@ -1,28 +1,28 @@
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 ContentItem( data class ContentItem(
var url: String? = null, val url: String,
val type: String? = null, val type: String,
var thumbnail: String? = null, val thumbnail: String,
var uploaderName: String? = null, val uploaderName: String? = null,
var uploaded: Long? = null, val uploaded: Long? = null,
var shortDescription: String? = null, val shortDescription: String? = null,
// Video only attributes // Video only attributes
var title: String? = null, val title: String? = null,
var uploaderUrl: String? = null, val uploaderUrl: String? = null,
var uploaderAvatar: String? = null, val uploaderAvatar: String? = null,
var uploadedDate: String? = null, val uploadedDate: String? = null,
var duration: Long? = null, val duration: Long = -1,
var views: Long? = null, val views: Long = -1,
var isShort: Boolean? = null, val isShort: Boolean? = null,
var uploaderVerified: Boolean? = null, val uploaderVerified: Boolean? = null,
// Channel and Playlist attributes // Channel and Playlist attributes
var name: String? = null, val name: String? = null,
var description: String? = null, val description: String? = null,
var subscribers: Long? = -1, val subscribers: Long = -1,
var videos: Long? = -1, val videos: Long = -1,
var verified: Boolean? = null val verified: Boolean? = null
) )

View File

@ -1,5 +1,6 @@
package com.github.libretube.api.obj package com.github.libretube.api.obj
data class DeleteUserRequest( import kotlinx.serialization.Serializable
var password: String? = null
) @Serializable
data class DeleteUserRequest(val password: String)

View File

@ -1,15 +1,16 @@
package com.github.libretube.api.obj package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true) @Serializable
data class Instances( data class Instances(
var name: String? = null, val name: String,
var api_url: String? = null, @SerialName("api_url") val apiUrl: String,
var locations: String? = null, val locations: String,
var version: String? = null, val version: String,
var up_to_date: Boolean? = null, @SerialName("up_to_date") val upToDate: Boolean,
var cdn: Boolean? = null, val cdn: Boolean,
var registered: Long? = null, val registered: Long,
var last_checked: Long? = null @SerialName("last_checked") val lastChecked: Long
) )

View File

@ -1,9 +1,9 @@
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 Login( data class Login(
val username: String? = null, val username: String,
val password: String? = null val password: String
) )

View File

@ -1,5 +1,6 @@
package com.github.libretube.api.obj package com.github.libretube.api.obj
data class Message( import kotlinx.serialization.Serializable
var message: String? = null
) @Serializable
data class Message(val message: String? = null)

View File

@ -1,5 +1,8 @@
package com.github.libretube.api.obj package com.github.libretube.api.obj
import kotlinx.serialization.Serializable
@Serializable
data class PipedConfig( data class PipedConfig(
val donationUrl: String? = null, val donationUrl: String? = null,
val statusPageUrl: String? = null, val statusPageUrl: String? = null,

View File

@ -1,23 +1,23 @@
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, val url: String? = null,
var format: String? = null, val format: String? = null,
var quality: String? = null, val quality: String? = null,
var mimeType: String? = null, val mimeType: String? = null,
var codec: String? = null, val codec: String? = null,
var videoOnly: Boolean? = null, val videoOnly: Boolean? = null,
var bitrate: Int? = null, val bitrate: Int? = null,
var initStart: Int? = null, val initStart: Int? = null,
var initEnd: Int? = null, val initEnd: Int? = null,
var indexStart: Int? = null, val indexStart: Int? = null,
var indexEnd: Int? = null, val indexEnd: Int? = null,
var width: Int? = null, val width: Int? = null,
var height: Int? = null, val height: Int? = null,
var fps: Int? = null, val fps: Int? = null,
val audioTrackName: String? = null, val audioTrackName: String? = null,
val audioTrackId: String? = null val audioTrackId: String? = null
) )

View File

@ -1,16 +1,16 @@
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 Playlist( data class Playlist(
var name: String? = null, val name: String? = null,
var thumbnailUrl: String? = null, val thumbnailUrl: String? = null,
var bannerUrl: String? = null, val bannerUrl: String? = null,
var nextpage: String? = null, val nextpage: String? = null,
var uploader: String? = null, val uploader: String? = null,
var uploaderUrl: String? = null, val uploaderUrl: String? = null,
var uploaderAvatar: String? = null, val uploaderAvatar: String? = null,
var videos: Int? = 0, val videos: Int = 0,
var relatedStreams: List<StreamItem>? = null val relatedStreams: List<StreamItem> = emptyList()
) )

View File

@ -1,12 +1,12 @@
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 PlaylistId( data class PlaylistId(
var playlistId: String? = null, val playlistId: String? = null,
var videoId: String? = null, val videoId: String? = null,
var videoIds: List<String>? = null, val videoIds: List<String> = emptyList(),
var newName: String? = null, val newName: String? = null,
var index: Int = -1 val index: Int = -1
) )

View File

@ -1,12 +1,12 @@
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 Playlists( data class Playlists(
var id: String? = null, val id: String? = null,
var name: String? = null, val name: String? = null,
var shortDescription: String? = null, val shortDescription: String? = null,
var thumbnail: String? = null, val thumbnail: String? = null,
var videos: Long? = null val videos: Long = 0
) )

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,11 +1,11 @@
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 SearchResult( data class SearchResult(
val items: MutableList<ContentItem>? = arrayListOf(), val items: List<ContentItem> = emptyList(),
val nextpage: String? = null, val nextpage: String? = null,
val suggestion: String? = "", val suggestion: String? = null,
val corrected: Boolean? = null val corrected: Boolean? = 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 Segment( data class Segment(
val UUID: String? = null, val UUID: String? = null,
val actionType: String? = null, val actionType: 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 SegmentData( data class SegmentData(
val hash: String? = null, val hash: String? = null,
val segments: List<Segment> = listOf(), val segments: List<Segment> = listOf(),

View File

@ -1,21 +1,21 @@
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, val url: String? = null,
val type: String? = null, val type: String? = null,
var title: String? = null, val title: String? = null,
var thumbnail: String? = null, val thumbnail: String? = null,
var uploaderName: String? = null, val uploaderName: String? = null,
var uploaderUrl: String? = null, val uploaderUrl: String? = null,
var uploaderAvatar: String? = null, val uploaderAvatar: String? = null,
var uploadedDate: String? = null, val uploadedDate: String? = null,
var duration: Long? = null, val duration: Long? = null,
var views: Long? = null, val views: Long? = null,
var uploaderVerified: Boolean? = null, val uploaderVerified: Boolean? = null,
var uploaded: Long? = null, val uploaded: Long? = null,
var shortDescription: String? = null, val shortDescription: String? = null,
val isShort: Boolean = false val isShort: Boolean = false
) )

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,6 @@
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 Subscribe( data class Subscribe(val channelId: String)
var channelId: String? = null
)

View File

@ -1,8 +1,6 @@
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 Subscribed( data class Subscribed(val subscribed: Boolean? = null)
var subscribed: Boolean? = null
)

View File

@ -1,11 +1,11 @@
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 Subscription( data class Subscription(
var url: String? = null, val url: String,
var name: String? = null, val name: String,
var avatar: String? = null, val avatar: String,
var verified: Boolean? = null val verified: Boolean
) )

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

@ -1,9 +1,9 @@
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 Token( data class Token(
var token: String? = null, val token: String? = null,
var error: String? = null val error: 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

@ -3,7 +3,9 @@ package com.github.libretube.db.obj
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "customInstance") @Entity(tableName = "customInstance")
class CustomInstance( class CustomInstance(
@PrimaryKey var name: String = "", @PrimaryKey var name: String = "",

View File

@ -2,7 +2,9 @@ package com.github.libretube.db.obj
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity @Entity
data class LocalPlaylist( data class LocalPlaylist(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)

View File

@ -3,7 +3,9 @@ package com.github.libretube.db.obj
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity @Entity
data class LocalPlaylistItem( data class LocalPlaylistItem(
@PrimaryKey(autoGenerate = true) val id: Int = 0, @PrimaryKey(autoGenerate = true) val id: Int = 0,

View File

@ -2,7 +2,9 @@ package com.github.libretube.db.obj
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
import kotlinx.serialization.Serializable
@Serializable
data class LocalPlaylistWithVideos( data class LocalPlaylistWithVideos(
@Embedded val playlist: LocalPlaylist = LocalPlaylist(), @Embedded val playlist: LocalPlaylist = LocalPlaylist(),
@Relation( @Relation(

View File

@ -2,7 +2,9 @@ package com.github.libretube.db.obj
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "localSubscription") @Entity(tableName = "localSubscription")
data class LocalSubscription( data class LocalSubscription(
@PrimaryKey val channelId: String = "" @PrimaryKey val channelId: String = ""

View File

@ -2,7 +2,9 @@ package com.github.libretube.db.obj
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "playlistBookmark") @Entity(tableName = "playlistBookmark")
data class PlaylistBookmark( data class PlaylistBookmark(
@PrimaryKey @PrimaryKey

View File

@ -2,7 +2,9 @@ package com.github.libretube.db.obj
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "searchHistoryItem") @Entity(tableName = "searchHistoryItem")
data class SearchHistoryItem( data class SearchHistoryItem(
@PrimaryKey val query: String = "" @PrimaryKey val query: String = ""

View File

@ -3,7 +3,9 @@ package com.github.libretube.db.obj
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "watchHistoryItem") @Entity(tableName = "watchHistoryItem")
data class WatchHistoryItem( data class WatchHistoryItem(
@PrimaryKey val videoId: String = "", @PrimaryKey val videoId: String = "",

View File

@ -3,7 +3,9 @@ package com.github.libretube.db.obj
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "watchPosition") @Entity(tableName = "watchPosition")
data class WatchPosition( data class WatchPosition(
@PrimaryKey val videoId: String = "", @PrimaryKey val videoId: String = "",

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

@ -7,14 +7,16 @@ import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.db.obj.SearchHistoryItem import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition import com.github.libretube.db.obj.WatchPosition
import kotlinx.serialization.Serializable
@Serializable
data class BackupFile( data class BackupFile(
var watchHistory: List<WatchHistoryItem>? = null, var watchHistory: List<WatchHistoryItem> = emptyList(),
var watchPositions: List<WatchPosition>? = null, var watchPositions: List<WatchPosition> = emptyList(),
var searchHistory: List<SearchHistoryItem>? = null, var searchHistory: List<SearchHistoryItem> = emptyList(),
var localSubscriptions: List<LocalSubscription>? = null, var localSubscriptions: List<LocalSubscription> = emptyList(),
var customInstances: List<CustomInstance>? = null, var customInstances: List<CustomInstance> = emptyList(),
var playlistBookmarks: List<PlaylistBookmark>? = null, var playlistBookmarks: List<PlaylistBookmark> = emptyList(),
var localPlaylists: List<LocalPlaylistWithVideos>? = null, var localPlaylists: List<LocalPlaylistWithVideos> = emptyList(),
var preferences: List<PreferenceItem>? = null var preferences: List<PreferenceItem> = emptyList()
) )

View File

@ -1,5 +1,8 @@
package com.github.libretube.obj package com.github.libretube.obj
import kotlinx.serialization.Serializable
@Serializable
data class ImportPlaylist( data class ImportPlaylist(
var name: String? = null, var name: String? = null,
val type: String? = null, val type: String? = null,

View File

@ -1,7 +1,10 @@
package com.github.libretube.obj package com.github.libretube.obj
import kotlinx.serialization.Serializable
@Serializable
data class ImportPlaylistFile( data class ImportPlaylistFile(
val format: String? = null, val format: String,
val version: Int? = null, val version: Int,
val playlists: List<ImportPlaylist>? = null val playlists: List<ImportPlaylist> = emptyList()
) )

View File

@ -1,7 +1,11 @@
package com.github.libretube.obj package com.github.libretube.obj
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class NewPipeSubscription( data class NewPipeSubscription(
val name: String? = null, val name: String,
val service_id: Int? = null, @SerialName("service_id") val serviceId: Int,
val url: String? = null val url: String
) )

View File

@ -1,7 +1,11 @@
package com.github.libretube.obj package com.github.libretube.obj
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class NewPipeSubscriptions( data class NewPipeSubscriptions(
val app_version: String = "", @SerialName("app_version") val appVersion: String = "",
val app_version_int: Int = 0, @SerialName("app_version_int") val appVersionInt: Int = 0,
val subscriptions: List<NewPipeSubscription>? = null val subscriptions: List<NewPipeSubscription> = emptyList()
) )

View File

@ -1,6 +1,11 @@
package com.github.libretube.obj package com.github.libretube.obj
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
@Serializable
data class PreferenceItem( data class PreferenceItem(
val key: String? = null, val key: String? = null,
val value: Any? = null val value: JsonPrimitive = JsonNull
) )

View File

@ -1,20 +1,24 @@
package com.github.libretube.obj.update package com.github.libretube.obj.update
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
@JsonIgnoreProperties(ignoreUnknown = true) @Serializable
data class Asset( data class Asset(
val browser_download_url: String? = null, @SerialName("browser_download_url") val browserDownloadUrl: String,
val content_type: String? = null, @SerialName("content_type") val contentType: String,
val created_at: String? = null, @SerialName("created_at") val createdAt: Instant,
val download_count: Int? = null, @SerialName("download_count") val downloadCount: Int,
val id: Int? = null, val id: Int,
val label: Any? = null, val label: JsonElement = JsonNull,
val name: String? = null, val name: String,
val node_id: String? = null, @SerialName("node_id") val nodeId: String,
val size: Int? = null, val size: Int,
val state: String? = null, val state: String,
val updated_at: String? = null, @SerialName("updated_at") val updatedAt: Instant,
val uploader: Uploader? = null, val uploader: User,
val url: String? = null val url: String
) )

View File

@ -1,25 +0,0 @@
package com.github.libretube.obj.update
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Author(
val avatar_url: String? = null,
val events_url: String? = null,
val followers_url: String? = null,
val following_url: String? = null,
val gists_url: String? = null,
val gravatar_id: String? = null,
val html_url: String? = null,
val id: Int? = null,
val login: String? = null,
val node_id: String? = null,
val organizations_url: String? = null,
val received_events_url: String? = null,
val repos_url: String? = null,
val site_admin: Boolean? = null,
val starred_url: String? = null,
val subscriptions_url: String? = null,
val type: String? = null,
val url: String? = null
)

View File

@ -1,15 +1,16 @@
package com.github.libretube.obj.update package com.github.libretube.obj.update
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true) @Serializable
data class Reactions( data class Reactions(
val confused: Int? = null, val confused: Int,
val eyes: Int? = null, val eyes: Int,
val heart: Int? = null, val heart: Int,
val hooray: Int? = null, val hooray: Int,
val laugh: Int? = null, val laugh: Int,
val rocket: Int? = null, val rocket: Int,
val total_count: Int? = null, @SerialName("total_count") val totalCount: Int,
val url: String? = null val url: String
) )

View File

@ -1,27 +1,29 @@
package com.github.libretube.obj.update package com.github.libretube.obj.update
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@JsonIgnoreProperties(ignoreUnknown = true) @Serializable
data class UpdateInfo( data class UpdateInfo(
val assets: List<Asset>? = null, val assets: List<Asset> = emptyList(),
val assets_url: String? = null, @SerialName("assets_url") val assetsUrl: String,
val author: Author? = null, val author: User,
val body: String? = null, val body: String,
val created_at: String? = null, @SerialName("created_at") val createdAt: Instant,
val draft: Boolean? = null, val draft: Boolean,
val html_url: String? = null, @SerialName("html_url") val htmlUrl: String,
val id: Int? = null, val id: Int,
val mentions_count: Int? = null, @SerialName("mentions_count") val mentionsCount: Int,
val name: String? = null, val name: String,
val node_id: String? = null, @SerialName("node_id") val nodeId: String,
val prerelease: Boolean? = null, val prerelease: Boolean,
val published_at: String? = null, @SerialName("published_at") val publishedAt: Instant,
val reactions: Reactions? = null, val reactions: Reactions,
val tag_name: String? = null, @SerialName("tag_name") val tagName: String,
val tarball_url: String? = null, @SerialName("tarball_url") val tarballUrl: String,
val target_commitish: String? = null, @SerialName("target_commitish") val targetCommitish: String,
val upload_url: String? = null, @SerialName("upload_url") val uploadUrl: String,
val url: String? = null, val url: String,
val zipball_url: String? = null @SerialName("zipball_url") val zipballUrl: String
) )

View File

@ -1,25 +0,0 @@
package com.github.libretube.obj.update
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Uploader(
val avatar_url: String? = null,
val events_url: String? = null,
val followers_url: String? = null,
val following_url: String? = null,
val gists_url: String? = null,
val gravatar_id: String? = null,
val html_url: String? = null,
val id: Int? = null,
val login: String? = null,
val node_id: String? = null,
val organizations_url: String? = null,
val received_events_url: String? = null,
val repos_url: String? = null,
val site_admin: Boolean? = null,
val starred_url: String? = null,
val subscriptions_url: String? = null,
val type: String? = null,
val url: String? = null
)

View File

@ -0,0 +1,26 @@
package com.github.libretube.obj.update
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class User(
@SerialName("avatar_url") val avatarUrl: String,
@SerialName("events_url") val eventsUrl: String,
@SerialName("followers_url") val followersUrl: String,
@SerialName("following_url") val followingUrl: String,
@SerialName("gists_url") val gistsUrl: String,
@SerialName("gravatar_id") val gravatarId: String,
@SerialName("html_url") val htmlUrl: String,
val id: Int,
val login: String,
@SerialName("node_id") val nodeId: String,
@SerialName("organizations_url") val organizationsUrl: String,
@SerialName("received_events_url") val receivedEventsUrl: String,
@SerialName("repos_url") val reposUrl: String,
@SerialName("site_admin") val siteAdmin: Boolean,
@SerialName("starred_url") val starredUrl: String,
@SerialName("subscriptions_url") val subscriptionsUrl: String,
val type: String,
val url: String
)

View File

@ -13,8 +13,8 @@ import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.SegmentData import com.github.libretube.api.obj.SegmentData
@ -40,6 +40,7 @@ import com.google.android.exoplayer2.Player
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.serialization.encodeToString
/** /**
* Loads the selected videos audio in background mode with a notification area. * Loads the selected videos audio in background mode with a notification area.
@ -318,10 +319,9 @@ class BackgroundMode : Service() {
runCatching { runCatching {
val categories = PlayerHelper.getSponsorBlockCategories() val categories = PlayerHelper.getSponsorBlockCategories()
if (categories.isEmpty()) return@runCatching if (categories.isEmpty()) return@runCatching
segmentData = segmentData = RetrofitInstance.api.getSegments(
RetrofitInstance.api.getSegments(
videoId, videoId,
ObjectMapper().writeValueAsString(categories) JsonHelper.json.encodeToString(categories)
) )
checkForSegments() checkForSegments()
} }

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,
url, streams.thumbnailUrl,
thumbnailTargetFile.absolutePath thumbnailTargetFile.absolutePath
) )
}
val downloadItems = streams.toDownloadItems( val downloadItems = streams.toDownloadItems(
videoId, videoId,

View File

@ -66,22 +66,21 @@ class CommentsAdapter(
commentorImage.scaleY = REPLIES_ADAPTER_SCALE commentorImage.scaleY = REPLIES_ADAPTER_SCALE
} }
commentInfos.text = commentInfos.text = comment.author + TextUtils.SEPARATOR + comment.commentedTime
comment.author.toString() + TextUtils.SEPARATOR + comment.commentedTime.toString()
commentText.text = HtmlCompat.fromHtml( commentText.text = HtmlCompat.fromHtml(
comment.commentText.toString(), comment.commentText,
HtmlCompat.FROM_HTML_MODE_LEGACY HtmlCompat.FROM_HTML_MODE_LEGACY
) )
ImageHelper.loadImage(comment.thumbnail, commentorImage) ImageHelper.loadImage(comment.thumbnail, commentorImage)
likesTextView.text = comment.likeCount.formatShort() likesTextView.text = comment.likeCount.formatShort()
if (comment.verified == true) verifiedImageView.visibility = View.VISIBLE if (comment.verified) verifiedImageView.visibility = View.VISIBLE
if (comment.pinned == true) pinnedImageView.visibility = View.VISIBLE if (comment.pinned) pinnedImageView.visibility = View.VISIBLE
if (comment.hearted == true) heartedImageView.visibility = View.VISIBLE if (comment.hearted) heartedImageView.visibility = View.VISIBLE
if (comment.repliesPage != null) repliesAvailable.visibility = View.VISIBLE if (comment.repliesPage != null) repliesAvailable.visibility = View.VISIBLE
if ((comment.replyCount ?: -1L) > 0L) { if (comment.replyCount > 0L) {
repliesCount.text = comment.replyCount?.formatShort() repliesCount.text = comment.replyCount.formatShort()
} }
commentorImage.setOnClickListener { commentorImage.setOnClickListener {
@ -99,7 +98,7 @@ class CommentsAdapter(
} }
root.setOnLongClickListener { root.setOnLongClickListener {
ClipboardHelper(root.context).save(comment.commentText.toString()) ClipboardHelper(root.context).save(comment.commentText)
Toast.makeText(root.context, R.string.copied, Toast.LENGTH_SHORT).show() Toast.makeText(root.context, R.string.copied, Toast.LENGTH_SHORT).show()
true true
} }

View File

@ -83,13 +83,13 @@ class SearchAdapter(
private fun bindWatch(item: ContentItem, binding: VideoRowBinding) { private fun bindWatch(item: ContentItem, binding: VideoRowBinding) {
binding.apply { binding.apply {
ImageHelper.loadImage(item.thumbnail, thumbnail) ImageHelper.loadImage(item.thumbnail, thumbnail)
thumbnailDuration.setFormattedDuration(item.duration!!, item.isShort) thumbnailDuration.setFormattedDuration(item.duration, item.isShort)
ImageHelper.loadImage(item.uploaderAvatar, channelImage) ImageHelper.loadImage(item.uploaderAvatar, channelImage)
videoTitle.text = item.title videoTitle.text = item.title
val viewsString = if (item.views?.toInt() != -1) item.views.formatShort() else "" val viewsString = if (item.views != -1L) item.views.formatShort() else ""
val uploadDate = if (item.uploadedDate != null) item.uploadedDate else "" val uploadDate = item.uploadedDate.orEmpty()
videoInfo.text = videoInfo.text =
if (viewsString != "" && uploadDate != "") { if (viewsString.isNotEmpty() && uploadDate.isNotEmpty()) {
"$viewsString$uploadDate" "$viewsString$uploadDate"
} else { } else {
viewsString + uploadDate viewsString + uploadDate
@ -98,7 +98,7 @@ class SearchAdapter(
root.setOnClickListener { root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, item.url) NavigationHelper.navigateVideo(root.context, item.url)
} }
val videoId = item.url!!.toID() val videoId = item.url.toID()
val videoName = item.title!! val videoName = item.title!!
root.setOnLongClickListener { root.setOnLongClickListener {
VideoOptionsBottomSheet(videoId, videoName) VideoOptionsBottomSheet(videoId, videoName)
@ -111,7 +111,7 @@ class SearchAdapter(
channelContainer.setOnClickListener { channelContainer.setOnClickListener {
NavigationHelper.navigateChannel(root.context, item.uploaderUrl) NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
} }
watchProgress.setWatchProgressLength(videoId, item.duration!!) watchProgress.setWatchProgressLength(videoId, item.duration)
} }
} }
@ -135,12 +135,12 @@ class SearchAdapter(
} }
root.setOnLongClickListener { root.setOnLongClickListener {
ChannelOptionsBottomSheet(item.url!!.toID(), item.name) ChannelOptionsBottomSheet(item.url.toID(), item.name)
.show((root.context as BaseActivity).supportFragmentManager) .show((root.context as BaseActivity).supportFragmentManager)
true true
} }
binding.searchSubButton.setupSubscriptionButton(item.url?.toID(), item.name?.toID()) binding.searchSubButton.setupSubscriptionButton(item.url.toID(), item.name?.toID())
} }
} }
@ -150,7 +150,7 @@ class SearchAdapter(
) { ) {
binding.apply { binding.apply {
ImageHelper.loadImage(item.thumbnail, playlistThumbnail) ImageHelper.loadImage(item.thumbnail, playlistThumbnail)
if (item.videos?.toInt() != -1) videoCount.text = item.videos.toString() if (item.videos != -1L) videoCount.text = item.videos.toString()
playlistTitle.text = item.name playlistTitle.text = item.name
playlistDescription.text = item.uploaderName playlistDescription.text = item.uploaderName
root.setOnClickListener { root.setOnClickListener {
@ -158,7 +158,7 @@ class SearchAdapter(
} }
deletePlaylist.visibility = View.GONE deletePlaylist.visibility = View.GONE
root.setOnLongClickListener { root.setOnLongClickListener {
val playlistId = item.url!!.toID() val playlistId = item.url.toID()
val playlistName = item.name!! val playlistName = item.name!!
PlaylistOptionsBottomSheet(playlistId, playlistName, PlaylistType.PUBLIC) PlaylistOptionsBottomSheet(playlistId, playlistName, PlaylistType.PUBLIC)
.show( .show(

View File

@ -11,6 +11,8 @@ import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem import com.github.libretube.obj.PreferenceItem
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
class BackupDialog( class BackupDialog(
private val createBackupFile: (BackupFile) -> Unit private val createBackupFile: (BackupFile) -> Unit
@ -45,8 +47,14 @@ class BackupDialog(
}) })
object Preferences : BackupOption(R.string.preferences, onSelected = { file -> object Preferences : BackupOption(R.string.preferences, onSelected = { file ->
file.preferences = PreferenceHelper.settings.all.map { file.preferences = PreferenceHelper.settings.all.map { (key, value) ->
PreferenceItem(it.key, it.value) val jsonValue = when (value) {
is Number -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is String -> JsonPrimitive(value)
else -> JsonNull
}
PreferenceItem(key, jsonValue)
} }
}) })
} }

View File

@ -8,6 +8,7 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.DeleteUserRequest
import com.github.libretube.databinding.DialogDeleteAccountBinding import com.github.libretube.databinding.DialogDeleteAccountBinding
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
@ -41,10 +42,7 @@ class DeleteAccountDialog : DialogFragment() {
val token = PreferenceHelper.getToken() val token = PreferenceHelper.getToken()
try { try {
RetrofitInstance.authApi.deleteAccount( RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password))
token,
com.github.libretube.api.obj.DeleteUserRequest(password)
)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()

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

@ -92,8 +92,8 @@ class LoginDialog : DialogFragment() {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
PreferenceHelper.setToken(response.token!!) PreferenceHelper.setToken(response.token)
PreferenceHelper.setUsername(login.username!!) PreferenceHelper.setUsername(login.username)
dialog?.dismiss() dialog?.dismiss()
activity?.recreate() activity?.recreate()

View File

@ -29,7 +29,7 @@ class UpdateDialog(
intent.putExtra("downloadUrl", downloadUrl) intent.putExtra("downloadUrl", downloadUrl)
context?.startService(intent) context?.startService(intent)
} else { } else {
val uri = Uri.parse(updateInfo.html_url) val uri = Uri.parse(updateInfo.htmlUrl)
val intent = Intent(Intent.ACTION_VIEW).setData(uri) val intent = Intent(Intent.ACTION_VIEW).setData(uri)
startActivity(intent) startActivity(intent)
} }
@ -40,8 +40,10 @@ class UpdateDialog(
private fun getDownloadUrl(updateInfo: UpdateInfo): String? { private fun getDownloadUrl(updateInfo: UpdateInfo): String? {
val supportedArchitectures = Build.SUPPORTED_ABIS val supportedArchitectures = Build.SUPPORTED_ABIS
supportedArchitectures.forEach { arch -> supportedArchitectures.forEach { arch ->
updateInfo.assets?.forEach { asset -> updateInfo.assets.forEach { asset ->
if (asset.name?.contains(arch) == true) return asset.browser_download_url if (asset.name.contains(arch)) {
return asset.browserDownloadUrl
}
} }
} }
return null return null

View File

@ -142,7 +142,7 @@ class ChannelFragment : BaseFragment() {
binding.channelShare.setOnClickListener { binding.channelShare.setOnClickListener {
val shareDialog = ShareDialog( val shareDialog = ShareDialog(
response.id!!.toID(), response.id.toID(),
ShareObjectType.CHANNEL, ShareObjectType.CHANNEL,
shareData shareData
) )
@ -169,10 +169,10 @@ class ChannelFragment : BaseFragment() {
R.string.subscribers, R.string.subscribers,
response.subscriberCount.formatShort() response.subscriberCount.formatShort()
) )
if (response.description?.trim() == "") { if (response.description.isBlank()) {
binding.channelDescription.visibility = View.GONE binding.channelDescription.visibility = View.GONE
} else { } else {
binding.channelDescription.text = response.description?.trim() binding.channelDescription.text = response.description.trim()
} }
binding.channelDescription.setOnClickListener { binding.channelDescription.setOnClickListener {
@ -186,13 +186,13 @@ class ChannelFragment : BaseFragment() {
// recyclerview of the videos by the channel // recyclerview of the videos by the channel
channelAdapter = VideosAdapter( channelAdapter = VideosAdapter(
response.relatedStreams.orEmpty().toMutableList(), response.relatedStreams.toMutableList(),
forceMode = VideosAdapter.Companion.ForceMode.CHANNEL forceMode = VideosAdapter.Companion.ForceMode.CHANNEL
) )
binding.channelRecView.adapter = channelAdapter binding.channelRecView.adapter = channelAdapter
} }
response.tabs?.let { setupTabs(it) } setupTabs(response.tabs)
} }
} }
@ -230,16 +230,13 @@ class ChannelFragment : BaseFragment() {
private fun loadTab(tab: ChannelTab) { private fun loadTab(tab: ChannelTab) {
scope.launch { scope.launch {
tab.data ?: return@launch
val response = try { val response = try {
RetrofitInstance.api.getChannelTab(tab.data) RetrofitInstance.api.getChannelTab(tab.data)
} catch (e: Exception) { } catch (e: Exception) {
return@launch return@launch
} }
val adapter = SearchAdapter( val adapter = SearchAdapter(response.content.toMutableList())
response.content.toMutableList()
)
runOnUiThread { runOnUiThread {
binding.channelRecView.adapter = adapter binding.channelRecView.adapter = adapter
@ -275,7 +272,7 @@ class ChannelFragment : BaseFragment() {
return@launchWhenCreated return@launchWhenCreated
} }
nextPage = response.nextpage nextPage = response.nextpage
channelAdapter?.insertItems(response.relatedStreams.orEmpty()) channelAdapter?.insertItems(response.relatedStreams)
isLoading = false isLoading = false
binding.channelRefresh.isRefreshing = false binding.channelRefresh.isRefreshing = false
} }
@ -291,9 +288,9 @@ class ChannelFragment : BaseFragment() {
) { ) {
scope.launch { scope.launch {
val newContent = try { val newContent = try {
RetrofitInstance.api.getChannelTab(tab.data ?: "", nextPage) RetrofitInstance.api.getChannelTab(tab.data, nextPage)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(TAG(), "Exception: $e")
null null
} }
onNewNextPage.invoke(newContent?.nextpage) onNewNextPage.invoke(newContent?.nextpage)

View File

@ -37,9 +37,9 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.CronetHelper import com.github.libretube.api.CronetHelper
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.ChapterSegment import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.PipedStream
@ -119,6 +119,8 @@ 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 kotlinx.serialization.encodeToString
import org.chromium.net.CronetEngine import org.chromium.net.CronetEngine
import retrofit2.HttpException import retrofit2.HttpException
@ -685,9 +687,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()
)
} }
} }
} }
@ -748,7 +748,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
segmentData = segmentData =
RetrofitInstance.api.getSegments( RetrofitInstance.api.getSegments(
videoId!!, videoId!!,
ObjectMapper().writeValueAsString(categories) JsonHelper.json.encodeToString(categories)
) )
if (segmentData.segments.isEmpty()) return@runCatching if (segmentData.segments.isEmpty()) return@runCatching
playerBinding.exoProgress.setSegments(segmentData.segments) playerBinding.exoProgress.setSegments(segmentData.segments)
@ -776,8 +776,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
@ -797,7 +796,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)
} }
} }
@ -826,7 +825,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 +869,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 +882,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 +969,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 +992,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
setupDescription(binding.playerDescription, description) setupDescription(binding.playerDescription, description)
@ -1009,7 +1006,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
) )
@ -1268,9 +1265,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).
@ -1300,7 +1297,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()
@ -1377,16 +1374,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!!
} }
@ -1515,9 +1510,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

@ -109,7 +109,7 @@ class PlaylistFragment : BaseFragment() {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
return@launchWhenCreated return@launchWhenCreated
} }
playlistFeed = response.relatedStreams.orEmpty().toMutableList() playlistFeed = response.relatedStreams.toMutableList()
binding.playlistScrollview.visibility = View.VISIBLE binding.playlistScrollview.visibility = View.VISIBLE
nextPage = response.nextpage nextPage = response.nextpage
playlistName = response.name playlistName = response.name
@ -140,7 +140,7 @@ class PlaylistFragment : BaseFragment() {
if (playlistFeed.isEmpty()) return@setOnClickListener if (playlistFeed.isEmpty()) return@setOnClickListener
NavigationHelper.navigateVideo( NavigationHelper.navigateVideo(
requireContext(), requireContext(),
response.relatedStreams!!.first().url?.toID(), response.relatedStreams.first().url?.toID(),
playlistId playlistId
) )
} }
@ -279,15 +279,9 @@ class PlaylistFragment : BaseFragment() {
val response = try { val response = try {
// load locally stored playlists with the auth api // load locally stored playlists with the auth api
if (playlistType == PlaylistType.PRIVATE) { if (playlistType == PlaylistType.PRIVATE) {
RetrofitInstance.authApi.getPlaylistNextPage( RetrofitInstance.authApi.getPlaylistNextPage(playlistId!!, nextPage!!)
playlistId!!,
nextPage!!
)
} else { } else {
RetrofitInstance.api.getPlaylistNextPage( RetrofitInstance.api.getPlaylistNextPage(playlistId!!, nextPage!!)
playlistId!!,
nextPage!!
)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
@ -295,7 +289,7 @@ class PlaylistFragment : BaseFragment() {
} }
nextPage = response.nextpage nextPage = response.nextpage
playlistAdapter?.updateItems(response.relatedStreams!!) playlistAdapter?.updateItems(response.relatedStreams)
isLoading = false isLoading = false
} }
} }

View File

@ -5,6 +5,7 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
@ -94,13 +95,9 @@ class SearchResultFragment : BaseFragment() {
return@launchWhenCreated return@launchWhenCreated
} }
runOnUiThread { runOnUiThread {
searchAdapter = SearchAdapter(response.items.orEmpty().toMutableList()) searchAdapter = SearchAdapter(response.items.toMutableList())
binding.searchRecycler.adapter = searchAdapter binding.searchRecycler.adapter = searchAdapter
binding.noSearchResult.visibility = if (response.items.orEmpty().isEmpty()) { binding.noSearchResult.isVisible = response.items.isEmpty()
View.VISIBLE
} else {
View.GONE
}
} }
nextPage = response.nextpage nextPage = response.nextpage
} }
@ -124,8 +121,8 @@ class SearchResultFragment : BaseFragment() {
} }
nextPage = response.nextpage!! nextPage = response.nextpage!!
kotlin.runCatching { kotlin.runCatching {
if (response.items?.isNotEmpty() == true) { if (response.items.isNotEmpty()) {
searchAdapter.updateItems(response.items.toMutableList()) searchAdapter.updateItems(response.items)
} }
} }
} }

View File

@ -48,7 +48,7 @@ class CommentsViewModel : ViewModel() {
return@launch return@launch
} }
val updatedPage = commentsPage.value?.apply { val updatedPage = commentsPage.value?.apply {
comments = comments.plus(response.comments).toMutableList() comments += response.comments
} }
nextPage = response.nextpage nextPage = response.nextpage
commentsPage.postValue(updatedPage) commentsPage.postValue(updatedPage)

View File

@ -68,7 +68,7 @@ class BackupRestoreSettings : BasePreferenceFragment() {
createBackupFile = registerForActivityResult( createBackupFile = registerForActivityResult(
CreateDocument("application/json") CreateDocument("application/json")
) { uri: Uri? -> ) { uri: Uri? ->
BackupHelper(requireContext()).advancedBackup(uri, backupFile) BackupHelper(requireContext()).createAdvancedBackup(uri, backupFile)
} }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -137,8 +137,8 @@ class InstanceSettings : BasePreferenceFragment() {
response?.sortBy { it.name } response?.sortBy { it.name }
instanceNames.addAll(response.orEmpty().map { it.name ?: "" }) instanceNames.addAll(response.orEmpty().map { it.name })
instanceValues.addAll(response.orEmpty().map { it.api_url ?: "" }) instanceValues.addAll(response.orEmpty().map { it.apiUrl })
customInstances.forEach { instance -> customInstances.forEach { instance ->
instanceNames += instance.name instanceNames += instance.name

View File

@ -20,7 +20,7 @@ import kotlinx.coroutines.runBlocking
*/ */
class ChannelOptionsBottomSheet( class ChannelOptionsBottomSheet(
private val channelId: String, private val channelId: String,
private val channelName: String? channelName: String?
) : BaseBottomSheet() { ) : BaseBottomSheet() {
private val shareData = ShareData(currentChannel = channelName) private val shareData = ShareData(currentChannel = channelName)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -44,7 +44,7 @@ class ChannelOptionsBottomSheet(
val channel = runBlocking { val channel = runBlocking {
RetrofitInstance.api.getChannel(channelId) RetrofitInstance.api.getChannel(channelId)
} }
channel.relatedStreams?.firstOrNull()?.url?.toID()?.let { channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
NavigationHelper.navigateVideo( NavigationHelper.navigateVideo(
requireContext(), requireContext(),
it, it,
@ -60,7 +60,7 @@ class ChannelOptionsBottomSheet(
val channel = runBlocking { val channel = runBlocking {
RetrofitInstance.api.getChannel(channelId) RetrofitInstance.api.getChannel(channelId)
} }
channel.relatedStreams?.firstOrNull()?.url?.toID()?.let { channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
BackgroundHelper.playOnBackground( BackgroundHelper.playOnBackground(
requireContext(), requireContext(),
videoId = it, videoId = it,

View File

@ -2,15 +2,22 @@ package com.github.libretube.util
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.fasterxml.jackson.databind.ObjectMapper import com.github.libretube.api.JsonHelper
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.query import com.github.libretube.extensions.query
import com.github.libretube.obj.BackupFile import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem import com.github.libretube.obj.PreferenceItem
import java.io.FileOutputStream import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import kotlinx.serialization.json.floatOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.longOrNull
/** /**
* Backup and restore the preferences * Backup and restore the preferences
@ -19,18 +26,15 @@ class BackupHelper(private val context: Context) {
/** /**
* Write a [BackupFile] containing the database content as well as the preferences * Write a [BackupFile] containing the database content as well as the preferences
*/ */
fun advancedBackup(uri: Uri?, backupFile: BackupFile) { fun createAdvancedBackup(uri: Uri?, backupFile: BackupFile) {
if (uri == null) return uri?.let {
try { try {
context.contentResolver.openFileDescriptor(uri, "w")?.use { context.contentResolver.openOutputStream(it)?.use { outputStream ->
FileOutputStream(it.fileDescriptor).use { fileOutputStream -> JsonHelper.json.encodeToStream(backupFile, outputStream)
fileOutputStream.write(
ObjectMapper().writeValueAsBytes(backupFile)
)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(TAG(), "Error while writing backup: $e")
}
} }
} }
@ -38,36 +42,33 @@ class BackupHelper(private val context: Context) {
* Restore data from a [BackupFile] * Restore data from a [BackupFile]
*/ */
fun restoreAdvancedBackup(uri: Uri?) { fun restoreAdvancedBackup(uri: Uri?) {
if (uri == null) return val backupFile = uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream ->
val mapper = ObjectMapper() JsonHelper.json.decodeFromStream<BackupFile>(inputStream)
val json = context.contentResolver.openInputStream(uri)?.use { }
it.bufferedReader().use { reader -> reader.readText() } } ?: return
}.orEmpty()
val backupFile = mapper.readValue(json, BackupFile::class.java)
query { query {
Database.watchHistoryDao().insertAll( Database.watchHistoryDao().insertAll(
*backupFile.watchHistory.orEmpty().toTypedArray() *backupFile.watchHistory.toTypedArray()
) )
Database.searchHistoryDao().insertAll( Database.searchHistoryDao().insertAll(
*backupFile.searchHistory.orEmpty().toTypedArray() *backupFile.searchHistory.toTypedArray()
) )
Database.watchPositionDao().insertAll( Database.watchPositionDao().insertAll(
*backupFile.watchPositions.orEmpty().toTypedArray() *backupFile.watchPositions.toTypedArray()
) )
Database.localSubscriptionDao().insertAll( Database.localSubscriptionDao().insertAll(
*backupFile.localSubscriptions.orEmpty().toTypedArray() *backupFile.localSubscriptions.toTypedArray()
) )
Database.customInstanceDao().insertAll( Database.customInstanceDao().insertAll(
*backupFile.customInstances.orEmpty().toTypedArray() *backupFile.customInstances.toTypedArray()
) )
Database.playlistBookmarkDao().insertAll( Database.playlistBookmarkDao().insertAll(
*backupFile.playlistBookmarks.orEmpty().toTypedArray() *backupFile.playlistBookmarks.toTypedArray()
) )
backupFile.localPlaylists?.forEach { backupFile.localPlaylists.forEach {
Database.localPlaylistsDao().createPlaylist(it.playlist) Database.localPlaylistsDao().createPlaylist(it.playlist)
val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id
it.videos.forEach { it.videos.forEach {
@ -90,18 +91,26 @@ class BackupHelper(private val context: Context) {
clear() clear()
// decide for each preference which type it is and save it to the preferences // decide for each preference which type it is and save it to the preferences
preferences.forEach { preferences.forEach { (key, jsonValue) ->
when (it.value) { val value = if (jsonValue.isString) {
is Boolean -> putBoolean(it.key, it.value) jsonValue.content
is Float -> putFloat(it.key, it.value) } else {
is Long -> putLong(it.key, it.value) jsonValue.booleanOrNull
?: jsonValue.intOrNull
?: jsonValue.longOrNull
?: jsonValue.floatOrNull
}
when (value) {
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
is Long -> putLong(key, value)
is Int -> { is Int -> {
when (it.key) { when (key) {
PreferenceKeys.START_FRAGMENT -> putInt(it.key, it.value) PreferenceKeys.START_FRAGMENT -> putInt(key, value)
else -> putLong(it.key, it.value.toLong()) else -> putLong(key, value.toLong())
} }
} }
is String -> putString(it.key, it.value) is String -> putString(key, value)
} }
} }
} }

View File

@ -4,8 +4,8 @@ import android.app.Activity
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.PlaylistsHelper import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
@ -15,11 +15,13 @@ import com.github.libretube.obj.ImportPlaylist
import com.github.libretube.obj.ImportPlaylistFile import com.github.libretube.obj.ImportPlaylistFile
import com.github.libretube.obj.NewPipeSubscription import com.github.libretube.obj.NewPipeSubscription
import com.github.libretube.obj.NewPipeSubscriptions import com.github.libretube.obj.NewPipeSubscriptions
import java.io.FileOutputStream
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.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import okio.use
class ImportHelper( class ImportHelper(
private val activity: Activity private val activity: Activity
@ -56,12 +58,11 @@ class ImportHelper(
return when (val fileType = activity.contentResolver.getType(uri)) { return when (val fileType = activity.contentResolver.getType(uri)) {
"application/json", "application/*", "application/octet-stream" -> { "application/json", "application/*", "application/octet-stream" -> {
// NewPipe subscriptions format // NewPipe subscriptions format
val subscriptions = ObjectMapper().readValue( val subscriptions = activity.contentResolver.openInputStream(uri)?.use {
uri.readText(), JsonHelper.json.decodeFromStream<NewPipeSubscriptions>(it)
NewPipeSubscriptions::class.java }
) subscriptions?.subscriptions.orEmpty().map {
subscriptions.subscriptions.orEmpty().map { it.url.replace("https://www.youtube.com/channel/", "")
it.url!!.replace("https://www.youtube.com/channel/", "")
} }
} }
"text/csv", "text/comma-separated-values" -> { "text/csv", "text/comma-separated-values" -> {
@ -91,20 +92,14 @@ class ImportHelper(
SubscriptionHelper.getFormattedLocalSubscriptions() SubscriptionHelper.getFormattedLocalSubscriptions()
) )
} }
val newPipeChannels = mutableListOf<NewPipeSubscription>() val newPipeChannels = subs.map {
subs.forEach { NewPipeSubscription(it.name, 0, "https://www.youtube.com${it.url}")
newPipeChannels += NewPipeSubscription(
name = it.name,
service_id = 0,
url = "https://www.youtube.com" + it.url
)
} }
val newPipeSubscriptions = NewPipeSubscriptions(subscriptions = newPipeChannels)
val newPipeSubscriptions = NewPipeSubscriptions( activity.contentResolver.openOutputStream(uri)?.use {
subscriptions = newPipeChannels JsonHelper.json.encodeToStream(newPipeSubscriptions, it)
) }
uri.write(newPipeSubscriptions)
activity.toastFromMainThread(R.string.exportsuccess) activity.toastFromMainThread(R.string.exportsuccess)
} }
@ -134,11 +129,10 @@ class ImportHelper(
} }
} }
"application/json", "application/*", "application/octet-stream" -> { "application/json", "application/*", "application/octet-stream" -> {
val playlistFile = ObjectMapper().readValue( val playlistFile = activity.contentResolver.openInputStream(uri)?.use {
uri.readText(), JsonHelper.json.decodeFromStream<ImportPlaylistFile>(it)
ImportPlaylistFile::class.java }
) importPlaylists.addAll(playlistFile?.playlists.orEmpty())
importPlaylists.addAll(playlistFile.playlists.orEmpty())
} }
else -> { else -> {
activity.applicationContext.toastFromMainThread("Unsupported file type $fileType") activity.applicationContext.toastFromMainThread("Unsupported file type $fileType")
@ -167,31 +161,13 @@ class ImportHelper(
runBlocking { runBlocking {
val playlists = PlaylistsHelper.exportPlaylists() val playlists = PlaylistsHelper.exportPlaylists()
val playlistFile = ImportPlaylistFile( val playlistFile = ImportPlaylistFile("Piped", 1, playlists)
format = "Piped",
version = 1,
playlists = playlists
)
uri.write(playlistFile) activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(playlistFile, it)
}
activity.toastFromMainThread(R.string.exportsuccess) activity.toastFromMainThread(R.string.exportsuccess)
} }
} }
private fun Uri.readText(): String {
return activity.contentResolver.openInputStream(this)?.use {
it.bufferedReader().use { reader -> reader.readText() }
}.orEmpty()
}
private fun Uri.write(text: Any) {
activity.contentResolver.openFileDescriptor(this, "w")?.use {
FileOutputStream(it.fileDescriptor).use { fileOutputStream ->
fileOutputStream.write(
ObjectMapper().writeValueAsBytes(text)
)
}
}
}
} }

View File

@ -1,48 +0,0 @@
package com.github.libretube.util
import android.content.Context
import android.net.Uri
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.api.obj.Streams
import java.io.File
import java.io.FileOutputStream
class MetadataHelper(
private val context: Context
) {
private val mapper = ObjectMapper()
private val metadataDir = DownloadHelper.getDownloadDir(context, DownloadHelper.METADATA_DIR)
fun createMetadata(fileName: String, streams: Streams) {
val targetFile = File(metadataDir, fileName)
targetFile.createNewFile()
context.contentResolver.openFileDescriptor(
Uri.fromFile(targetFile),
"w"
)?.use {
FileOutputStream(it.fileDescriptor).use { fileOutputStream ->
fileOutputStream.write(
mapper.writeValueAsBytes(
streams
)
)
}
}
}
fun getMetadata(fileName: String): Streams? {
val sourceFile = File(metadataDir, fileName)
return try {
val json = context.contentResolver.openInputStream(
Uri.fromFile(sourceFile)
)?.use {
it.bufferedReader().use { reader -> reader.readText() }
}
mapper.readValue(json, Streams::class.java)
} catch (e: Exception) {
return null
}
}
}

View File

@ -147,7 +147,7 @@ object PlayingQueue {
scope.launch { scope.launch {
while (channelNextPage != null) { while (channelNextPage != null) {
RetrofitInstance.api.getChannelNextPage(channelId, nextPage!!).apply { RetrofitInstance.api.getChannelNextPage(channelId, nextPage!!).apply {
add(*relatedStreams.orEmpty().toTypedArray()) add(*relatedStreams.toTypedArray())
channelNextPage = this.nextpage channelNextPage = this.nextpage
} }
} }
@ -158,7 +158,7 @@ object PlayingQueue {
scope.launch { scope.launch {
runCatching { runCatching {
val channel = RetrofitInstance.api.getChannel(channelId) val channel = RetrofitInstance.api.getChannel(channelId)
add(*channel.relatedStreams.orEmpty().toTypedArray()) add(*channel.relatedStreams.toTypedArray())
updateCurrent(newCurrentStream) updateCurrent(newCurrentStream)
if (channel.nextpage == null) return@launch if (channel.nextpage == null) return@launch
fetchMoreFromChannel(channelId, channel.nextpage) fetchMoreFromChannel(channelId, channel.nextpage)

View File

@ -2,10 +2,11 @@ package com.github.libretube.util
import android.net.Uri import android.net.Uri
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 {
/** /**
@ -42,16 +43,9 @@ 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)
} }
/** /**

View File

@ -5,13 +5,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.7.22'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.4.0' classpath 'com.android.tools.build:gradle:7.4.0'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.22' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@ -11,13 +11,15 @@ espresso = "3.5.1"
workRuntime = "2.7.1" workRuntime = "2.7.1"
exoplayer = "2.18.2" exoplayer = "2.18.2"
retrofit = "2.9.0" retrofit = "2.9.0"
jacksonAnnotations = "2.13.4"
desugaring = "2.0.0" desugaring = "2.0.0"
cronetEmbedded = "108.5359.79" cronetEmbedded = "108.5359.79"
cronetOkHttp = "0.1.0" cronetOkHttp = "0.1.0"
coil = "2.2.2" coil = "2.2.2"
leakcanary = "2.10" leakcanary = "2.10"
room = "2.5.0" room = "2.5.0"
kotlinxSerialization = "1.4.1"
kotlinxDatetime = "0.4.0"
kotlinxRetrofit = "0.8.0"
[libraries] [libraries]
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
@ -33,8 +35,6 @@ androidx-work-runtime = { group = "androidx.work", name="work-runtime-ktx", vers
exoplayer = { group = "com.google.android.exoplayer", name = "exoplayer", version.ref = "exoplayer" } exoplayer = { group = "com.google.android.exoplayer", name = "exoplayer", version.ref = "exoplayer" }
exoplayer-extension-mediasession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" } exoplayer-extension-mediasession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" }
square-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } square-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
square-retrofit-converterJackson = { group = "com.squareup.retrofit2", name = "converter-jackson", version.ref = "retrofit" }
jacksonAnnotations = { group = "com.fasterxml.jackson.core", name = "jackson-annotations", version.ref = "jacksonAnnotations" }
desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugaring" } desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugaring" }
exoplayer-extension-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" } exoplayer-extension-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" }
exoplayer-dash = { group = "com.google.android.exoplayer", name = "exoplayer-dash", version.ref = "exoplayer" } exoplayer-dash = { group = "com.google.android.exoplayer", name = "exoplayer-dash", version.ref = "exoplayer" }
@ -47,3 +47,6 @@ lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-kt
lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" } lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
room = { group = "androidx.room", name="room-runtime", version.ref = "room" } room = { group = "androidx.room", name="room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-retrofit = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "kotlinxRetrofit" }