mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-04-25 10:43:01 +00:00
- Added **fallback for sync failures** [#407](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/407) - Added **history setting** to remove non-existent series/episodes on refresh [#420](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/420) - Added **movies to history** - Added **queue persistence** - Changed **download item state handling** - Changed **download item removal processing** - Made small changes to **font detection** - Changed **rate limit error handling** - Fixed issue where **files were not always cleaned up** for removed downloads - Fixed **crash when the queue list was modified** - Fixed **changelog parsing** not handling versions like `vX.X.X.X`, which caused changes to be re-added on every restart
358 lines
13 KiB
C#
358 lines
13 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Media.Imaging;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using CRD.Downloader;
|
|
using CRD.Downloader.Crunchyroll;
|
|
using CRD.Utils;
|
|
using CRD.Utils.Files;
|
|
using CRD.Utils.Structs;
|
|
using CRD.Utils.Structs.Crunchyroll;
|
|
|
|
namespace CRD.ViewModels;
|
|
|
|
public partial class DownloadsPageViewModel : ViewModelBase{
|
|
public ObservableCollection<DownloadItemModel> Items{ get; }
|
|
|
|
[ObservableProperty]
|
|
private bool shutdownWhenQueueEmpty;
|
|
|
|
[ObservableProperty]
|
|
private bool autoDownload;
|
|
|
|
[ObservableProperty]
|
|
private bool removeFinished;
|
|
|
|
[ObservableProperty]
|
|
private QueueManager queueManagerIns;
|
|
|
|
public DownloadsPageViewModel(){
|
|
QueueManagerIns = QueueManager.Instance;
|
|
QueueManagerIns.UpdateDownloadListItems();
|
|
Items = QueueManagerIns.DownloadItemModels;
|
|
AutoDownload = CrunchyrollManager.Instance.CrunOptions.AutoDownload;
|
|
RemoveFinished = CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload;
|
|
ShutdownWhenQueueEmpty = CrunchyrollManager.Instance.CrunOptions.ShutdownWhenQueueEmpty;
|
|
}
|
|
|
|
|
|
partial void OnAutoDownloadChanged(bool value){
|
|
CrunchyrollManager.Instance.CrunOptions.AutoDownload = value;
|
|
if (value){
|
|
QueueManagerIns.UpdateDownloadListItems();
|
|
}
|
|
|
|
CfgManager.WriteCrSettings();
|
|
}
|
|
|
|
partial void OnRemoveFinishedChanged(bool value){
|
|
CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload = value;
|
|
CfgManager.WriteCrSettings();
|
|
}
|
|
|
|
partial void OnShutdownWhenQueueEmptyChanged(bool value){
|
|
CrunchyrollManager.Instance.CrunOptions.ShutdownWhenQueueEmpty = value;
|
|
CfgManager.WriteCrSettings();
|
|
}
|
|
|
|
[RelayCommand]
|
|
public void ClearQueue(){
|
|
var items = QueueManagerIns.Queue;
|
|
QueueManagerIns.ClearQueue();
|
|
|
|
foreach (var crunchyEpMeta in items){
|
|
if (!crunchyEpMeta.DownloadProgress.IsDone){
|
|
foreach (var downloadItemDownloadedFile in crunchyEpMeta.downloadedFiles){
|
|
try{
|
|
if (File.Exists(downloadItemDownloadedFile)){
|
|
File.Delete(downloadItemDownloadedFile);
|
|
}
|
|
} catch (Exception){
|
|
// ignored
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
public void RetryQueue(){
|
|
var items = QueueManagerIns.Queue;
|
|
|
|
foreach (var crunchyEpMeta in items){
|
|
if (crunchyEpMeta.DownloadProgress.IsError){
|
|
crunchyEpMeta.DownloadProgress.ResetForRetry();
|
|
}
|
|
}
|
|
|
|
QueueManagerIns.UpdateDownloadListItems();
|
|
}
|
|
|
|
[RelayCommand]
|
|
public void PauseQueue(){
|
|
AutoDownload = false;
|
|
foreach (var item in Items){
|
|
if (item.epMeta.DownloadProgress.State is DownloadState.Downloading or DownloadState.Processing){
|
|
item.ToggleIsDownloading();
|
|
}
|
|
}
|
|
|
|
QueueManagerIns.UpdateDownloadListItems();
|
|
}
|
|
|
|
}
|
|
|
|
public partial class DownloadItemModel : INotifyPropertyChanged{
|
|
public string ImageUrl{ get; set; }
|
|
public Bitmap? ImageBitmap{ get; set; }
|
|
public string Title{ get; set; }
|
|
|
|
public bool isDownloading{ get; set; }
|
|
public bool Done{ get; set; }
|
|
public bool Paused{ get; set; }
|
|
|
|
public double Percent{ get; set; }
|
|
public string Time{ get; set; }
|
|
public string DoingWhat{ get; set; }
|
|
public string DownloadSpeed{ get; set; }
|
|
public string InfoText{ get; set; }
|
|
public string InfoTextHover{ get; set; }
|
|
public CrunchyEpMeta epMeta{ get; set; }
|
|
|
|
|
|
public bool Error{ get; set; }
|
|
public bool ShowPauseIcon{ get; set; }
|
|
|
|
public DownloadItemModel(CrunchyEpMeta epMetaF){
|
|
epMeta = epMetaF;
|
|
|
|
ImageUrl = epMeta.Image ?? string.Empty;
|
|
Title = epMeta.SeriesTitle + (!string.IsNullOrEmpty(epMeta.Season) ? " - S" + epMeta.Season + "E" + (epMeta.EpisodeNumber != string.Empty ? epMeta.EpisodeNumber : epMeta.AbsolutEpisodeNumberE) : "") + " - " +
|
|
epMeta.EpisodeTitle;
|
|
|
|
Done = epMeta.DownloadProgress.IsDone;
|
|
isDownloading = epMeta.DownloadProgress.State is DownloadState.Downloading or DownloadState.Processing;
|
|
ShowPauseIcon = isDownloading;
|
|
Percent = epMeta.DownloadProgress.Percent;
|
|
Time = "Estimated Time: " + TimeSpan.FromSeconds(epMeta.DownloadProgress.Time).ToString(@"hh\:mm\:ss");
|
|
DownloadSpeed = CrunchyrollManager.Instance.CrunOptions.DownloadSpeedInBits
|
|
? $"{epMeta.DownloadProgress.DownloadSpeedBytes * 8 / 1000000.0:F2} Mb/s"
|
|
: $"{epMeta.DownloadProgress.DownloadSpeedBytes / 1000000.0:F2} MB/s";
|
|
|
|
;
|
|
Paused = epMeta.DownloadProgress.IsPaused;
|
|
DoingWhat = Paused ? "Paused" :
|
|
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
|
|
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting";
|
|
|
|
InfoText = JoinWithSeparator(
|
|
GetDubString(),
|
|
GetSubtitleString(),
|
|
epMeta.Resolution
|
|
);
|
|
InfoTextHover = epMeta.AvailableQualities;
|
|
|
|
Error = epMeta.DownloadProgress.IsError;
|
|
}
|
|
|
|
string JoinWithSeparator(params string[] parts){
|
|
return string.Join(" - ", parts.Where(part => !string.IsNullOrEmpty(part)));
|
|
}
|
|
|
|
private string GetDubString(){
|
|
if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){
|
|
return "";
|
|
}
|
|
|
|
return epMeta.SelectedDubs.Aggregate("Dub: ", (current, crunOptionsDlDub) => current + (crunOptionsDlDub + " "));
|
|
}
|
|
|
|
private string GetSubtitleString(){
|
|
var hardSubs = epMeta.Hslang != "none" ? "Hardsub: " : "";
|
|
if (hardSubs != string.Empty){
|
|
var locale = Languages.Locale2language(epMeta.Hslang).CrLocale;
|
|
if (epMeta.AvailableSubs != null && epMeta.AvailableSubs.Contains(locale)){
|
|
hardSubs += locale + " ";
|
|
}
|
|
|
|
return hardSubs;
|
|
}
|
|
|
|
if (epMeta.DownloadSubs.Count < 1){
|
|
return "";
|
|
}
|
|
|
|
var softSubs = "Softsub: ";
|
|
|
|
if (epMeta.DownloadSubs.Contains("all")){
|
|
if (epMeta.AvailableSubs != null){
|
|
return epMeta.AvailableSubs.Aggregate(softSubs, (current, epMetaAvailableSub) => current + (epMetaAvailableSub + " "));
|
|
}
|
|
}
|
|
|
|
foreach (var crunOptionsDlSub in epMeta.DownloadSubs){
|
|
if (epMeta.AvailableSubs != null && epMeta.AvailableSubs.Contains(crunOptionsDlSub)){
|
|
softSubs += crunOptionsDlSub + " ";
|
|
}
|
|
}
|
|
|
|
return softSubs;
|
|
}
|
|
|
|
public void Refresh(){
|
|
Done = epMeta.DownloadProgress.IsDone;
|
|
isDownloading = epMeta.DownloadProgress.State is DownloadState.Downloading or DownloadState.Processing;
|
|
ShowPauseIcon = isDownloading;
|
|
Percent = epMeta.DownloadProgress.Percent;
|
|
Time = "Estimated Time: " + TimeSpan.FromSeconds(epMeta.DownloadProgress.Time).ToString(@"hh\:mm\:ss");
|
|
DownloadSpeed = CrunchyrollManager.Instance.CrunOptions.DownloadSpeedInBits
|
|
? $"{epMeta.DownloadProgress.DownloadSpeedBytes * 8 / 1000000.0:F2} Mb/s"
|
|
: $"{epMeta.DownloadProgress.DownloadSpeedBytes / 1000000.0:F2} MB/s";
|
|
|
|
Paused = epMeta.DownloadProgress.IsPaused;
|
|
DoingWhat = Paused ? "Paused" :
|
|
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
|
|
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting";
|
|
|
|
InfoText = JoinWithSeparator(
|
|
GetDubString(),
|
|
GetSubtitleString(),
|
|
epMeta.Resolution
|
|
);
|
|
InfoTextHover = epMeta.AvailableQualities;
|
|
Error = epMeta.DownloadProgress.IsError;
|
|
|
|
|
|
if (PropertyChanged != null){
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowPauseIcon)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Percent)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Time)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadSpeed)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DoingWhat)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Error)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoText)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoTextHover)));
|
|
}
|
|
}
|
|
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
[RelayCommand]
|
|
public void ToggleIsDownloading(){
|
|
if (epMeta.DownloadProgress.State is DownloadState.Downloading or DownloadState.Processing){
|
|
epMeta.DownloadProgress.ResumeState = epMeta.DownloadProgress.State;
|
|
epMeta.DownloadProgress.State = DownloadState.Paused;
|
|
isDownloading = false;
|
|
Paused = true;
|
|
ShowPauseIcon = false;
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowPauseIcon)));
|
|
|
|
QueueManager.Instance.ReleaseDownloadSlot(epMeta);
|
|
QueueManager.Instance.RefreshQueue();
|
|
return;
|
|
}
|
|
|
|
if (epMeta.DownloadProgress.IsPaused){
|
|
if (!QueueManager.Instance.TryResumeDownload(epMeta))
|
|
return;
|
|
|
|
epMeta.DownloadProgress.State = epMeta.DownloadProgress.ResumeState;
|
|
isDownloading = epMeta.DownloadProgress.State is DownloadState.Downloading or DownloadState.Processing;
|
|
Paused = false;
|
|
ShowPauseIcon = isDownloading;
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowPauseIcon)));
|
|
return;
|
|
}
|
|
|
|
StartDownload();
|
|
}
|
|
|
|
[RelayCommand]
|
|
public void RetryDownload(){
|
|
epMeta.DownloadProgress.ResetForRetry();
|
|
isDownloading = false;
|
|
Paused = false;
|
|
ShowPauseIcon = false;
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowPauseIcon)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Error)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Percent)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Time)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadSpeed)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DoingWhat)));
|
|
|
|
QueueManager.Instance.RefreshQueue();
|
|
StartDownload();
|
|
}
|
|
|
|
public Task StartDownload(){
|
|
QueueManager.Instance.TryStartDownload(this);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
internal async Task StartDownloadCore(){
|
|
if (isDownloading)
|
|
return;
|
|
|
|
epMeta.RenewCancellationToken();
|
|
isDownloading = true;
|
|
epMeta.DownloadProgress.State = DownloadState.Downloading;
|
|
Paused = false;
|
|
ShowPauseIcon = true;
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowPauseIcon)));
|
|
|
|
CrDownloadOptions? newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
|
|
|
if (epMeta.OnlySubs){
|
|
newOptions?.Novids = true;
|
|
newOptions?.Noaudio = true;
|
|
}
|
|
|
|
await CrunchyrollManager.Instance.DownloadEpisode(
|
|
epMeta,
|
|
epMeta.DownloadSettings ?? newOptions ?? CrunchyrollManager.Instance.CrunOptions);
|
|
}
|
|
|
|
[RelayCommand]
|
|
public void RemoveFromQueue(){
|
|
CrunchyEpMeta? downloadItem = QueueManager.Instance.Queue.FirstOrDefault(e => e.Equals(epMeta)) ?? null;
|
|
if (downloadItem != null){
|
|
QueueManager.Instance.RemoveFromQueue(downloadItem);
|
|
epMeta.CancelDownload();
|
|
if (!Done){
|
|
foreach (var downloadItemDownloadedFile in downloadItem.downloadedFiles){
|
|
try{
|
|
if (File.Exists(downloadItemDownloadedFile)){
|
|
File.Delete(downloadItemDownloadedFile);
|
|
}
|
|
} catch (Exception){
|
|
// ignored
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task LoadImage(){
|
|
ImageBitmap = await Helpers.LoadImage(ImageUrl, 208, 117);
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
|
|
}
|
|
}
|