Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.libretube.enums.FileType
import java.nio.file.Path
import kotlin.io.path.fileSize

@Entity(
tableName = "downloadItem",
Expand All @@ -31,4 +32,6 @@ data class DownloadItem(
var quality: String? = null,
var language: String? = null,
var downloadSize: Long = -1L
)
) {
val isFinished get() = runCatching { path.fileSize() }.getOrDefault(0L) < downloadSize
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,4 @@ object DownloadHelper {
DatabaseHolder.Database.downloadDao().deleteDownload(download)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.core.app.NotificationCompat.Builder
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.getSystemService
import androidx.core.util.contains
import androidx.core.util.keyIterator
import androidx.core.util.set
import androidx.core.util.valueIterator
Expand Down Expand Up @@ -140,6 +141,7 @@ class DownloadService : LifecycleService() {
ACTION_DOWNLOAD_RESUME -> resume(downloadId!!)
ACTION_DOWNLOAD_PAUSE -> pause(downloadId!!)
ACTION_DOWNLOAD_STOP -> stop(downloadId!!)
ACTION_RESUME_ALL -> resumeAll()
}

registerNetworkChangedCallback()
Expand Down Expand Up @@ -499,6 +501,40 @@ class DownloadService : LifecycleService() {
stopServiceIfDone()
}

/**
* Resume all downloads: Queue them all, then fill empty slots.
*/
private fun resumeAll() {
lifecycleScope.launch(coroutineContext) {
val incompleteItems = withContext(Dispatchers.IO) {
Database.downloadDao().getAll()
.flatMap { it.downloadItems }
.filter { !it.isFinished }
}

incompleteItems.forEach {
if (!downloadQueue.contains(it.id)) {
downloadQueue.put(it.id, false)
}
}

val max = DownloadHelper.getMaxConcurrentDownloads()
val current = downloadQueue.valueIterator().asSequence().count { it }
val slotsToFill = max - current

if (slotsToFill > 0) {
val candidates = incompleteItems.filter { !downloadQueue[it.id] }
.take(slotsToFill)

candidates.forEach { item ->
launch {
downloadFile(item)
}
}
}
}
}

/**
* Stop downloading job for given [id]. If no downloads are active, stop the service.
*/
Expand Down Expand Up @@ -696,6 +732,8 @@ class DownloadService : LifecycleService() {
"com.github.libretube.services.DownloadService.ACTION_SERVICE_STARTED"
const val ACTION_SERVICE_STOPPED =
"com.github.libretube.services.DownloadService.ACTION_SERVICE_STOPPED"
const val ACTION_RESUME_ALL =
"com.github.libretube.services.DownloadService.ACTION_RESUME_ALL"

// any values that are not in that range are strictly rate limited by YT or are very slow due
// to the amount of requests that's being made
Expand All @@ -704,4 +742,4 @@ class DownloadService : LifecycleService() {

var IS_DOWNLOAD_RUNNING = false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
}

binding.playlistName.text = playlist.downloadPlaylist.title
binding.playlistName.isVisible = true

playlist.downloadVideos.map { it.videoId }
}
Expand Down Expand Up @@ -285,6 +286,18 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
toggleVisibilities()
}

binding.resumeAll.setOnClickListener {
val intent = Intent(requireContext(), DownloadService::class.java)
intent.action = DownloadService.ACTION_RESUME_ALL
requireContext().startService(intent)
}

binding.resumeAll.setOnClickListener {
val intent = Intent(requireContext(), DownloadService::class.java)
intent.action = DownloadService.ACTION_RESUME_ALL
requireContext().startService(intent)
}

binding.deleteAll.setOnClickListener {
showDeleteAllDialog(binding.root.context, adapter)
}
Expand Down Expand Up @@ -328,9 +341,13 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
val binding = _binding ?: return

val isEmpty = adapter.itemCount == 0
val hasIncomplete = adapter.currentList.any { download ->
download.downloadItems.any { !it.isFinished }
}
binding.downloadsEmpty.isVisible = isEmpty
binding.downloadsContainer.isGone = isEmpty
binding.deleteAll.isGone = isEmpty
binding.resumeAll.isVisible = hasIncomplete
binding.shuffleAll.isGone = isEmpty
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,8 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
playlistName = response.name
isLoading = false

if (!response.thumbnailUrl.isNullOrEmpty()) {
ImageHelper.loadImage(response.thumbnailUrl, binding.thumbnail)
} else {
binding.thumbnail.setImageResource(R.drawable.ic_empty_playlist)
binding.thumbnail.setPadding(64f.dpToPx())
binding.thumbnail.setBackgroundColor(com.google.android.material.R.attr.colorSurface)
}
setPlaylistThumbnail(response.thumbnailUrl)

binding.playlistProgress.isGone = true
binding.playlistAppBar.isVisible = true
binding.playlistRecView.isVisible = true
Expand Down Expand Up @@ -186,7 +181,8 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
)
}

binding.playlistInfo.text = getChannelAndVideoString(response, playlistFeed.size)
binding.playlistInfo.text =
getChannelAndVideoString(response, playlistFeed.size)
}
})

