Merge pull request #2442 from Bnyro/master

Improve the playlist cloning algorithm
This commit is contained in:
Bnyro 2022-12-19 16:58:59 +01:00 committed by GitHub
commit b98e6f5965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 306 additions and 142 deletions

View File

@ -6,6 +6,7 @@ import com.github.libretube.R
import com.github.libretube.api.obj.Playlist import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.PlaylistId import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.api.obj.Playlists import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.YOUTUBE_FRONTEND_URL import com.github.libretube.constants.YOUTUBE_FRONTEND_URL
import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist import com.github.libretube.db.obj.LocalPlaylist
@ -19,13 +20,13 @@ import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.obj.ImportPlaylist import com.github.libretube.obj.ImportPlaylist
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ProxyHelper import com.github.libretube.util.ProxyHelper
import java.io.IOException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException
object PlaylistsHelper { object PlaylistsHelper {
private val pipedPlaylistRegex = "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}".toRegex() private val pipedPlaylistRegex = "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}".toRegex()
@ -110,16 +111,17 @@ object PlaylistsHelper {
return null return null
} }
suspend fun addToPlaylist(playlistId: String, vararg videoIds: String): Boolean { suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean {
if (!loggedIn) { if (!loggedIn) {
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll() val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId } .first { it.playlist.id.toString() == playlistId }
for (videoId in videoIds) { for (video in videos) {
val localPlaylistItem = RetrofitInstance.api.getStreams(videoId).toLocalPlaylistItem(playlistId, videoId) val localPlaylistItem = video.toLocalPlaylistItem(playlistId)
awaitQuery { awaitQuery {
// avoid duplicated videos in a playlist // avoid duplicated videos in a playlist
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByVideoId(playlistId, videoId) DatabaseHolder.Database.localPlaylistsDao()
.deletePlaylistItemsByVideoId(playlistId, localPlaylistItem.videoId)
// add the new video to the database // add the new video to the database
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem) DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
@ -128,7 +130,9 @@ object PlaylistsHelper {
// set the new playlist thumbnail URL // set the new playlist thumbnail URL
localPlaylistItem.thumbnailUrl?.let { localPlaylistItem.thumbnailUrl?.let {
localPlaylist.playlist.thumbnailUrl = it localPlaylist.playlist.thumbnailUrl = it
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(localPlaylist.playlist) DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(
localPlaylist.playlist
)
} }
} }
} }
@ -140,7 +144,7 @@ object PlaylistsHelper {
token, token,
PlaylistId( PlaylistId(
playlistId = playlistId, playlistId = playlistId,
videoIds = videoIds.toList() videoIds = videos.toList().map { it.url!!.toID() }
) )
).message == "ok" ).message == "ok"
} }
@ -172,13 +176,19 @@ object PlaylistsHelper {
DatabaseHolder.Database.localPlaylistsDao().getAll() DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId } }.first { it.playlist.id.toString() == playlistId }
awaitQuery { awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(transaction.videos[index]) DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(
transaction.videos[index]
)
} }
if (transaction.videos.size > 1) { if (transaction.videos.size > 1) {
if (index == 0) { if (index == 0) {
transaction.videos[1].thumbnailUrl?.let { transaction.playlist.thumbnailUrl = it } transaction.videos[1].thumbnailUrl?.let {
transaction.playlist.thumbnailUrl = it
}
awaitQuery { awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist) DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(
transaction.playlist
)
} }
} }
return return
@ -203,12 +213,28 @@ object PlaylistsHelper {
suspend fun importPlaylists(appContext: Context, playlists: List<ImportPlaylist>) { suspend fun importPlaylists(appContext: Context, playlists: List<ImportPlaylist>) {
for (playlist in playlists) { for (playlist in playlists) {
val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue
// if logged in, add the playlists by their ID via an api call
val success: Boolean = if (loggedIn) {
addToPlaylist( addToPlaylist(
playlistId, playlistId,
*playlist.videos.map { *playlist.videos.map {
it.substring(it.length - 11, it.length) StreamItem(url = it)
}.toTypedArray() }.toTypedArray()
) )
} else {
// if not logged in, all video information needs to become fetched manually
try {
val streamItems = playlist.videos.map {
RetrofitInstance.api.getStreams(it).toStreamItem(it)
}
addToPlaylist(playlistId, *streamItems.toTypedArray())
} catch (e: Exception) {
false
}
}
appContext.toastFromMainThread(
if (success) R.string.importsuccess else R.string.server_error
)
} }
} }
@ -253,9 +279,7 @@ object PlaylistsHelper {
addToPlaylist( addToPlaylist(
newPlaylist, newPlaylist,
*playlist.relatedStreams.orEmpty() *playlist.relatedStreams.orEmpty().toTypedArray()
.map { it.url!!.toID() }
.toTypedArray()
) )
var nextPage = playlist.nextpage var nextPage = playlist.nextpage
@ -264,9 +288,7 @@ object PlaylistsHelper {
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage).apply { RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage).apply {
addToPlaylist( addToPlaylist(
newPlaylist, newPlaylist,
*relatedStreams.orEmpty() *relatedStreams.orEmpty().toTypedArray()
.map { it.url!!.toID() }
.toTypedArray()
) )
}.nextpage }.nextpage
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -61,7 +61,12 @@ object SubscriptionHelper {
} }
} }
fun handleUnsubscribe(context: Context, channelId: String, channelName: String?, onUnsubscribe: () -> Unit) { fun handleUnsubscribe(
context: Context,
channelId: String,
channelName: String?,
onUnsubscribe: () -> Unit
) {
if (!PreferenceHelper.getBoolean(PreferenceKeys.CONFIRM_UNSUBSCRIBE, false)) { if (!PreferenceHelper.getBoolean(PreferenceKeys.CONFIRM_UNSUBSCRIBE, false)) {
unsubscribe(channelId) unsubscribe(channelId)
onUnsubscribe.invoke() onUnsubscribe.invoke()

View File

@ -4,6 +4,8 @@ import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun Float.round(decimalPlaces: Int): Float { fun Float.round(decimalPlaces: Int): Float {
return (this * 10.0.pow(decimalPlaces.toDouble())).roundToInt() / 10.0.pow(decimalPlaces.toDouble()) return (this * 10.0.pow(decimalPlaces.toDouble())).roundToInt() / 10.0.pow(
decimalPlaces.toDouble()
)
.toFloat() .toFloat()
} }

View File

@ -1,18 +1,18 @@
package com.github.libretube.extensions package com.github.libretube.extensions
import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.StreamItem
import com.github.libretube.db.obj.LocalPlaylistItem import com.github.libretube.db.obj.LocalPlaylistItem
fun Streams.toLocalPlaylistItem(playlistId: String, videoId: String): LocalPlaylistItem { fun StreamItem.toLocalPlaylistItem(playlistId: String): LocalPlaylistItem {
return LocalPlaylistItem( return LocalPlaylistItem(
playlistId = playlistId.toInt(), playlistId = playlistId.toInt(),
videoId = videoId, videoId = url!!.toID(),
title = title, title = title,
thumbnailUrl = thumbnailUrl, thumbnailUrl = thumbnail,
uploader = uploader, uploader = uploaderName,
uploaderUrl = uploaderUrl, uploaderUrl = uploaderUrl,
uploaderAvatar = uploaderAvatar, uploaderAvatar = uploaderAvatar,
uploadDate = uploadDate, uploadDate = uploadedDate,
duration = duration duration = duration
) )
} }

View File

@ -80,7 +80,9 @@ class UpdateService : Service() {
} }
private fun getDownloadDirectory(): File { private fun getDownloadDirectory(): File {
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val downloadsDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
)
if (!downloadsDir.canWrite()) return DownloadHelper.getOfflineStorageDir(this) if (!downloadsDir.canWrite()) return DownloadHelper.getOfflineStorageDir(this)
return downloadsDir return downloadsDir
} }

View File

@ -286,7 +286,9 @@ class MainActivity : BaseActivity() {
searchViewModel.setQuery(null) searchViewModel.setQuery(null)
navController.navigate(R.id.searchFragment) navController.navigate(R.id.searchFragment)
} }
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) item.setShowAsAction(
MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
)
return true return true
} }

View File

@ -67,7 +67,10 @@ class CommentsAdapter(
} }
commentInfos.text = comment.author.toString() + TextUtils.SEPARATOR + comment.commentedTime.toString() commentInfos.text = comment.author.toString() + TextUtils.SEPARATOR + comment.commentedTime.toString()
commentText.text = HtmlCompat.fromHtml(comment.commentText.toString(), HtmlCompat.FROM_HTML_MODE_LEGACY) commentText.text = HtmlCompat.fromHtml(
comment.commentText.toString(),
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()
@ -102,7 +105,11 @@ class CommentsAdapter(
} }
} }
private fun showMoreReplies(nextPage: String, showMoreBtn: Button, repliesAdapter: CommentsAdapter) { private fun showMoreReplies(
nextPage: String,
showMoreBtn: Button,
repliesAdapter: CommentsAdapter
) {
when (repliesAdapter.itemCount) { when (repliesAdapter.itemCount) {
0 -> { 0 -> {
fetchReplies(nextPage) { fetchReplies(nextPage) {

View File

@ -61,8 +61,14 @@ class DownloadsAdapter(
) { _, index -> ) { _, index ->
when (index) { when (index) {
0 -> { 0 -> {
val audioDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.AUDIO_DIR) val audioDir = DownloadHelper.getDownloadDir(
val videoDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.VIDEO_DIR) root.context,
DownloadHelper.AUDIO_DIR
)
val videoDir = DownloadHelper.getDownloadDir(
root.context,
DownloadHelper.VIDEO_DIR
)
listOf(audioDir, videoDir).forEach { listOf(audioDir, videoDir).forEach {
val f = File(it, file.name) val f = File(it, file.name)

View File

@ -20,10 +20,10 @@ import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistViewHolder import com.github.libretube.ui.viewholders.PlaylistViewHolder
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper import com.github.libretube.util.NavigationHelper
import java.io.IOException
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 java.io.IOException
class PlaylistAdapter( class PlaylistAdapter(
private val videoFeed: MutableList<StreamItem>, private val videoFeed: MutableList<StreamItem>,

View File

@ -18,7 +18,11 @@ class PlaylistBookmarkAdapter(
private val bookmarkMode: BookmarkMode = BookmarkMode.FRAGMENT private val bookmarkMode: BookmarkMode = BookmarkMode.FRAGMENT
) : RecyclerView.Adapter<PlaylistBookmarkViewHolder>() { ) : RecyclerView.Adapter<PlaylistBookmarkViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistBookmarkViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistBookmarkViewHolder {
val binding = PlaylistBookmarkRowBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = PlaylistBookmarkRowBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return PlaylistBookmarkViewHolder(binding) return PlaylistBookmarkViewHolder(binding)
} }
@ -40,7 +44,11 @@ class PlaylistBookmarkAdapter(
uploaderName.text = bookmark.uploader uploaderName.text = bookmark.uploader
root.setOnClickListener { root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, bookmark.playlistId, PlaylistType.PUBLIC) NavigationHelper.navigatePlaylist(
root.context,
bookmark.playlistId,
PlaylistType.PUBLIC
)
} }
root.setOnLongClickListener { root.setOnLongClickListener {

View File

@ -102,7 +102,10 @@ class SearchAdapter(
val videoName = item.title!! val videoName = item.title!!
root.setOnLongClickListener { root.setOnLongClickListener {
VideoOptionsBottomSheet(videoId, videoName) VideoOptionsBottomSheet(videoId, videoName)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name) .show(
(root.context as BaseActivity).supportFragmentManager,
VideoOptionsBottomSheet::class.java.name
)
true true
} }
channelContainer.setOnClickListener { channelContainer.setOnClickListener {
@ -123,7 +126,10 @@ class SearchAdapter(
searchViews.text = root.context.getString( searchViews.text = root.context.getString(
R.string.subscribers, R.string.subscribers,
item.subscribers.formatShort() item.subscribers.formatShort()
) + TextUtils.SEPARATOR + root.context.getString(R.string.videoCount, item.videos.toString()) ) + TextUtils.SEPARATOR + root.context.getString(
R.string.videoCount,
item.videos.toString()
)
root.setOnClickListener { root.setOnClickListener {
NavigationHelper.navigateChannel(root.context, item.url) NavigationHelper.navigateChannel(root.context, item.url)
} }
@ -155,7 +161,10 @@ class SearchAdapter(
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((root.context as BaseActivity).supportFragmentManager, PlaylistOptionsBottomSheet::class.java.name) .show(
(root.context as BaseActivity).supportFragmentManager,
PlaylistOptionsBottomSheet::class.java.name
)
true true
} }
} }

View File

@ -64,9 +64,15 @@ class VideosAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideosViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideosViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
return when { return when {
viewType == CAUGHT_UP_TYPE -> VideosViewHolder(AllCaughtUpRowBinding.inflate(layoutInflater, parent, false)) viewType == CAUGHT_UP_TYPE -> VideosViewHolder(
forceMode in listOf(ForceMode.TRENDING, ForceMode.RELATED, ForceMode.HOME) -> VideosViewHolder(TrendingRowBinding.inflate(layoutInflater, parent, false)) AllCaughtUpRowBinding.inflate(layoutInflater, parent, false)
forceMode == ForceMode.CHANNEL -> VideosViewHolder(VideoRowBinding.inflate(layoutInflater, parent, false)) )
forceMode in listOf(ForceMode.TRENDING, ForceMode.RELATED, ForceMode.HOME) -> VideosViewHolder(
TrendingRowBinding.inflate(layoutInflater, parent, false)
)
forceMode == ForceMode.CHANNEL -> VideosViewHolder(
VideoRowBinding.inflate(layoutInflater, parent, false)
)
PreferenceHelper.getBoolean( PreferenceHelper.getBoolean(
PreferenceKeys.ALTERNATIVE_VIDEOS_LAYOUT, PreferenceKeys.ALTERNATIVE_VIDEOS_LAYOUT,
false false
@ -102,7 +108,9 @@ class VideosAdapter(
video.uploaderName + TextUtils.SEPARATOR + video.uploaderName + TextUtils.SEPARATOR +
video.views.formatShort() + " " + video.views.formatShort() + " " +
root.context.getString(R.string.views_placeholder) + root.context.getString(R.string.views_placeholder) +
TextUtils.SEPARATOR + video.uploaded?.let { DateUtils.getRelativeTimeSpanString(it) } TextUtils.SEPARATOR + video.uploaded?.let {
DateUtils.getRelativeTimeSpanString(it)
}
video.duration?.let { thumbnailDuration.setFormattedDuration(it) } video.duration?.let { thumbnailDuration.setFormattedDuration(it) }
channelImage.setOnClickListener { channelImage.setOnClickListener {
NavigationHelper.navigateChannel(root.context, video.uploaderUrl) NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
@ -118,7 +126,10 @@ class VideosAdapter(
if (videoId == null || videoName == null) return@setOnLongClickListener true if (videoId == null || videoName == null) return@setOnLongClickListener true
VideoOptionsBottomSheet(videoId, videoName) VideoOptionsBottomSheet(videoId, videoName)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name) .show(
(root.context as BaseActivity).supportFragmentManager,
VideoOptionsBottomSheet::class.java.name
)
true true
} }
@ -134,7 +145,9 @@ class VideosAdapter(
videoInfo.text = videoInfo.text =
video.views.formatShort() + " " + video.views.formatShort() + " " +
root.context.getString(R.string.views_placeholder) + root.context.getString(R.string.views_placeholder) +
TextUtils.SEPARATOR + video.uploaded?.let { DateUtils.getRelativeTimeSpanString(it) } TextUtils.SEPARATOR + video.uploaded?.let {
DateUtils.getRelativeTimeSpanString(it)
}
thumbnailDuration.text = thumbnailDuration.text =
video.duration?.let { DateUtils.formatElapsedTime(it) } video.duration?.let { DateUtils.formatElapsedTime(it) }
@ -159,7 +172,10 @@ class VideosAdapter(
root.setOnLongClickListener { root.setOnLongClickListener {
if (videoId == null || videoName == null) return@setOnLongClickListener true if (videoId == null || videoName == null) return@setOnLongClickListener true
VideoOptionsBottomSheet(videoId, videoName) VideoOptionsBottomSheet(videoId, videoName)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name) .show(
(root.context as BaseActivity).supportFragmentManager,
VideoOptionsBottomSheet::class.java.name
)
true true
} }

View File

@ -59,7 +59,10 @@ class WatchHistoryAdapter(
} }
root.setOnLongClickListener { root.setOnLongClickListener {
VideoOptionsBottomSheet(video.videoId, video.title!!) VideoOptionsBottomSheet(video.videoId, video.title!!)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name) .show(
(root.context as BaseActivity).supportFragmentManager,
VideoOptionsBottomSheet::class.java.name
)
true true
} }

View File

@ -11,8 +11,10 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.databinding.DialogAddtoplaylistBinding import com.github.libretube.databinding.DialogAddtoplaylistBinding
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toStreamItem
import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.ui.models.PlaylistViewModel import com.github.libretube.ui.models.PlaylistViewModel
import com.github.libretube.util.ThemeHelper import com.github.libretube.util.ThemeHelper
@ -86,7 +88,10 @@ class AddToPlaylistDialog(
val appContext = context?.applicationContext ?: return val appContext = context?.applicationContext ?: return
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val success = try { val success = try {
PlaylistsHelper.addToPlaylist(playlistId, videoId) PlaylistsHelper.addToPlaylist(
playlistId,
RetrofitInstance.api.getStreams(videoId).toStreamItem(videoId)
)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
appContext.toastFromMainThread(R.string.unknown_error) appContext.toastFromMainThread(R.string.unknown_error)

View File

@ -38,7 +38,9 @@ class DeletePlaylistDialog(
if (playlistType == PlaylistType.LOCAL) { if (playlistType == PlaylistType.LOCAL) {
awaitQuery { awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId) DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId) DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(
playlistId
)
} }
onSuccess.invoke() onSuccess.invoke()
return return

View File

@ -21,8 +21,8 @@ import com.github.libretube.util.ImageHelper
import com.github.libretube.util.MetadataHelper import com.github.libretube.util.MetadataHelper
import com.github.libretube.util.ThemeHelper import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import retrofit2.HttpException
class DownloadDialog( class DownloadDialog(
private val videoId: String private val videoId: String

View File

@ -20,7 +20,10 @@ class NavBarOptionsDialog : DialogFragment() {
val options = NavBarHelper.getNavBarItems(requireContext()) val options = NavBarHelper.getNavBarItems(requireContext())
val adapter = NavBarOptionsAdapter(options.toMutableList(), NavBarHelper.getStartFragmentId(requireContext())) val adapter = NavBarOptionsAdapter(
options.toMutableList(),
NavBarHelper.getStartFragmentId(requireContext())
)
val itemTouchCallback = object : ItemTouchHelper.Callback() { val itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags( override fun getMovementFlags(

View File

@ -27,11 +27,11 @@ import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.extensions.setupSubscriptionButton import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import java.io.IOException
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 retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException
class ChannelFragment : BaseFragment() { class ChannelFragment : BaseFragment() {
private lateinit var binding: FragmentChannelBinding private lateinit var binding: FragmentChannelBinding
@ -134,7 +134,11 @@ class ChannelFragment : BaseFragment() {
if (isSubscribed == null) return@launchWhenCreated if (isSubscribed == null) return@launchWhenCreated
runOnUiThread { runOnUiThread {
binding.channelSubscribe.setupSubscriptionButton(channelId, channelName, binding.notificationBell) binding.channelSubscribe.setupSubscriptionButton(
channelId,
channelName,
binding.notificationBell
)
binding.channelShare.setOnClickListener { binding.channelShare.setOnClickListener {
val shareDialog = ShareDialog( val shareDialog = ShareDialog(

View File

@ -79,7 +79,11 @@ class HomeFragment : BaseFragment() {
if (feed.isEmpty()) return@runOrError if (feed.isEmpty()) return@runOrError
runOnUiThread { runOnUiThread {
makeVisible(binding.featuredRV, binding.featuredTV) makeVisible(binding.featuredRV, binding.featuredTV)
binding.featuredRV.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) binding.featuredRV.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.featuredRV.adapter = VideosAdapter( binding.featuredRV.adapter = VideosAdapter(
feed.toMutableList(), feed.toMutableList(),
forceMode = VideosAdapter.Companion.ForceMode.HOME forceMode = VideosAdapter.Companion.ForceMode.HOME
@ -108,7 +112,10 @@ class HomeFragment : BaseFragment() {
runOnUiThread { runOnUiThread {
makeVisible(binding.playlistsRV, binding.playlistsTV) makeVisible(binding.playlistsRV, binding.playlistsTV)
binding.playlistsRV.layoutManager = LinearLayoutManager(context) binding.playlistsRV.layoutManager = LinearLayoutManager(context)
binding.playlistsRV.adapter = PlaylistsAdapter(playlists.toMutableList(), PlaylistsHelper.getPrivatePlaylistType()) binding.playlistsRV.adapter = PlaylistsAdapter(
playlists.toMutableList(),
PlaylistsHelper.getPrivatePlaylistType()
)
binding.playlistsRV.adapter?.registerAdapterDataObserver(object : binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() { RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
@ -129,7 +136,11 @@ class HomeFragment : BaseFragment() {
if (bookmarkedPlaylists.isEmpty()) return@runOrError if (bookmarkedPlaylists.isEmpty()) return@runOrError
runOnUiThread { runOnUiThread {
makeVisible(binding.bookmarksTV, binding.bookmarksRV) makeVisible(binding.bookmarksTV, binding.bookmarksRV)
binding.bookmarksRV.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) binding.bookmarksRV.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.bookmarksRV.adapter = PlaylistBookmarkAdapter( binding.bookmarksRV.adapter = PlaylistBookmarkAdapter(
bookmarkedPlaylists, bookmarkedPlaylists,
PlaylistBookmarkAdapter.Companion.BookmarkMode.HOME PlaylistBookmarkAdapter.Companion.BookmarkMode.HOME

View File

@ -105,15 +105,15 @@ import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
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 org.chromium.net.CronetEngine import org.chromium.net.CronetEngine
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
import kotlin.math.abs
class PlayerFragment : BaseFragment(), OnlinePlayerOptions { class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
@ -1160,7 +1160,11 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
if (resolutions.isEmpty()) { if (resolutions.isEmpty()) {
return listOf( return listOf(
VideoResolution(getString(R.string.hls), resolution = Int.MAX_VALUE, adaptiveSourceUrl = streams.hls) VideoResolution(
getString(R.string.hls),
resolution = Int.MAX_VALUE,
adaptiveSourceUrl = streams.hls
)
) )
} else { } else {
resolutions.add(0, VideoResolution(getString(R.string.auto_quality), Int.MAX_VALUE)) resolutions.add(0, VideoResolution(getString(R.string.auto_quality), Int.MAX_VALUE))
@ -1187,7 +1191,10 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
// set the default subtitle if available // set the default subtitle if available
val newParams = trackSelector.buildUponParameters() val newParams = trackSelector.buildUponParameters()
if (PlayerHelper.defaultSubtitleCode != "" && subtitleCodesList.contains(PlayerHelper.defaultSubtitleCode)) { if (PlayerHelper.defaultSubtitleCode != "" && subtitleCodesList.contains(
PlayerHelper.defaultSubtitleCode
)
) {
newParams newParams
.setPreferredTextLanguage(PlayerHelper.defaultSubtitleCode) .setPreferredTextLanguage(PlayerHelper.defaultSubtitleCode)
.setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
@ -1464,7 +1471,10 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
return false return false
} }
val backgroundModeRunning = BackgroundHelper.isServiceRunning(requireContext(), BackgroundMode::class.java) val backgroundModeRunning = BackgroundHelper.isServiceRunning(
requireContext(),
BackgroundMode::class.java
)
return exoPlayer.isPlaying && !backgroundModeRunning return exoPlayer.isPlaying && !backgroundModeRunning
} }

View File

@ -34,8 +34,8 @@ import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.TextUtils import com.github.libretube.util.TextUtils
import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import retrofit2.HttpException
class PlaylistFragment : BaseFragment() { class PlaylistFragment : BaseFragment() {
private lateinit var binding: FragmentPlaylistBinding private lateinit var binding: FragmentPlaylistBinding

View File

@ -18,8 +18,8 @@ import com.github.libretube.extensions.hideKeyboard
import com.github.libretube.ui.adapters.SearchAdapter import com.github.libretube.ui.adapters.SearchAdapter
import com.github.libretube.ui.base.BaseFragment import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import retrofit2.HttpException
class SearchResultFragment : BaseFragment() { class SearchResultFragment : BaseFragment() {
private lateinit var binding: FragmentSearchResultBinding private lateinit var binding: FragmentSearchResultBinding

View File

@ -150,7 +150,9 @@ class SubscriptionsFragment : BaseFragment() {
// add an "all caught up item" // add an "all caught up item"
if (sortOrder == 0) { if (sortOrder == 0) {
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime() val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime()
val caughtUpIndex = feed.indexOfFirst { (it.uploaded ?: 0L) / 1000 < lastCheckedFeedTime } val caughtUpIndex = feed.indexOfFirst {
(it.uploaded ?: 0L) / 1000 < lastCheckedFeedTime
}
if (caughtUpIndex > 0) { if (caughtUpIndex > 0) {
sortedFeed.add(caughtUpIndex, StreamItem(type = "caught")) sortedFeed.add(caughtUpIndex, StreamItem(type = "caught"))
} }

View File

@ -17,8 +17,8 @@ import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.util.LocaleHelper import com.github.libretube.util.LocaleHelper
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import retrofit2.HttpException
class TrendsFragment : BaseFragment() { class TrendsFragment : BaseFragment() {
private lateinit var binding: FragmentTrendsBinding private lateinit var binding: FragmentTrendsBinding

View File

@ -42,7 +42,10 @@ class AppearanceSettings : BasePreferenceFragment() {
} }
val changeIcon = findPreference<Preference>(PreferenceKeys.APP_ICON) val changeIcon = findPreference<Preference>(PreferenceKeys.APP_ICON)
val iconPref = PreferenceHelper.getString(PreferenceKeys.APP_ICON, IconsSheetAdapter.Companion.AppIcon.Default.activityAlias) val iconPref = PreferenceHelper.getString(
PreferenceKeys.APP_ICON,
IconsSheetAdapter.Companion.AppIcon.Default.activityAlias
)
IconsSheetAdapter.availableIcons.firstOrNull { it.activityAlias == iconPref }?.let { IconsSheetAdapter.availableIcons.firstOrNull { it.activityAlias == iconPref }?.let {
changeIcon?.summary = getString(it.nameResource) changeIcon?.summary = getString(it.nameResource)
} }

View File

@ -85,7 +85,11 @@ class MainSettings : BasePreferenceFragment() {
// checking for update: yes -> dialog, no -> snackBar // checking for update: yes -> dialog, no -> snackBar
update?.setOnPreferenceClickListener { update?.setOnPreferenceClickListener {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Toast.makeText(context, "Updater is disabled for debug versions!", Toast.LENGTH_SHORT).show() Toast.makeText(
context,
"Updater is disabled for debug versions!",
Toast.LENGTH_SHORT
).show()
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {

View File

@ -40,9 +40,15 @@ class NotificationSettings : BasePreferenceFragment() {
true true
} }
val notificationTime = findPreference<SwitchPreferenceCompat>(PreferenceKeys.NOTIFICATION_TIME_ENABLED) val notificationTime = findPreference<SwitchPreferenceCompat>(
val notificationStartTime = findPreference<TimePickerPreference>(PreferenceKeys.NOTIFICATION_START_TIME) PreferenceKeys.NOTIFICATION_TIME_ENABLED
val notificationEndTime = findPreference<TimePickerPreference>(PreferenceKeys.NOTIFICATION_END_TIME) )
val notificationStartTime = findPreference<TimePickerPreference>(
PreferenceKeys.NOTIFICATION_START_TIME
)
val notificationEndTime = findPreference<TimePickerPreference>(
PreferenceKeys.NOTIFICATION_END_TIME
)
listOf(notificationStartTime, notificationEndTime).forEach { listOf(notificationStartTime, notificationEndTime).forEach {
it?.isEnabled = notificationTime?.isChecked == true it?.isEnabled = notificationTime?.isChecked == true
} }

View File

@ -28,7 +28,11 @@ class CommentsSheet(
private lateinit var commentsAdapter: CommentsAdapter private lateinit var commentsAdapter: CommentsAdapter
private var isLoading = false private var isLoading = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CommentsSheetBinding.inflate(layoutInflater) binding = CommentsSheetBinding.inflate(layoutInflater)
// set a fixed maximum height // set a fixed maximum height
binding.root.maxHeight = maxHeight binding.root.maxHeight = maxHeight

View File

@ -244,8 +244,12 @@ internal class CustomExoPlayerView(
R.drawable.ic_aspect_ratio, R.drawable.ic_aspect_ratio,
{ {
when (resizeMode) { when (resizeMode) {
AspectRatioFrameLayout.RESIZE_MODE_FIT -> context.getString(R.string.resize_mode_fit) AspectRatioFrameLayout.RESIZE_MODE_FIT -> context.getString(
AspectRatioFrameLayout.RESIZE_MODE_FILL -> context.getString(R.string.resize_mode_fill) R.string.resize_mode_fit
)
AspectRatioFrameLayout.RESIZE_MODE_FILL -> context.getString(
R.string.resize_mode_fill
)
else -> context.getString(R.string.resize_mode_zoom) else -> context.getString(R.string.resize_mode_zoom)
} }
} }
@ -403,7 +407,9 @@ internal class CustomExoPlayerView(
// If brightness progress goes to below 0, set to system brightness // If brightness progress goes to below 0, set to system brightness
if (distance <= 0) { if (distance <= 0) {
brightnessHelper.resetToSystemBrightness() brightnessHelper.resetToSystemBrightness()
gestureViewBinding.brightnessImageView.setImageResource(R.drawable.ic_brightness_auto) gestureViewBinding.brightnessImageView.setImageResource(
R.drawable.ic_brightness_auto
)
gestureViewBinding.brightnessTextView.text = resources.getString(R.string.auto) gestureViewBinding.brightnessTextView.text = resources.getString(R.string.auto)
return return
} }

View File

@ -35,7 +35,10 @@ class SliderPreference(
) )
} }
private val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.SliderPreference) private val typedArray = context.obtainStyledAttributes(
attributeSet,
R.styleable.SliderPreference
)
override fun onAttached() { override fun onAttached() {
super.onAttached() super.onAttached()

View File

@ -52,7 +52,12 @@ class BrightnessHelper(private val activity: Activity) {
* Set current brightness value with scaling to given range. * Set current brightness value with scaling to given range.
* [shouldSave] determines whether the value should be persisted. * [shouldSave] determines whether the value should be persisted.
*/ */
fun setBrightnessWithScale(value: Float, maxValue: Float, minValue: Float = 0.0f, shouldSave: Boolean = false) { fun setBrightnessWithScale(
value: Float,
maxValue: Float,
minValue: Float = 0.0f,
shouldSave: Boolean = false
) {
brightness = value.normalize(minValue, maxValue, minBrightness, maxBrightness) brightness = value.normalize(minValue, maxValue, minBrightness, maxBrightness)
if (shouldSave) savedBrightness = brightness if (shouldSave) savedBrightness = brightness
} }

View File

@ -2,14 +2,14 @@ package com.github.libretube.util
import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Streams
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.StringWriter import java.io.StringWriter
import javax.xml.parsers.DocumentBuilder import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult import javax.xml.transform.stream.StreamResult
import org.w3c.dom.Document
import org.w3c.dom.Element
// Based off of https://github.com/TeamPiped/Piped/blob/master/src/utils/DashUtils.js // Based off of https://github.com/TeamPiped/Piped/blob/master/src/utils/DashUtils.js
@ -66,7 +66,9 @@ object DashHelper {
for (stream in streams.audioStreams!!) { for (stream in streams.audioStreams!!) {
val adapSetInfo = val adapSetInfo =
adapSetInfos.find { it.mimeType == stream.mimeType && it.audioTrackId == stream.audioTrackId } adapSetInfos.find {
it.mimeType == stream.mimeType && it.audioTrackId == stream.audioTrackId
}
if (adapSetInfo != null) { if (adapSetInfo != null) {
adapSetInfo.formats.add(stream) adapSetInfo.formats.add(stream)
continue continue

View File

@ -13,9 +13,9 @@ import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import com.github.libretube.api.CronetHelper import com.github.libretube.api.CronetHelper
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import okio.use
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import okio.use
object ImageHelper { object ImageHelper {
lateinit var imageLoader: ImageLoader lateinit var imageLoader: ImageLoader

View File

@ -15,11 +15,11 @@ 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 java.io.FileOutputStream
class ImportHelper( class ImportHelper(
private val activity: Activity private val activity: Activity
@ -56,7 +56,10 @@ 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(uri.readText(), NewPipeSubscriptions::class.java) val subscriptions = ObjectMapper().readValue(
uri.readText(),
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/", "")
} }
@ -131,7 +134,10 @@ class ImportHelper(
} }
} }
"application/json", "application/*", "application/octet-stream" -> { "application/json", "application/*", "application/octet-stream" -> {
val playlistFile = ObjectMapper().readValue(uri.readText(), ImportPlaylistFile::class.java) val playlistFile = ObjectMapper().readValue(
uri.readText(),
ImportPlaylistFile::class.java
)
importPlaylists.addAll(playlistFile.playlists.orEmpty()) importPlaylists.addAll(playlistFile.playlists.orEmpty())
} }
else -> { else -> {

View File

@ -124,7 +124,13 @@ object PreferenceHelper {
fun toggleIgnorableNotificationChannel(channelId: String) { fun toggleIgnorableNotificationChannel(channelId: String) {
val ignorableChannels = getIgnorableNotificationChannels().toMutableList() val ignorableChannels = getIgnorableNotificationChannels().toMutableList()
if (ignorableChannels.contains(channelId)) ignorableChannels.remove(channelId) else ignorableChannels.add(channelId) if (ignorableChannels.contains(channelId)) {
ignorableChannels.remove(channelId)
} else {
ignorableChannels.add(
channelId
)
}
editor.putString( editor.putString(
PreferenceKeys.IGNORED_NOTIFICATION_CHANNELS, PreferenceKeys.IGNORED_NOTIFICATION_CHANNELS,
ignorableChannels.joinToString(",") ignorableChannels.joinToString(",")

View File

@ -2,13 +2,13 @@ package com.github.libretube.util
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.URI import java.net.URI
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object ProxyHelper { object ProxyHelper {
private fun getImageProxyUrl(): String? { private fun getImageProxyUrl(): String? {

View File

@ -17,8 +17,8 @@ import com.github.libretube.extensions.toID
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.views.TimePickerPreference import com.github.libretube.ui.views.TimePickerPreference
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import kotlinx.coroutines.runBlocking
import java.time.LocalTime import java.time.LocalTime
import kotlinx.coroutines.runBlocking
/** /**
* The notification worker which checks for new streams in a certain frequency * The notification worker which checks for new streams in a certain frequency