Expand Down Expand Up @@ -388,9 +384,10 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist

playlistAdapter?.submitList(videos)

updatePlaylistDuration()
updatePlaylistDuration(videos)
}

@SuppressLint("StringFormatInvalid")
private fun removeFromPlaylist(sortedFeedPosition: Int) {
val playlistAdapter = playlistAdapter ?: return

Expand Down Expand Up @@ -423,6 +420,7 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
)
}
.show()
updateInfo(updatedList)
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Expand All @@ -431,6 +429,16 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
}
}

private fun updateInfo(updatedList: List<PlaylistItem>) {
val playlistCount = updatedList.size
binding.playlistInfo.text = getChannelAndVideoString(
Playlist(name = playlistName, videos = playlistCount),
playlistCount
)
updatePlaylistDuration(updatedList)
setPlaylistThumbnail(updatedList.firstOrNull()?.item?.thumbnail)
}

private fun reAddToPlaylist(
streamItem: StreamItem,
sortedFeedPosition: Int,
Expand All @@ -447,6 +455,7 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist

withContext(Dispatchers.Main) {
playlistAdapter.submitList(fixedList)
updateInfo(fixedList)
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Expand All @@ -464,7 +473,11 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
*
* I.e., this method adds the given offset to all videos with an originalPlaylistIndex > modifiedPosition.
*/
private fun fixItemIndices(items: List<PlaylistItem>, modifiedPosition: Int, offset: Int): List<PlaylistItem> {
private fun fixItemIndices(
items: List<PlaylistItem>,
modifiedPosition: Int,
offset: Int
): List<PlaylistItem> {
return items.map {
if (it.originalPlaylistIndex > modifiedPosition) {
it.copy(originalPlaylistIndex = it.originalPlaylistIndex + offset)
Expand Down Expand Up @@ -504,18 +517,30 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
PlaylistItem(item, currentList.size + index)
}
playlistAdapter?.submitList(newList)
updatePlaylistDuration()
updatePlaylistDuration(newList)
isLoading = false
}
}

@SuppressLint("SetTextI18n")
private fun updatePlaylistDuration() {
val totalDuration = playlistFeed.sumOf { it.duration ?: 0 } ?: return
private fun updatePlaylistDuration(updatedList: List<PlaylistItem>) {
val totalDuration = updatedList.sumOf { it.item.duration ?: 0 }
binding.playlistDuration.text = DateUtils.formatElapsedTime(totalDuration) +
if (nextPage != null) "+" else ""
}

// Update the Cover/Thumbnail of the playlist if the first video was removed
private fun setPlaylistThumbnail(thumbnailUrl: String?) {
if (!thumbnailUrl.isNullOrEmpty()) {
ImageHelper.loadImage(thumbnailUrl, binding.thumbnail)
} else {
binding.thumbnail.setImageResource(R.drawable.ic_empty_playlist)
binding.thumbnail.setPadding(64f.dpToPx())
binding.thumbnail.setBackgroundColor(com.google.android.material.R.attr.colorSurface)
}
}


override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
Expand Down
57 changes: 36 additions & 21 deletions app/src/main/res/layout/fragment_download_content.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,64 @@
android:layout_height="match_parent"
android:orientation="vertical">

<LinearLayout
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:paddingHorizontal="8dp">

<TextView
android:id="@+id/playlist_name"
android:layout_width="0dp"
android:layout_weight="1"
<com.google.android.material.button.MaterialButton
android:id="@+id/resume_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textSize="18sp"
android:textStyle="bold"
android:ellipsize="end"
android:paddingHorizontal="8dp"
tools:text="Downloaded Playlist Name"/>
android:text="@string/resume_all"
android:contentDescription="@string/resume_all"
app:icon="@drawable/ic_play"
android:tooltipText="@string/resume_all"
android:visibility="gone"
tools:targetApi="o"
tools:visibility="visible" />

<com.google.android.material.button.MaterialButton
android:id="@+id/sort_type"
style="?materialButtonTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="10dp"
android:layout_alignParentEnd="true"
android:text="@string/sort_by"
app:icon="@drawable/ic_sort"
android:gravity="end"
app:iconGravity="textEnd" />

</LinearLayout>
</RelativeLayout>

<TextView
android:id="@+id/playlist_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:textSize="18sp"
android:textStyle="bold"
android:ellipsize="end"
android:paddingHorizontal="8dp"
android:visibility="gone"
tools:visibility="visible"
tools:text="Downloaded Playlist Name"/>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloads_recView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"/>
android:orientation="vertical" />

<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</LinearLayout>

Expand Down Expand Up @@ -110,9 +130,4 @@
tools:targetApi="o"
tools:visibility="visible" />

<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@
<string name="showing_results_for">Showing results for:</string>
<string name="long_press_fast_forward">Fast forward on long press</string>
<string name="long_press_fast_forward_summary">Play video with double speed while tapping and holding the screen.</string>
<string name="resume_all">Resume Downloads</string>

<!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string>
Expand Down
Loading