mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 20:10:26 +00:00
Add - Added --historyRefreshActive parameter
Add - Added **hardware acceleration options** for sync timings Add - Added an **icon for episodes removed from Crunchyroll** or moved to a different season Add - Added **highlighting for selected dubs/subs** in history Add - Added **highlighting of episode titles** when all selected dubs/subs are available Add - Added option to **set history series to inactive** Add - Added **filter for Active and Inactive** series in history Add - Added **download retry options** to settings, making retry variables editable Chg - Changed **Sonarr missing filter** to respect the "Skip Unmonitored" setting Chg - Changed to **show an error** if an episode wasn't added to the queue Chg - Changed to show **dates for episodes** in the history Chg - Changed **sync timings algorithm** to improve overall performance Chg - Changed **sync timings algorithm** to more accurately synchronize dubs Chg - Changed **series/season refresh messages** to indicate failure or success more clearly Chg - Changed **error display frequency** to reduce repeated popups for the same message Fix - Fixed **movie detection** to properly verify if the ID corresponds to a movie Fix - Fixed **long option hiding remove buttons** for FFmpeg and MKVMerge additional options Fix - Fixed **toast message timer** not updating correctly Fix - Fixed **path length error** Fix - Fixed a **rare crash when navigating history**
This commit is contained in:
parent
709647bb99
commit
4f6d0f2257
35 changed files with 1178 additions and 237 deletions
|
|
@ -24,6 +24,8 @@ public partial class App : Application{
|
|||
desktop.MainWindow = new MainWindow{
|
||||
DataContext = new MainWindowViewModel(manager),
|
||||
};
|
||||
|
||||
desktop.MainWindow.Opened += (_, _) => { manager.SetBackgroundImage(); };
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -114,16 +114,16 @@ public class CrAuth{
|
|||
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||
} else{
|
||||
if (response.ResponseContent.Contains("invalid_credentials")){
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 10));
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 5));
|
||||
} else if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 10));
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5));
|
||||
} else{
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {response.ResponseContent.Substring(0, response.ResponseContent.Length < 200 ? response.ResponseContent.Length : 200)}",
|
||||
ToastType.Error, 10));
|
||||
ToastType.Error, 5));
|
||||
await Console.Error.WriteLineAsync("Full Response: " + response.ResponseContent);
|
||||
}
|
||||
}
|
||||
|
|
@ -231,6 +231,15 @@ public class CrAuth{
|
|||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5));
|
||||
Console.Error.WriteLine($"Failed to login - Cloudflare error try to change to BetaAPI in settings");
|
||||
}
|
||||
|
||||
if (response.IsOk){
|
||||
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||
|
||||
|
|
@ -242,6 +251,9 @@ public class CrAuth{
|
|||
} else{
|
||||
Console.Error.WriteLine("Token Auth Failed");
|
||||
await AuthAnonymous();
|
||||
|
||||
MainWindow.Instance.ShowError("Login failed. Please check the log for more details.");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,23 +40,27 @@ public class CrMovies{
|
|||
return null;
|
||||
}
|
||||
|
||||
if (movie.Total == 1 && movie.Data != null){
|
||||
return movie.Data.First();
|
||||
if (movie is{ Total: 1, Data: not null }){
|
||||
var movieRes = movie.Data.First();
|
||||
return movieRes.type != "movie" ? null : movieRes;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("Multiple movie returned with one ID?");
|
||||
if (movie.Data != null) return movie.Data.First();
|
||||
if (movie.Data != null){
|
||||
var movieRes = movie.Data.First();
|
||||
return movieRes.type != "movie" ? null : movieRes;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public CrunchyEpMeta? EpisodeMeta(CrunchyMovie episodeP, List<string> dubLang){
|
||||
|
||||
if (!string.IsNullOrEmpty(episodeP.AudioLocale) && !dubLang.Contains(episodeP.AudioLocale)){
|
||||
Console.Error.WriteLine("Movie not available in the selected dub lang");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var images = (episodeP.Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
|
||||
|
||||
var epMeta = new CrunchyEpMeta();
|
||||
|
|
@ -78,7 +82,7 @@ public class CrMovies{
|
|||
Time = 0,
|
||||
DownloadSpeed = 0
|
||||
};
|
||||
epMeta.AvailableSubs = new List<string>();
|
||||
epMeta.AvailableSubs = [];
|
||||
epMeta.Description = episodeP.Description;
|
||||
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ public class CrMusic{
|
|||
}
|
||||
|
||||
|
||||
public async Task<CrunchyMusicVideoList?> ParseArtistVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
|
||||
public async Task<CrunchyMusicVideoList?> ParseArtistVideosByIdAsync(string? artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
|
||||
if (string.IsNullOrEmpty(artistId)){
|
||||
return new CrunchyMusicVideoList();
|
||||
}
|
||||
|
||||
var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang);
|
||||
var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/concerts", crLocale, forcedLang);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -114,7 +115,8 @@ public class CrunchyrollManager{
|
|||
options.QualityVideo = "best";
|
||||
options.CcTag = "CC";
|
||||
options.CcSubsFont = "Trebuchet MS";
|
||||
options.FsRetryTime = 5;
|
||||
options.RetryDelay = 5;
|
||||
options.RetryAttempts = 5;
|
||||
options.Numbers = 2;
|
||||
options.Timeout = 15000;
|
||||
options.DubLang = new List<string>(){ "ja-JP" };
|
||||
|
|
@ -409,7 +411,7 @@ public class CrunchyrollManager{
|
|||
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
||||
SignsSubsAsForced = options.SignsSubsAsForced,
|
||||
},
|
||||
fileNameAndPath);
|
||||
fileNameAndPath, data);
|
||||
|
||||
if (result is{ merger: not null, isMuxed: true }){
|
||||
mergers.Add(result.merger);
|
||||
|
|
@ -474,7 +476,7 @@ public class CrunchyrollManager{
|
|||
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
||||
SignsSubsAsForced = options.SignsSubsAsForced,
|
||||
},
|
||||
fileNameAndPath);
|
||||
fileNameAndPath, data);
|
||||
|
||||
syncError = result.syncError;
|
||||
muxError = !result.isMuxed;
|
||||
|
|
@ -646,7 +648,7 @@ public class CrunchyrollManager{
|
|||
|
||||
#endregion
|
||||
|
||||
private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename){
|
||||
private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename, CrunchyEpMeta crunchyEpMeta){
|
||||
var muxToMp3 = false;
|
||||
|
||||
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
|
||||
|
|
@ -733,6 +735,16 @@ public class CrunchyrollManager{
|
|||
bool isMuxed, syncError = false;
|
||||
|
||||
if (options is{ SyncTiming: true, DlVideoOnce: true }){
|
||||
crunchyEpMeta.DownloadProgress = new DownloadProgress(){
|
||||
IsDownloading = true,
|
||||
Percent = 100,
|
||||
Time = 0,
|
||||
DownloadSpeed = 0,
|
||||
Doing = "Muxing – Syncing Dub Timings"
|
||||
};
|
||||
|
||||
QueueManager.Instance.Queue.Refresh();
|
||||
|
||||
var basePath = merger.options.OnlyVid.First().Path;
|
||||
var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList();
|
||||
|
||||
|
|
@ -764,6 +776,16 @@ public class CrunchyrollManager{
|
|||
syncVideosList.ForEach(syncVideo => {
|
||||
if (syncVideo.Path != null) Helpers.DeleteFile(syncVideo.Path);
|
||||
});
|
||||
|
||||
crunchyEpMeta.DownloadProgress = new DownloadProgress(){
|
||||
IsDownloading = true,
|
||||
Percent = 100,
|
||||
Time = 0,
|
||||
DownloadSpeed = 0,
|
||||
Doing = "Muxing"
|
||||
};
|
||||
|
||||
QueueManager.Instance.Queue.Refresh();
|
||||
}
|
||||
|
||||
if (!options.Mp4 && !muxToMp3){
|
||||
|
|
@ -777,12 +799,12 @@ public class CrunchyrollManager{
|
|||
|
||||
private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){
|
||||
if (Profile.Username == "???"){
|
||||
MainWindow.Instance.ShowError("User Account not recognized - are you signed in?");
|
||||
MainWindow.Instance.ShowError($"User Account not recognized - are you signed in?");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
FileName = "./unknown",
|
||||
ErrorText = "Login problem"
|
||||
ErrorText = "User Account not recognized - are you signed in?"
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1347,7 +1369,7 @@ public class CrunchyrollManager{
|
|||
|
||||
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
|
||||
|
||||
string onlyFileName = Path.GetFileNameWithoutExtension(fileName);
|
||||
string onlyFileName = Path.GetFileName(fileName);
|
||||
int maxLength = 220;
|
||||
|
||||
if (onlyFileName.Length > maxLength){
|
||||
|
|
@ -1361,7 +1383,7 @@ public class CrunchyrollManager{
|
|||
if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){
|
||||
titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength);
|
||||
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
|
||||
onlyFileName = Path.GetFileNameWithoutExtension(fileName);
|
||||
onlyFileName = Path.GetFileName(fileName);
|
||||
|
||||
if (onlyFileName.Length > maxLength){
|
||||
fileName = Helpers.LimitFileNameLength(fileName, maxLength);
|
||||
|
|
@ -1372,7 +1394,7 @@ public class CrunchyrollManager{
|
|||
fileName = Helpers.LimitFileNameLength(fileName, maxLength);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"Filename changed to {Path.GetFileNameWithoutExtension(fileName)}");
|
||||
Console.Error.WriteLine($"Filename changed to {Path.GetFileName(fileName)}");
|
||||
}
|
||||
|
||||
//string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
|
||||
|
|
@ -1704,7 +1726,7 @@ public class CrunchyrollManager{
|
|||
try{
|
||||
// Parsing and constructing the file names
|
||||
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
|
||||
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray());
|
||||
var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray());
|
||||
if (Path.IsPathRooted(outFile)){
|
||||
tsFile = outFile;
|
||||
} else{
|
||||
|
|
@ -1712,14 +1734,14 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
// Check if the path is absolute
|
||||
bool isAbsolute = Path.IsPathRooted(outFile);
|
||||
var isAbsolute = Path.IsPathRooted(outFile);
|
||||
|
||||
// Get all directory parts of the path except the last segment (assuming it's a file)
|
||||
string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty<string>();
|
||||
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ??[];
|
||||
|
||||
// Initialize the cumulative path based on whether the original path is absolute or not
|
||||
string cumulativePath = isAbsolute ? "" : fileDir;
|
||||
for (int i = 0; i < directories.Length; i++){
|
||||
var cumulativePath = isAbsolute ? "" : fileDir;
|
||||
for (var i = 0; i < directories.Length; i++){
|
||||
// Build the path incrementally
|
||||
cumulativePath = Path.Combine(cumulativePath, directories[i]);
|
||||
|
||||
|
|
@ -2028,7 +2050,8 @@ public class CrunchyrollManager{
|
|||
M3U8Json = videoJson,
|
||||
// BaseUrl = chunkPlaylist.BaseUrl,
|
||||
Threads = options.Partsize,
|
||||
FsRetryTime = options.FsRetryTime * 1000,
|
||||
FsRetryTime = options.RetryDelay * 1000,
|
||||
Retries = options.RetryAttempts,
|
||||
Override = options.Force,
|
||||
}, data, true, false);
|
||||
|
||||
|
|
@ -2085,7 +2108,8 @@ public class CrunchyrollManager{
|
|||
M3U8Json = audioJson,
|
||||
// BaseUrl = chunkPlaylist.BaseUrl,
|
||||
Threads = options.Partsize,
|
||||
FsRetryTime = options.FsRetryTime * 1000,
|
||||
FsRetryTime = options.RetryDelay * 1000,
|
||||
Retries = options.RetryAttempts,
|
||||
Override = options.Force,
|
||||
}, data, false, true);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -20,6 +21,7 @@ using CRD.Utils.Structs.History;
|
|||
using CRD.ViewModels;
|
||||
using CRD.ViewModels.Utils;
|
||||
using CRD.Views.Utils;
|
||||
using DynamicData;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
|
@ -58,7 +60,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _muxToMp4;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _muxFonts;
|
||||
|
||||
|
|
@ -209,6 +211,11 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
new(){ Content = "tv/samsung" }
|
||||
];
|
||||
|
||||
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
|
||||
|
||||
[ObservableProperty]
|
||||
private StringItemWithDisplayName _selectedFFmpegHWAccel;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isEncodeEnabled;
|
||||
|
||||
|
|
@ -275,6 +282,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
|
||||
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
||||
|
||||
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||
|
||||
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
||||
SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0];
|
||||
|
||||
|
||||
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
|
||||
|
||||
SelectedSubLang.Clear();
|
||||
|
|
@ -391,6 +404,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.FfmpegHwAccelFlag = SelectedFFmpegHWAccel.value;
|
||||
|
||||
List<string> softSubs = new List<string>();
|
||||
foreach (var listBoxItem in SelectedSubLang){
|
||||
softSubs.Add(listBoxItem.Content + "");
|
||||
|
|
@ -568,4 +583,61 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
|
||||
}
|
||||
}
|
||||
|
||||
private List<StringItemWithDisplayName> GetAvailableHWAccelOptions(){
|
||||
try{
|
||||
using (var process = new Process()){
|
||||
process.StartInfo.FileName = CfgManager.PathFFMPEG;
|
||||
process.StartInfo.Arguments = "-hwaccels";
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
|
||||
string output = string.Empty;
|
||||
|
||||
process.OutputDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data)){
|
||||
output += e.Data + Environment.NewLine;
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
// process.BeginErrorReadLine();
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
var lines = output.Split(new[]{ '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var accels = lines.Skip(1).Select(l => l.Trim().ToLower()).ToList();
|
||||
return MapHWAccelOptions(accels);
|
||||
}
|
||||
} catch (Exception e){
|
||||
Console.WriteLine("Failed to get Available HW Accel Options" + e);
|
||||
}
|
||||
|
||||
|
||||
return[];
|
||||
}
|
||||
|
||||
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){
|
||||
var options = new List<StringItemWithDisplayName>{
|
||||
new(){ DisplayName = "CPU Only", value = "" },
|
||||
new(){ DisplayName = "Auto", value = "-hwaccel auto " }
|
||||
};
|
||||
|
||||
if (accels.Contains("cuda")) options.Add(new StringItemWithDisplayName{ DisplayName = "NVIDIA (CUDA)", value = "-hwaccel cuda " });
|
||||
if (accels.Contains("qsv")) options.Add(new StringItemWithDisplayName{ DisplayName = "Intel Quick Sync (QSV)", value = "-hwaccel qsv " });
|
||||
if (accels.Contains("dxva2")) options.Add(new StringItemWithDisplayName{ DisplayName = "AMD/Intel DXVA2", value = "-hwaccel dxva2" });
|
||||
if (accels.Contains("d3d11va")) options.Add(new StringItemWithDisplayName{ DisplayName = "AMD/Intel D3D11VA", value = "-hwaccel d3d11va " });
|
||||
if (accels.Contains("d3d12va")) options.Add(new StringItemWithDisplayName{ DisplayName = "AMD/Intel D3D12VA", value = "-hwaccel d3d12va " });
|
||||
if (accels.Contains("vaapi")) options.Add(new StringItemWithDisplayName{ DisplayName = "VAAPI (Linux)", value = "-hwaccel vaapi " });
|
||||
if (accels.Contains("videotoolbox")) options.Add(new StringItemWithDisplayName{ DisplayName = "Apple VideoToolbox", value = "-hwaccel videotoolbox " });
|
||||
|
||||
// if (accels.Contains("opencl")) options.Add(new(){DisplayName = "OpenCL (Advanced)", value ="-hwaccel opencl "});
|
||||
// if (accels.Contains("vulkan")) options.Add(new(){DisplayName = "Vulkan (Experimental)", value ="-hwaccel vulkan "});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -286,7 +286,7 @@
|
|||
<CheckBox IsChecked="{Binding DownloadChapters}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Mark as watched" Description="Mark the downloaded episodes as watched on Crunchyroll">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding MarkAsWatched}"> </CheckBox>
|
||||
|
|
@ -377,7 +377,7 @@
|
|||
<CheckBox IsChecked="{Binding DefaultSubSigns}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include Fonts" Description="Includes the fonts in the mkv">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding MuxFonts}"> </CheckBox>
|
||||
|
|
@ -409,7 +409,23 @@
|
|||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Sync Timings" Description="Does not work for all episodes but for the ones that only have a different intro">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding SyncTimings}"> </CheckBox>
|
||||
<StackPanel HorizontalAlignment="Right">
|
||||
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding SyncTimings}"> </CheckBox>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding SyncTimings}">
|
||||
<TextBlock VerticalAlignment="Center" Text="Video Processing Method" Margin=" 0 0 5 0" ></TextBlock>
|
||||
<ComboBox HorizontalAlignment="Right"
|
||||
ItemsSource="{Binding FFmpegHWAccel}"
|
||||
SelectedItem="{Binding SelectedFFmpegHWAccel}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayName}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
|
@ -438,7 +454,11 @@
|
|||
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
|
||||
CornerRadius="10" Margin="2">
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
|
||||
<TextBlock Text="{Binding stringValue}" Margin="5,0" TextTrimming="CharacterEllipsis" MaxWidth="300" TextWrapping="NoWrap">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="{Binding stringValue}" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
</TextBlock>
|
||||
<Button Content="X" FontSize="10" VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
|
||||
Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveMkvMergeParam}"
|
||||
|
|
@ -476,7 +496,11 @@
|
|||
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
|
||||
CornerRadius="10" Margin="2">
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
|
||||
<TextBlock Text="{Binding stringValue}" Margin="5,0" TextTrimming="CharacterEllipsis" MaxWidth="300" TextWrapping="NoWrap">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="{Binding stringValue}" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
</TextBlock>
|
||||
<Button Content="X" FontSize="10" VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
|
||||
Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveFfmpegParam}"
|
||||
|
|
|
|||
|
|
@ -20,9 +20,30 @@ namespace CRD.Downloader;
|
|||
public class History{
|
||||
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
|
||||
|
||||
public async Task<bool> CrUpdateSeries(string seriesId, string? seasonId){
|
||||
public async Task<bool> CrUpdateSeries(string? seriesId, string? seasonId){
|
||||
if (string.IsNullOrEmpty(seriesId)){
|
||||
return false;
|
||||
}
|
||||
|
||||
await crunInstance.CrAuth.RefreshToken(true);
|
||||
|
||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||
|
||||
if (historySeries != null){
|
||||
if (string.IsNullOrEmpty(seasonId)){
|
||||
foreach (var historySeriesSeason in historySeries.Seasons){
|
||||
foreach (var historyEpisode in historySeriesSeason.EpisodesList){
|
||||
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
|
||||
}
|
||||
}
|
||||
} else{
|
||||
foreach (var historyEpisode in historySeries.Seasons.First(historySeason => historySeason.SeasonId == seasonId).EpisodesList){
|
||||
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true);
|
||||
|
||||
if (parsedSeries == null){
|
||||
|
|
@ -31,6 +52,7 @@ public class History{
|
|||
}
|
||||
|
||||
if (parsedSeries.Data != null){
|
||||
var result = false;
|
||||
foreach (var s in parsedSeries.Data){
|
||||
var sId = s.Id;
|
||||
if (s.Versions is{ Count: > 0 }){
|
||||
|
|
@ -48,17 +70,19 @@ public class History{
|
|||
|
||||
var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
|
||||
|
||||
if (seasonData.Data is{ Count: > 0 }) await UpdateWithSeasonData(seasonData.Data.ToList<IHistorySource>());
|
||||
if (seasonData.Data is{ Count: > 0 }){
|
||||
result = true;
|
||||
await UpdateWithSeasonData(seasonData.Data.ToList<IHistorySource>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||
historySeries ??= crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||
|
||||
if (historySeries != null){
|
||||
MatchHistorySeriesWithSonarr(false);
|
||||
await MatchHistoryEpisodesWithSonarr(false, historySeries);
|
||||
CfgManager.UpdateHistoryFile();
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +139,6 @@ public class History{
|
|||
historySeries.SeriesStreamingService = StreamingService.Crunchyroll;
|
||||
|
||||
await RefreshSeriesData(seriesId, historySeries);
|
||||
|
||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.GetSeasonId());
|
||||
|
||||
if (historySeason != null){
|
||||
|
|
@ -140,7 +163,8 @@ public class History{
|
|||
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
|
||||
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
|
||||
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
|
||||
EpisodeType = historySource.GetEpisodeType()
|
||||
EpisodeType = historySource.GetEpisodeType(),
|
||||
IsEpisodeAvailableOnStreamingService = true,
|
||||
};
|
||||
|
||||
historySeason.EpisodesList.Add(newHistoryEpisode);
|
||||
|
|
@ -154,6 +178,7 @@ public class History{
|
|||
historyEpisode.EpisodeSeasonNum = historySource.GetSeasonNum();
|
||||
historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate();
|
||||
historyEpisode.EpisodeType = historySource.GetEpisodeType();
|
||||
historyEpisode.IsEpisodeAvailableOnStreamingService = true;
|
||||
|
||||
historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang();
|
||||
historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs();
|
||||
|
|
@ -218,7 +243,7 @@ public class History{
|
|||
}
|
||||
}
|
||||
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 2));
|
||||
}
|
||||
|
||||
public HistoryEpisode? GetHistoryEpisode(string? seriesId, string? seasonId, string episodeId){
|
||||
|
|
@ -277,11 +302,11 @@ public class History{
|
|||
if (historySeries != null){
|
||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||
if (historySeries.HistorySeriesDubLangOverride.Count > 0){
|
||||
dublist = historySeries.HistorySeriesDubLangOverride;
|
||||
dublist = historySeries.HistorySeriesDubLangOverride.ToList();
|
||||
}
|
||||
|
||||
if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){
|
||||
sublist = historySeries.HistorySeriesSoftSubsOverride;
|
||||
sublist = historySeries.HistorySeriesSoftSubsOverride.ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){
|
||||
|
|
@ -295,11 +320,11 @@ public class History{
|
|||
if (historySeason != null){
|
||||
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
|
||||
if (historySeason.HistorySeasonDubLangOverride.Count > 0){
|
||||
dublist = historySeason.HistorySeasonDubLangOverride;
|
||||
dublist = historySeason.HistorySeasonDubLangOverride.ToList();
|
||||
}
|
||||
|
||||
if (historySeason.HistorySeasonSoftSubsOverride.Count > 0){
|
||||
sublist = historySeason.HistorySeasonSoftSubsOverride;
|
||||
sublist = historySeason.HistorySeasonSoftSubsOverride.ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){
|
||||
|
|
@ -327,11 +352,11 @@ public class History{
|
|||
if (historySeries != null){
|
||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||
if (historySeries.HistorySeriesDubLangOverride.Count > 0){
|
||||
dublist = historySeries.HistorySeriesDubLangOverride;
|
||||
dublist = historySeries.HistorySeriesDubLangOverride.ToList();
|
||||
}
|
||||
|
||||
if (historySeason is{ HistorySeasonDubLangOverride.Count: > 0 }){
|
||||
dublist = historySeason.HistorySeasonDubLangOverride;
|
||||
dublist = historySeason.HistorySeasonDubLangOverride.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,7 +372,7 @@ public class History{
|
|||
if (historySeries != null){
|
||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||
if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){
|
||||
sublist = historySeries.HistorySeriesSoftSubsOverride;
|
||||
sublist = historySeries.HistorySeriesSoftSubsOverride.ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){
|
||||
|
|
@ -355,7 +380,7 @@ public class History{
|
|||
}
|
||||
|
||||
if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){
|
||||
sublist = historySeason.HistorySeasonSoftSubsOverride;
|
||||
sublist = historySeason.HistorySeasonSoftSubsOverride.ToList();
|
||||
}
|
||||
|
||||
if (historySeason != null && !string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){
|
||||
|
|
@ -551,7 +576,8 @@ public class History{
|
|||
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
|
||||
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
|
||||
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
|
||||
EpisodeType = historySource.GetEpisodeType()
|
||||
EpisodeType = historySource.GetEpisodeType(),
|
||||
IsEpisodeAvailableOnStreamingService = true
|
||||
};
|
||||
|
||||
newSeason.EpisodesList.Add(newHistoryEpisode);
|
||||
|
|
@ -566,13 +592,22 @@ public class History{
|
|||
}
|
||||
|
||||
foreach (var historySeries in crunInstance.HistoryList){
|
||||
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
if (string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty);
|
||||
if (sonarrSeries != null){
|
||||
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
|
||||
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
|
||||
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
|
||||
}
|
||||
} else if (updateAll){
|
||||
var sonarrSeries = SonarrClient.Instance.SonarrSeries.FirstOrDefault(series => series.Id + "" == historySeries.SonarrSeriesId);
|
||||
if (sonarrSeries != null){
|
||||
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
|
||||
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
|
||||
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
|
||||
} else{
|
||||
Console.Error.WriteLine($"Unable to find sonarr series for {historySeries.SeriesTitle}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -716,7 +751,7 @@ public class History{
|
|||
if (string.IsNullOrEmpty(title)){
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
SonarrSeries? closestMatch = null;
|
||||
double highestSimilarity = 0.0;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.History;
|
||||
using CRD.Utils.Updater;
|
||||
using ExtendedXmlSerializer.Core.Sources;
|
||||
using FluentAvalonia.Styling;
|
||||
|
||||
namespace CRD.Downloader;
|
||||
|
|
@ -66,22 +68,42 @@ public partial class ProgramManager : ObservableObject{
|
|||
|
||||
private readonly FluentAvaloniaTheme? _faTheme;
|
||||
|
||||
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
|
||||
#region Startup Param Variables
|
||||
|
||||
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
|
||||
bool historyRefreshAdded = false;
|
||||
private bool exitOnTaskFinish;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
public IStorageProvider StorageProvider;
|
||||
|
||||
public ProgramManager(){
|
||||
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme;
|
||||
|
||||
foreach (var arg in Environment.GetCommandLineArgs()){
|
||||
if (arg == "--historyRefreshAll"){
|
||||
taskQueue.Enqueue(RefreshAll);
|
||||
} else if (arg == "--historyAddToQueue"){
|
||||
taskQueue.Enqueue(AddMissingToQueue);
|
||||
} else if (arg == "--exit"){
|
||||
exitOnTaskFinish = true;
|
||||
switch (arg){
|
||||
case "--historyRefreshAll":
|
||||
if (!historyRefreshAdded){
|
||||
taskQueue.Enqueue(() => RefreshHistory(FilterType.All));
|
||||
historyRefreshAdded = true;
|
||||
}
|
||||
|
||||
break;
|
||||
case "--historyRefreshActive":
|
||||
if (!historyRefreshAdded){
|
||||
taskQueue.Enqueue(() => RefreshHistory(FilterType.Active));
|
||||
historyRefreshAdded = true;
|
||||
}
|
||||
|
||||
break;
|
||||
case "--historyAddToQueue":
|
||||
taskQueue.Enqueue(AddMissingToQueue);
|
||||
break;
|
||||
case "--exit":
|
||||
exitOnTaskFinish = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,16 +112,54 @@ public partial class ProgramManager : ObservableObject{
|
|||
CleanUpOldUpdater();
|
||||
}
|
||||
|
||||
private async Task RefreshAll(){
|
||||
private async Task RefreshHistory(FilterType filterType){
|
||||
FetchingData = true;
|
||||
|
||||
foreach (var item in CrunchyrollManager.Instance.HistoryList){
|
||||
|
||||
List<HistorySeries> filteredItems;
|
||||
var historyList = CrunchyrollManager.Instance.HistoryList;
|
||||
|
||||
switch (filterType){
|
||||
case FilterType.All:
|
||||
filteredItems = historyList.ToList();
|
||||
break;
|
||||
|
||||
case FilterType.MissingEpisodes:
|
||||
filteredItems = historyList.Where(item => item.NewEpisodes > 0).ToList();
|
||||
break;
|
||||
|
||||
case FilterType.MissingEpisodesSonarr:
|
||||
filteredItems = historyList.Where(historySeries =>
|
||||
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) &&
|
||||
historySeries.Seasons.Any(season =>
|
||||
season.EpisodesList.Any(historyEpisode =>
|
||||
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile &&
|
||||
(!CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored || historyEpisode.SonarrIsMonitored))))
|
||||
.ToList();
|
||||
break;
|
||||
|
||||
case FilterType.ContinuingOnly:
|
||||
filteredItems = historyList.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
|
||||
break;
|
||||
case FilterType.Active:
|
||||
filteredItems = historyList.Where(item => !item.IsInactive).ToList();
|
||||
break;
|
||||
case FilterType.Inactive:
|
||||
filteredItems = historyList.Where(item => item.IsInactive).ToList();
|
||||
break;
|
||||
|
||||
default:
|
||||
filteredItems = new List<HistorySeries>();
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var item in filteredItems){
|
||||
item.SetFetchingData();
|
||||
}
|
||||
|
||||
for (int i = 0; i < CrunchyrollManager.Instance.HistoryList.Count; i++){
|
||||
await CrunchyrollManager.Instance.HistoryList[i].FetchData("");
|
||||
CrunchyrollManager.Instance.HistoryList[i].UpdateNewEpisodes();
|
||||
for (int i = 0; i < filteredItems.Count; i++){
|
||||
await filteredItems[i].FetchData("");
|
||||
filteredItems[i].UpdateNewEpisodes();
|
||||
}
|
||||
|
||||
FetchingData = false;
|
||||
|
|
@ -115,10 +175,16 @@ public partial class ProgramManager : ObservableObject{
|
|||
|
||||
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress.Done != true)){
|
||||
Console.WriteLine("Waiting for downloads to complete...");
|
||||
await Task.Delay(2000); // Wait for 2 second before checking again
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBackgroundImage(){
|
||||
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){
|
||||
Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity,
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task Init(){
|
||||
CrunchyrollManager.Instance.InitOptions();
|
||||
|
|
@ -142,12 +208,7 @@ public partial class ProgramManager : ObservableObject{
|
|||
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){
|
||||
Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity,
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius);
|
||||
}
|
||||
|
||||
|
||||
await CrunchyrollManager.Instance.Init();
|
||||
|
||||
FinishedLoading = true;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public partial class QueueManager : ObservableObject{
|
|||
public int ActiveDownloads;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hasFailedItem;
|
||||
|
||||
|
|
@ -92,9 +92,8 @@ public partial class QueueManager : ObservableObject{
|
|||
}
|
||||
|
||||
HasFailedItem = Queue.Any(item => item.DownloadProgress.Error);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){
|
||||
if (string.IsNullOrEmpty(epId)){
|
||||
|
|
@ -190,6 +189,7 @@ public partial class QueueManager : ObservableObject{
|
|||
|
||||
Queue.Add(selected);
|
||||
|
||||
|
||||
if (selected.Data.Count < dubLang.Count && !CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub){
|
||||
Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs");
|
||||
Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: ");
|
||||
|
|
@ -214,38 +214,44 @@ public partial class QueueManager : ObservableObject{
|
|||
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
|
||||
}
|
||||
} else{
|
||||
Console.WriteLine("Couldn't find episode trying to find movie with id");
|
||||
|
||||
var movie = await CrunchyrollManager.Instance.CrMovies.ParseMovieById(epId, crLocale);
|
||||
return;
|
||||
}
|
||||
|
||||
if (movie != null){
|
||||
var movieMeta = CrunchyrollManager.Instance.CrMovies.EpisodeMeta(movie, dubLang);
|
||||
Console.WriteLine("Couldn't find episode trying to find movie with id");
|
||||
|
||||
if (movieMeta != null){
|
||||
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
|
||||
movieMeta.OnlySubs = onlySubs;
|
||||
var movie = await CrunchyrollManager.Instance.CrMovies.ParseMovieById(epId, crLocale);
|
||||
|
||||
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
||||
if (movie != null){
|
||||
var movieMeta = CrunchyrollManager.Instance.CrMovies.EpisodeMeta(movie, dubLang);
|
||||
|
||||
if (movieMeta.OnlySubs){
|
||||
newOptions.Novids = true;
|
||||
newOptions.Noaudio = true;
|
||||
}
|
||||
if (movieMeta != null){
|
||||
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
|
||||
movieMeta.OnlySubs = onlySubs;
|
||||
|
||||
newOptions.DubLang = dubLang;
|
||||
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
||||
|
||||
movieMeta.DownloadSettings = newOptions;
|
||||
|
||||
movieMeta.VideoQuality = CrunchyrollManager.Instance.CrunOptions.QualityVideo;
|
||||
|
||||
Queue.Add(movieMeta);
|
||||
|
||||
Console.WriteLine("Added Movie to Queue");
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Added Movie to Queue", ToastType.Information, 1));
|
||||
if (movieMeta.OnlySubs){
|
||||
newOptions.Novids = true;
|
||||
newOptions.Noaudio = true;
|
||||
}
|
||||
|
||||
newOptions.DubLang = dubLang;
|
||||
|
||||
movieMeta.DownloadSettings = newOptions;
|
||||
|
||||
movieMeta.VideoQuality = CrunchyrollManager.Instance.CrunOptions.QualityVideo;
|
||||
|
||||
Queue.Add(movieMeta);
|
||||
|
||||
Console.WriteLine("Added Movie to Queue");
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Added Movie to Queue", ToastType.Information, 1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"No episode or movie found with the id: {epId}");
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue - No episode or movie found with the id: {epId}", ToastType.Error, 3));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,37 @@
|
|||
using System.Runtime.Serialization;
|
||||
using CRD.Utils.JsonConv;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace CRD.Utils;
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum StreamingService{
|
||||
[EnumMember(Value = "Crunchyroll")]
|
||||
Crunchyroll,
|
||||
[EnumMember(Value = "Unknown")]
|
||||
Unknown
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum EpisodeType{
|
||||
[EnumMember(Value = "MusicVideo")]
|
||||
MusicVideo,
|
||||
[EnumMember(Value = "Concert")]
|
||||
Concert,
|
||||
[EnumMember(Value = "Episode")]
|
||||
Episode,
|
||||
[EnumMember(Value = "Unknown")]
|
||||
Unknown
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum SeriesType{
|
||||
[EnumMember(Value = "Artist")]
|
||||
Artist,
|
||||
[EnumMember(Value = "Series")]
|
||||
Series,
|
||||
[EnumMember(Value = "Unknown")]
|
||||
Unknown
|
||||
}
|
||||
|
||||
|
|
@ -171,17 +184,25 @@ public enum DownloadMediaType{
|
|||
Description,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum ScaledBorderAndShadowSelection{
|
||||
[EnumMember(Value = "Dont Add")]
|
||||
DontAdd,
|
||||
[EnumMember(Value = "ScaledBorderAndShadow Yes")]
|
||||
ScaledBorderAndShadowYes,
|
||||
[EnumMember(Value = "ScaledBorderAndShadow No")]
|
||||
ScaledBorderAndShadowNo,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum HistoryViewType{
|
||||
[EnumMember(Value = "Posters")]
|
||||
Posters,
|
||||
[EnumMember(Value = "Table")]
|
||||
Table,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum SortingType{
|
||||
[EnumMember(Value = "Series Title")]
|
||||
SeriesTitle,
|
||||
|
|
@ -193,6 +214,7 @@ public enum SortingType{
|
|||
HistorySeriesAddDate,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum FilterType{
|
||||
[EnumMember(Value = "All")]
|
||||
All,
|
||||
|
|
@ -205,14 +227,27 @@ public enum FilterType{
|
|||
|
||||
[EnumMember(Value = "Continuing Only")]
|
||||
ContinuingOnly,
|
||||
|
||||
[EnumMember(Value = "Active")]
|
||||
Active,
|
||||
|
||||
[EnumMember(Value = "Inactive")]
|
||||
Inactive,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum CrunchyUrlType{
|
||||
[EnumMember(Value = "Artist")]
|
||||
Artist,
|
||||
[EnumMember(Value = "MusicVideo")]
|
||||
MusicVideo,
|
||||
[EnumMember(Value = "Concert")]
|
||||
Concert,
|
||||
[EnumMember(Value = "Episode")]
|
||||
Episode,
|
||||
[EnumMember(Value = "Series")]
|
||||
Series,
|
||||
[EnumMember(Value = "Unknown")]
|
||||
Unknown
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ public class HlsDownloader{
|
|||
throw new Exception("Failed to download key");
|
||||
_data.Keys[kUri] = rkey;
|
||||
} catch (Exception ex){
|
||||
throw new Exception($"Error at segment {p}: {ex.Message}", ex);
|
||||
throw new Exception($"Key Error at segment {p}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ public class HlsDownloader{
|
|||
|
||||
// Set default user-agent if not provided
|
||||
if (!request.Headers.Contains("User-Agent")){
|
||||
request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0");
|
||||
request.Headers.Add("User-Agent", ApiUrls.FirefoxUserAgent);
|
||||
}
|
||||
|
||||
return await SendRequestWithRetry(request, partIndex, segOffset, isKey, retryCount);
|
||||
|
|
@ -445,7 +445,7 @@ public class HlsDownloader{
|
|||
response = await HttpClientReq.Instance.GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await ReadContentAsByteArrayAsync(response.Content);
|
||||
} catch (HttpRequestException ex){
|
||||
} catch (Exception ex) when (ex is HttpRequestException or IOException){
|
||||
// Log retry attempts
|
||||
string partType = isKey ? "Key" : "Part";
|
||||
int partIndx = partIndex + 1 + segOffset;
|
||||
|
|
@ -453,8 +453,14 @@ public class HlsDownloader{
|
|||
Console.WriteLine($"\tError: {ex.Message}");
|
||||
if (attempt == retryCount)
|
||||
throw; // rethrow after last retry
|
||||
|
||||
|
||||
await Task.Delay(_data.WaitTime);
|
||||
}catch (Exception ex) {
|
||||
|
||||
Console.WriteLine($"Unexpected exception at part {partIndex + 1 + segOffset}:");
|
||||
Console.WriteLine($"\tType: {ex.GetType()}");
|
||||
Console.WriteLine($"\tMessage: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,6 +406,7 @@ public class Helpers{
|
|||
// If something went wrong, delete the temporary output file
|
||||
File.Delete(tempOutputFilePath);
|
||||
Console.Error.WriteLine("FFmpeg processing failed.");
|
||||
Console.Error.WriteLine($"Command: {ffmpegCommand}");
|
||||
}
|
||||
|
||||
return (IsOk: isSuccess, ErrorCode: process.ExitCode);
|
||||
|
|
@ -774,7 +775,7 @@ public class Helpers{
|
|||
AutoDownload = yaml.AutoDownload,
|
||||
RemoveFinishedDownload = yaml.RemoveFinishedDownload,
|
||||
Timeout = yaml.Timeout,
|
||||
FsRetryTime = yaml.FsRetryTime,
|
||||
RetryDelay = yaml.FsRetryTime,
|
||||
Force = yaml.Force,
|
||||
SimultaneousDownloads = yaml.SimultaneousDownloads,
|
||||
Theme = yaml.Theme,
|
||||
|
|
|
|||
|
|
@ -271,5 +271,6 @@ public static class ApiUrls{
|
|||
|
||||
public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE=";
|
||||
|
||||
public static readonly string MobileUserAgent = "Crunchyroll/3.78.3 Android/15 okhttp/4.12.0";
|
||||
public static readonly string MobileUserAgent = "Crunchyroll/3.79.0 Android/15 okhttp/4.12.0";
|
||||
public static readonly string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0";
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ public class LocaleConverter : JsonConverter{
|
|||
return locale;
|
||||
}
|
||||
|
||||
return Locale.Unknown; // Default to defaulT if no match is found
|
||||
return Locale.Unknown;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer){
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -312,6 +313,7 @@ public class Merger{
|
|||
return -100;
|
||||
}
|
||||
|
||||
|
||||
// Load frames from start of the videos
|
||||
var baseFramesStart = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData{
|
||||
FilePath = fp,
|
||||
|
|
@ -401,9 +403,10 @@ public class Merger{
|
|||
var result = await Helpers.ExecuteCommandAsync(type, bin, command);
|
||||
|
||||
if (!result.IsOk && type == "mkvmerge" && result.ErrorCode == 1){
|
||||
Console.WriteLine($"[{type}] Mkvmerge finished with at least one warning");
|
||||
Console.Error.WriteLine($"[{type}] Mkvmerge finished with at least one warning");
|
||||
} else if (!result.IsOk){
|
||||
Console.Error.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}");
|
||||
Console.Error.WriteLine($"[{type}] Merging failed command: {command}");
|
||||
return false;
|
||||
} else{
|
||||
Console.WriteLine($"[{type} Done]");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Globalization;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using SixLabors.ImageSharp;
|
||||
|
|
@ -17,7 +18,8 @@ namespace CRD.Utils.Muxing;
|
|||
public class SyncingHelper{
|
||||
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
|
||||
var ffmpegPath = CfgManager.PathFFMPEG;
|
||||
var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -fps_mode vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%05d.png\"";
|
||||
var arguments =
|
||||
$"{CrunchyrollManager.Instance.CrunOptions.FfmpegHwAccelFlag}-ss {offset} -t {duration} -i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -vsync vfr -frame_pts true \"{outputDir}\\frame%05d.jpg\"";
|
||||
|
||||
var output = "";
|
||||
|
||||
|
|
@ -86,8 +88,8 @@ public class SyncingHelper{
|
|||
var2 /= count - 1;
|
||||
covariance /= count - 1;
|
||||
|
||||
double c1 = 0.01 * 0.01 * 255 * 255;
|
||||
double c2 = 0.03 * 0.03 * 255 * 255;
|
||||
double c1 = 0.01 * 0.01;
|
||||
double c2 = 0.03 * 0.03;
|
||||
|
||||
double ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) /
|
||||
((mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2));
|
||||
|
|
@ -103,7 +105,8 @@ public class SyncingHelper{
|
|||
for (int y = 0; y < accessor.Height; y++){
|
||||
Span<Rgba32> row = accessor.GetRowSpan(y);
|
||||
for (int x = 0; x < row.Length; x++){
|
||||
pixels[index++] = row[x].R;
|
||||
pixels[index++] = row[x].R / 255f;
|
||||
;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -130,16 +133,17 @@ public class SyncingHelper{
|
|||
float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight);
|
||||
|
||||
// Check if any frame is completely black, if so, skip SSIM calculation
|
||||
if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2)){
|
||||
if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2) ||
|
||||
IsMonochromaticFrame(pixels1) || IsMonochromaticFrame(pixels2)){
|
||||
// Return a negative value or zero to indicate no SSIM comparison for black frames.
|
||||
return (-1.0,99);
|
||||
return (-1.0, 99);
|
||||
}
|
||||
|
||||
|
||||
// Compute SSIM
|
||||
return (CalculateSSIM(pixels1, pixels2),CalculatePixelDifference(pixels1,pixels2));
|
||||
return (CalculateSSIM(pixels1, pixels2), CalculatePixelDifference(pixels1, pixels2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static double CalculatePixelDifference(float[] pixels1, float[] pixels2){
|
||||
double totalDifference = 0;
|
||||
int count = pixels1.Length;
|
||||
|
|
@ -151,40 +155,84 @@ public class SyncingHelper{
|
|||
return totalDifference / count; // Average difference
|
||||
}
|
||||
|
||||
private static bool IsBlackFrame(float[] pixels, float threshold = 1.0f){
|
||||
private static bool IsBlackFrame(float[] pixels, float threshold = 0.02f){
|
||||
// Check if all pixel values are below the threshold, indicating a black frame.
|
||||
return pixels.All(p => p <= threshold);
|
||||
}
|
||||
|
||||
|
||||
private static bool IsMonochromaticFrame(float[] pixels, float stdDevThreshold = 0.05f){
|
||||
float avg = pixels.Average();
|
||||
double variance = pixels.Average(p => Math.Pow(p - avg, 2));
|
||||
double stdDev = Math.Sqrt(variance);
|
||||
return stdDev < stdDevThreshold;
|
||||
}
|
||||
|
||||
public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){
|
||||
var (ssim, pixelDiff) = ComputeSSIM(imagePath1, imagePath2, 256, 256);
|
||||
var (ssim, pixelDiff) = ComputeSSIM(imagePath1, imagePath2, 256, 144);
|
||||
// Console.WriteLine($"SSIM: {ssim}");
|
||||
// Console.WriteLine(pixelDiff);
|
||||
|
||||
return ssim > ssimThreshold && pixelDiff < 10;
|
||||
|
||||
return ssim > ssimThreshold && pixelDiff < 0.04;
|
||||
}
|
||||
|
||||
|
||||
public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames,bool reverseCompare = false, double ssimThreshold = 0.9){
|
||||
public static float[] GetPixelsArray(string imagePath, int targetWidth = 256, int targetHeight = 144){
|
||||
using var image = Image.Load<Rgba32>(imagePath);
|
||||
image.Mutate(x => x.Resize(new ResizeOptions{
|
||||
Size = new Size(targetWidth, targetHeight),
|
||||
Mode = ResizeMode.Max
|
||||
}).Grayscale());
|
||||
return ExtractPixels(image, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
public static bool AreFramesSimilarPreprocessed(float[] image1, float[] image2, double ssimThreshold){
|
||||
if (IsBlackFrame(image1) || IsBlackFrame(image2) ||
|
||||
IsMonochromaticFrame(image1) || IsMonochromaticFrame(image2)){
|
||||
return false;
|
||||
}
|
||||
|
||||
var pixelDiff = CalculatePixelDifference(image1, image2);
|
||||
|
||||
if (pixelDiff > 0.04){
|
||||
return false;
|
||||
}
|
||||
|
||||
var ssim = CalculateSSIM(image1, image2);
|
||||
|
||||
return ssim > ssimThreshold && pixelDiff < 0.04;
|
||||
}
|
||||
|
||||
public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames, bool reverseCompare = false, double ssimThreshold = 0.9){
|
||||
if (reverseCompare){
|
||||
baseFrames.Reverse();
|
||||
compareFrames.Reverse();
|
||||
}
|
||||
|
||||
|
||||
var preprocessedCompareFrames = compareFrames.Select(f => new{
|
||||
Frame = f,
|
||||
Pixels = GetPixelsArray(f.FilePath)
|
||||
}).ToList();
|
||||
|
||||
var delay = 0.0;
|
||||
|
||||
foreach (var baseFrame in baseFrames){
|
||||
var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold));
|
||||
var baseFramePixels = GetPixelsArray(baseFrame.FilePath);
|
||||
var matchingFrame = preprocessedCompareFrames.AsParallel()
|
||||
.WithExecutionMode(ParallelExecutionMode.ForceParallelism).FirstOrDefault(f => AreFramesSimilarPreprocessed(baseFramePixels, f.Pixels, ssimThreshold));
|
||||
if (matchingFrame != null){
|
||||
Console.WriteLine($"Matched Frame:");
|
||||
Console.WriteLine($"\t Base Frame Path: {baseFrame.FilePath} Time: {baseFrame.Time},");
|
||||
Console.WriteLine($"\t Compare Frame Path: {matchingFrame.FilePath} Time: {matchingFrame.Time}");
|
||||
return baseFrame.Time - matchingFrame.Time;
|
||||
Console.WriteLine($"\t Compare Frame Path: {matchingFrame.Frame.FilePath} Time: {matchingFrame.Frame.Time}");
|
||||
delay = baseFrame.Time - matchingFrame.Frame.Time;
|
||||
break;
|
||||
} else{
|
||||
// Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");
|
||||
Debug.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
preprocessedCompareFrames.Clear();
|
||||
GC.Collect(); // Segment float arrays to avoid calling GC.Collect ?
|
||||
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,11 @@ public class CrDownloadOptions{
|
|||
[JsonIgnore]
|
||||
public int Timeout{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int FsRetryTime{ get; set; }
|
||||
[JsonProperty("retry_delay")]
|
||||
public int RetryDelay{ get; set; }
|
||||
|
||||
[JsonProperty("retry_attempts")]
|
||||
public int RetryAttempts{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Force{ get; set; } = "";
|
||||
|
|
@ -232,6 +235,9 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("mux_sync_dubs")]
|
||||
public bool SyncTiming{ get; set; }
|
||||
|
||||
[JsonProperty("mux_sync_hwaccel")]
|
||||
public string? FfmpegHwAccelFlag{ get; set; }
|
||||
|
||||
[JsonProperty("encode_enabled")]
|
||||
public bool IsEncodeEnabled{ get; set; }
|
||||
|
|
|
|||
|
|
@ -95,4 +95,8 @@ public class CrunchyMovie{
|
|||
|
||||
[JsonProperty("premium_date")]
|
||||
public DateTime PremiumDate{ get; set; }
|
||||
|
||||
[JsonProperty("type")]
|
||||
public string type{ get; set; }
|
||||
|
||||
}
|
||||
|
|
@ -108,6 +108,11 @@ public class StringItem{
|
|||
public string stringValue{ get; set; }
|
||||
}
|
||||
|
||||
public class StringItemWithDisplayName{
|
||||
public string DisplayName{ get; set; }
|
||||
public string value{ get; set; }
|
||||
}
|
||||
|
||||
public class WindowSettings{
|
||||
public double Width{ get; set; }
|
||||
public double Height{ get; set; }
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
[JsonProperty("episode_special_episode")]
|
||||
public bool SpecialEpisode{ get; set; }
|
||||
|
||||
[JsonProperty("episode_available_on_streaming_service")]
|
||||
public bool IsEpisodeAvailableOnStreamingService{ get; set; }
|
||||
|
||||
[JsonProperty("episode_type")]
|
||||
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown;
|
||||
|
||||
|
|
@ -61,6 +64,22 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
[JsonProperty("history_episode_available_dub_lang")]
|
||||
public List<string> HistoryEpisodeAvailableDubLang{ get; set; } =[];
|
||||
|
||||
[JsonIgnore]
|
||||
public string ReleaseDateFormated{
|
||||
get{
|
||||
if (!EpisodeCrPremiumAirDate.HasValue ||
|
||||
EpisodeCrPremiumAirDate.Value == DateTime.MinValue ||
|
||||
EpisodeCrPremiumAirDate.Value.Date == new DateTime(1970, 1, 1))
|
||||
return string.Empty;
|
||||
|
||||
|
||||
var cultureInfo = System.Globalization.CultureInfo.InvariantCulture;
|
||||
string monthAbbreviation = cultureInfo.DateTimeFormat.GetAbbreviatedMonthName(EpisodeCrPremiumAirDate.Value.Month);
|
||||
|
||||
return string.Format("{0:00}.{1}.{2}", EpisodeCrPremiumAirDate.Value.Day, monthAbbreviation, EpisodeCrPremiumAirDate.Value.Year);
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public void ToggleWasDownloaded(){
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
public string HistorySeasonVideoQualityOverride{ get; set; } = "";
|
||||
|
||||
[JsonProperty("history_season_soft_subs_override")]
|
||||
public List<string> HistorySeasonSoftSubsOverride{ get; set; } =[];
|
||||
public ObservableCollection<string> HistorySeasonSoftSubsOverride{ get; set; } =[];
|
||||
|
||||
[JsonProperty("history_season_dub_lang_override")]
|
||||
public List<string> HistorySeasonDubLangOverride{ get; set; } =[];
|
||||
public ObservableCollection<string> HistorySeasonDubLangOverride{ get; set; } =[];
|
||||
|
||||
[JsonIgnore]
|
||||
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ using Avalonia.Media.Imaging;
|
|||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.CustomList;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Views;
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace CRD.Utils.Structs.History;
|
||||
|
||||
|
|
@ -20,6 +22,9 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
[JsonProperty("series_type")]
|
||||
public SeriesType SeriesType{ get; set; } = SeriesType.Unknown;
|
||||
|
||||
[JsonProperty("series_is_inactive")]
|
||||
public bool IsInactive{ get; set; }
|
||||
|
||||
[JsonProperty("series_title")]
|
||||
public string? SeriesTitle{ get; set; }
|
||||
|
|
@ -67,10 +72,10 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
public List<string> HistorySeriesAvailableDubLang{ get; set; } =[];
|
||||
|
||||
[JsonProperty("history_series_soft_subs_override")]
|
||||
public List<string> HistorySeriesSoftSubsOverride{ get; set; } =[];
|
||||
public ObservableCollection<string> HistorySeriesSoftSubsOverride{ get; set; } =[];
|
||||
|
||||
[JsonProperty("history_series_dub_lang_override")]
|
||||
public List<string> HistorySeriesDubLangOverride{ get; set; } =[];
|
||||
public ObservableCollection<string> HistorySeriesDubLangOverride{ get; set; } =[];
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
|
|
@ -460,17 +465,19 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
}
|
||||
}
|
||||
|
||||
public async Task FetchData(string? seasonId){
|
||||
public async Task<bool> FetchData(string? seasonId){
|
||||
Console.WriteLine($"Fetching Data for: {SeriesTitle}");
|
||||
FetchingData = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
|
||||
|
||||
var isOk = true;
|
||||
|
||||
switch (SeriesType){
|
||||
case SeriesType.Artist:
|
||||
try{
|
||||
await CrunchyrollManager.Instance.CrMusic.ParseArtistVideosByIdAsync(SeriesId,
|
||||
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, true, true);
|
||||
} catch (Exception e){
|
||||
isOk = false;
|
||||
Console.Error.WriteLine("Failed to update History artist");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
|
|
@ -480,8 +487,9 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
case SeriesType.Unknown:
|
||||
default:
|
||||
try{
|
||||
await CrunchyrollManager.Instance.History.CrUpdateSeries(SeriesId, seasonId);
|
||||
isOk = await CrunchyrollManager.Instance.History.CrUpdateSeries(SeriesId, seasonId);
|
||||
} catch (Exception e){
|
||||
isOk = false;
|
||||
Console.Error.WriteLine("Failed to update History series");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
|
|
@ -495,6 +503,8 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
UpdateNewEpisodes();
|
||||
FetchingData = false;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
|
||||
|
||||
return isOk;
|
||||
}
|
||||
|
||||
public void RemoveSeason(string? season){
|
||||
|
|
|
|||
139
CRD/Utils/UI/EpisodeHighlightTextBlock.cs
Normal file
139
CRD/Utils/UI/EpisodeHighlightTextBlock.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.Structs.History;
|
||||
|
||||
namespace CRD.Utils.UI;
|
||||
|
||||
public class EpisodeHighlightTextBlock : TextBlock{
|
||||
public static readonly StyledProperty<HistorySeries?> SeriesProperty =
|
||||
AvaloniaProperty.Register<EpisodeHighlightTextBlock, HistorySeries?>(nameof(Series));
|
||||
|
||||
public static readonly StyledProperty<HistorySeason?> SeasonProperty =
|
||||
AvaloniaProperty.Register<EpisodeHighlightTextBlock, HistorySeason?>(nameof(Season));
|
||||
|
||||
public static readonly StyledProperty<HistoryEpisode?> EpisodeProperty =
|
||||
AvaloniaProperty.Register<EpisodeHighlightTextBlock, HistoryEpisode?>(nameof(Episode));
|
||||
|
||||
public static readonly StyledProperty<StreamingService> StreamingServiceProperty =
|
||||
AvaloniaProperty.Register<EpisodeHighlightTextBlock, StreamingService>(nameof(StreamingService));
|
||||
|
||||
|
||||
private HistorySeries? _lastSeries;
|
||||
private HistorySeason? _lastSeason;
|
||||
|
||||
public HistorySeries? Series{
|
||||
get => GetValue(SeriesProperty);
|
||||
set => SetValue(SeriesProperty, value);
|
||||
}
|
||||
|
||||
public HistorySeason? Season{
|
||||
get => GetValue(SeasonProperty);
|
||||
set => SetValue(SeasonProperty, value);
|
||||
}
|
||||
|
||||
public HistoryEpisode? Episode{
|
||||
get => GetValue(EpisodeProperty);
|
||||
set => SetValue(EpisodeProperty, value);
|
||||
}
|
||||
|
||||
public StreamingService StreamingService{
|
||||
get => GetValue(StreamingServiceProperty);
|
||||
set => SetValue(StreamingServiceProperty, value);
|
||||
}
|
||||
|
||||
private void SubscribeSeries(HistorySeries series){
|
||||
series.HistorySeriesDubLangOverride.CollectionChanged += OnCollectionChanged;
|
||||
series.HistorySeriesSoftSubsOverride.CollectionChanged += OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void UnsubscribeSeries(HistorySeries series){
|
||||
series.HistorySeriesDubLangOverride.CollectionChanged -= OnCollectionChanged;
|
||||
series.HistorySeriesSoftSubsOverride.CollectionChanged -= OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void SubscribeSeason(HistorySeason season){
|
||||
season.HistorySeasonDubLangOverride.CollectionChanged += OnCollectionChanged;
|
||||
season.HistorySeasonSoftSubsOverride.CollectionChanged += OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void UnsubscribeSeason(HistorySeason season){
|
||||
season.HistorySeasonDubLangOverride.CollectionChanged -= OnCollectionChanged;
|
||||
season.HistorySeasonSoftSubsOverride.CollectionChanged -= OnCollectionChanged;
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e){
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
|
||||
if (_lastSeries != null)
|
||||
UnsubscribeSeries(_lastSeries);
|
||||
|
||||
if (_lastSeason != null)
|
||||
UnsubscribeSeason(_lastSeason);
|
||||
}
|
||||
|
||||
private void OnCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e){
|
||||
UpdateText();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change){
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property == SeriesProperty){
|
||||
if (_lastSeries != null)
|
||||
UnsubscribeSeries(_lastSeries);
|
||||
|
||||
_lastSeries = change.NewValue as HistorySeries;
|
||||
|
||||
if (_lastSeries != null)
|
||||
SubscribeSeries(_lastSeries);
|
||||
}
|
||||
|
||||
if (change.Property == SeasonProperty){
|
||||
if (_lastSeason != null)
|
||||
UnsubscribeSeason(_lastSeason);
|
||||
|
||||
_lastSeason = change.NewValue as HistorySeason;
|
||||
|
||||
if (_lastSeason != null)
|
||||
SubscribeSeason(_lastSeason);
|
||||
}
|
||||
|
||||
if (change.Property == SeriesProperty ||
|
||||
change.Property == SeasonProperty ||
|
||||
change.Property == StreamingServiceProperty){
|
||||
UpdateText();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateText(){
|
||||
|
||||
Text = "E" + Episode?.Episode + " - " + Episode?.EpisodeTitle;
|
||||
|
||||
var streamingService = Series?.SeriesStreamingService ?? StreamingService;
|
||||
|
||||
var dubSet =
|
||||
Season?.HistorySeasonDubLangOverride.Any() == true ? new HashSet<string>(Season.HistorySeasonDubLangOverride) :
|
||||
Series?.HistorySeriesDubLangOverride.Any() == true ? new HashSet<string>(Series.HistorySeriesDubLangOverride) :
|
||||
streamingService == StreamingService.Crunchyroll ? new HashSet<string>(CrunchyrollManager.Instance.CrunOptions.DubLang) :
|
||||
new HashSet<string>();
|
||||
|
||||
var subSet =
|
||||
Season?.HistorySeasonSoftSubsOverride.Any() == true ? new HashSet<string>(Season.HistorySeasonSoftSubsOverride) :
|
||||
Series?.HistorySeriesSoftSubsOverride.Any() == true ? new HashSet<string>(Series.HistorySeriesSoftSubsOverride) :
|
||||
streamingService == StreamingService.Crunchyroll ? new HashSet<string>(CrunchyrollManager.Instance.CrunOptions.DlSubs) :
|
||||
new HashSet<string>();
|
||||
|
||||
var higlight = dubSet.IsSubsetOf(Episode?.HistoryEpisodeAvailableDubLang ?? []) &&
|
||||
subSet.IsSubsetOf(Episode?.HistoryEpisodeAvailableSoftSubs ?? []);
|
||||
|
||||
if (higlight){
|
||||
Foreground = Brushes.Orange;
|
||||
} else{
|
||||
ClearValue(ForegroundProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
170
CRD/Utils/UI/HighlightingTextBlock.cs
Normal file
170
CRD/Utils/UI/HighlightingTextBlock.cs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Media;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.Structs.History;
|
||||
|
||||
namespace CRD.Utils.UI;
|
||||
|
||||
public class HighlightingTextBlock : TextBlock{
|
||||
public static readonly StyledProperty<IEnumerable<string>?> ItemsProperty =
|
||||
AvaloniaProperty.Register<HighlightingTextBlock, IEnumerable<string>?>(nameof(Items));
|
||||
|
||||
public static readonly StyledProperty<HistorySeries?> SeriesProperty =
|
||||
AvaloniaProperty.Register<HighlightingTextBlock, HistorySeries?>(nameof(Series));
|
||||
|
||||
public static readonly StyledProperty<HistorySeason?> SeasonProperty =
|
||||
AvaloniaProperty.Register<HighlightingTextBlock, HistorySeason?>(nameof(Season));
|
||||
|
||||
public static readonly StyledProperty<StreamingService> StreamingServiceProperty =
|
||||
AvaloniaProperty.Register<HighlightingTextBlock, StreamingService>(nameof(StreamingService));
|
||||
|
||||
public static readonly StyledProperty<bool> CheckDubsProperty =
|
||||
AvaloniaProperty.Register<HighlightingTextBlock, bool>(nameof(CheckDubs));
|
||||
|
||||
public static readonly StyledProperty<bool> HighlightEntireTextProperty =
|
||||
AvaloniaProperty.Register<HighlightingTextBlock, bool>(nameof(HighlightEntireText));
|
||||
|
||||
private HistorySeries? _lastSeries;
|
||||
private HistorySeason? _lastSeason;
|
||||
|
||||
public bool HighlightEntireText{
|
||||
get => GetValue(HighlightEntireTextProperty);
|
||||
set => SetValue(HighlightEntireTextProperty, value);
|
||||
}
|
||||
|
||||
public bool CheckDubs{
|
||||
get => GetValue(CheckDubsProperty);
|
||||
set => SetValue(CheckDubsProperty, value);
|
||||
}
|
||||
|
||||
public IEnumerable<string>? Items{
|
||||
get => GetValue(ItemsProperty);
|
||||
set => SetValue(ItemsProperty, value);
|
||||
}
|
||||
|
||||
public HistorySeries? Series{
|
||||
get => GetValue(SeriesProperty);
|
||||
set => SetValue(SeriesProperty, value);
|
||||
}
|
||||
|
||||
public HistorySeason? Season{
|
||||
get => GetValue(SeasonProperty);
|
||||
set => SetValue(SeasonProperty, value);
|
||||
}
|
||||
|
||||
public StreamingService StreamingService{
|
||||
get => GetValue(StreamingServiceProperty);
|
||||
set => SetValue(StreamingServiceProperty, value);
|
||||
}
|
||||
|
||||
private void SubscribeSeries(HistorySeries series){
|
||||
series.HistorySeriesDubLangOverride.CollectionChanged += OnCollectionChanged;
|
||||
series.HistorySeriesSoftSubsOverride.CollectionChanged += OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void UnsubscribeSeries(HistorySeries series){
|
||||
series.HistorySeriesDubLangOverride.CollectionChanged -= OnCollectionChanged;
|
||||
series.HistorySeriesSoftSubsOverride.CollectionChanged -= OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void SubscribeSeason(HistorySeason season){
|
||||
season.HistorySeasonDubLangOverride.CollectionChanged += OnCollectionChanged;
|
||||
season.HistorySeasonSoftSubsOverride.CollectionChanged += OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void UnsubscribeSeason(HistorySeason season){
|
||||
season.HistorySeasonDubLangOverride.CollectionChanged -= OnCollectionChanged;
|
||||
season.HistorySeasonSoftSubsOverride.CollectionChanged -= OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void OnCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e){
|
||||
UpdateText();
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e){
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
|
||||
if (_lastSeries != null)
|
||||
UnsubscribeSeries(_lastSeries);
|
||||
|
||||
if (_lastSeason != null)
|
||||
UnsubscribeSeason(_lastSeason);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change){
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property == SeriesProperty){
|
||||
if (_lastSeries != null)
|
||||
UnsubscribeSeries(_lastSeries);
|
||||
|
||||
_lastSeries = change.NewValue as HistorySeries;
|
||||
|
||||
if (_lastSeries != null)
|
||||
SubscribeSeries(_lastSeries);
|
||||
}
|
||||
|
||||
if (change.Property == SeasonProperty){
|
||||
if (_lastSeason != null)
|
||||
UnsubscribeSeason(_lastSeason);
|
||||
|
||||
_lastSeason = change.NewValue as HistorySeason;
|
||||
|
||||
if (_lastSeason != null)
|
||||
SubscribeSeason(_lastSeason);
|
||||
}
|
||||
|
||||
if (change.Property == ItemsProperty ||
|
||||
change.Property == SeriesProperty ||
|
||||
change.Property == SeasonProperty ||
|
||||
change.Property == StreamingServiceProperty ||
|
||||
change.Property == CheckDubsProperty){
|
||||
UpdateText();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateText(){
|
||||
Inlines?.Clear();
|
||||
if (Items == null) return;
|
||||
|
||||
var streamingService = Series?.SeriesStreamingService ?? StreamingService;
|
||||
|
||||
IEnumerable<string> source;
|
||||
|
||||
if (CheckDubs){
|
||||
source =
|
||||
Season?.HistorySeasonDubLangOverride?.Any() == true ? Season.HistorySeasonDubLangOverride :
|
||||
Series?.HistorySeriesDubLangOverride?.Any() == true ? Series.HistorySeriesDubLangOverride :
|
||||
streamingService == StreamingService.Crunchyroll ? CrunchyrollManager.Instance.CrunOptions.DubLang :
|
||||
Enumerable.Empty<string>();
|
||||
} else{
|
||||
source =
|
||||
Season?.HistorySeasonSoftSubsOverride?.Any() == true ? Season.HistorySeasonSoftSubsOverride :
|
||||
Series?.HistorySeriesSoftSubsOverride?.Any() == true ? Series.HistorySeriesSoftSubsOverride :
|
||||
streamingService == StreamingService.Crunchyroll ? CrunchyrollManager.Instance.CrunOptions.DlSubs :
|
||||
Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
var highlightSet = new HashSet<string>(source);
|
||||
|
||||
|
||||
foreach (var item in Items){
|
||||
var run = new Run(item);
|
||||
|
||||
if (highlightSet.Contains(item)){
|
||||
run.Foreground = Brushes.Orange;
|
||||
// run.FontWeight = FontWeight.Bold;
|
||||
}
|
||||
|
||||
Inlines?.Add(run);
|
||||
Inlines?.Add(new Run(", "));
|
||||
}
|
||||
|
||||
if (Inlines?.Count > 0)
|
||||
Inlines.RemoveAt(Inlines.Count - 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ using CRD.Utils.Files;
|
|||
using CRD.Utils.Sonarr;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.History;
|
||||
using CRD.Views;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
|
||||
|
|
@ -265,7 +266,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void ApplyFilter(){
|
||||
public void ApplyFilter(){
|
||||
List<HistorySeries> filteredItems;
|
||||
|
||||
switch (currentFilterType){
|
||||
|
|
@ -282,13 +283,20 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) &&
|
||||
historySeries.Seasons.Any(season =>
|
||||
season.EpisodesList.Any(historyEpisode =>
|
||||
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile)))
|
||||
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile &&
|
||||
(!CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored || historyEpisode.SonarrIsMonitored))))
|
||||
.ToList();
|
||||
break;
|
||||
|
||||
case FilterType.ContinuingOnly:
|
||||
filteredItems = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
|
||||
break;
|
||||
case FilterType.Active:
|
||||
filteredItems = Items.Where(item => !item.IsInactive).ToList();
|
||||
break;
|
||||
case FilterType.Inactive:
|
||||
filteredItems = Items.Where(item => item.IsInactive).ToList();
|
||||
break;
|
||||
|
||||
default:
|
||||
filteredItems = new List<HistorySeries>();
|
||||
|
|
@ -532,6 +540,20 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
seriesArgs.Series?.UpdateNewEpisodes();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task UpdateData(SeasonDialogArgs seriesArgs){
|
||||
if (seriesArgs.Series != null){
|
||||
var result = await seriesArgs.Series.FetchData(seriesArgs.Season?.SeasonId);
|
||||
|
||||
MessageBus.Current.SendMessage(result
|
||||
? new ToastMessage(string.IsNullOrEmpty(seriesArgs.Season?.SeasonId) ? $"Series Refreshed" : $"Season Refreshed", ToastType.Information, 2)
|
||||
: new ToastMessage(string.IsNullOrEmpty(seriesArgs.Season?.SeasonId) ? $"Series Refresh Failed" : $"Season Refreshed Failed", ToastType.Error, 2));
|
||||
} else{
|
||||
MessageBus.Current.SendMessage(new ToastMessage(string.IsNullOrEmpty(seriesArgs.Season?.SeasonId) ? $"Refresh Failed" : $"Season Refresh Failed", ToastType.Error, 2));
|
||||
Console.Error.WriteLine("Failed to get Series Data from View Tree - report issue");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenFolderPath(HistorySeries? series){
|
||||
try{
|
||||
|
|
@ -544,6 +566,11 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void ToggleInactive(){
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
}
|
||||
|
||||
public class HistoryPageProperties{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
public static bool _sonarrAvailable;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
public static bool _showMonitoredBookmark;
|
||||
|
||||
|
|
@ -38,12 +38,6 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
|
||||
private IStorageProvider? _storageProvider;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _availableDubs;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _availableSubs;
|
||||
|
||||
public SeriesPageViewModel(){
|
||||
_storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider));
|
||||
|
||||
|
|
@ -58,25 +52,20 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
|
||||
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){
|
||||
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected;
|
||||
|
||||
|
||||
if (SonarrAvailable){
|
||||
ShowMonitoredBookmark = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored;
|
||||
}
|
||||
|
||||
} else{
|
||||
SonarrAvailable = false;
|
||||
}
|
||||
} else{
|
||||
SonarrConnected = SonarrAvailable = false;
|
||||
}
|
||||
|
||||
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
|
||||
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
|
||||
|
||||
SelectedSeries.UpdateSeriesFolderPath();
|
||||
}
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenFolderDialogAsync(HistorySeason? season){
|
||||
|
|
@ -188,13 +177,14 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
|
||||
[RelayCommand]
|
||||
public async Task UpdateData(string? season){
|
||||
await SelectedSeries.FetchData(season);
|
||||
var result = await SelectedSeries.FetchData(season);
|
||||
|
||||
MessageBus.Current.SendMessage(result
|
||||
? new ToastMessage(string.IsNullOrEmpty(season) ? $"Series Refreshed" : $"Season Refreshed", ToastType.Information, 2)
|
||||
: new ToastMessage(string.IsNullOrEmpty(season) ? $"Series Refresh Failed" : $"Season Refresh Failed", ToastType.Error, 2));
|
||||
|
||||
SelectedSeries.Seasons.Refresh();
|
||||
|
||||
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
|
||||
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
|
||||
|
||||
// MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +200,11 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public void ToggleInactive(){
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void NavBack(){
|
||||
SelectedSeries.UpdateNewEpisodes();
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private double? _downloadSpeed;
|
||||
|
||||
[ObservableProperty]
|
||||
private double? _retryAttempts;
|
||||
|
||||
[ObservableProperty]
|
||||
private double? _retryDelay;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedHistoryLang;
|
||||
|
|
@ -258,6 +264,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
HistorySkipUnmonitored = options.HistorySkipUnmonitored;
|
||||
HistoryCountSonarr = options.HistoryCountSonarr;
|
||||
DownloadSpeed = options.DownloadSpeedLimit;
|
||||
RetryAttempts = Math.Clamp((options.RetryAttempts), 1, 10);
|
||||
RetryDelay = Math.Clamp((options.RetryDelay), 1, 30);
|
||||
DownloadToTempFolder = options.DownloadToTempFolder;
|
||||
SimultaneousDownloads = options.SimultaneousDownloads;
|
||||
LogMode = options.LogMode;
|
||||
|
|
@ -283,6 +291,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1);
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.RetryAttempts = Math.Clamp((int)(RetryAttempts ?? 0), 1, 10);
|
||||
CrunchyrollManager.Instance.CrunOptions.RetryDelay = Math.Clamp((int)(RetryDelay ?? 0), 1, 30);
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder;
|
||||
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
<Popup IsLightDismissEnabled="True"
|
||||
MaxWidth="{Binding Bounds.Width, ElementName=SearchBar}"
|
||||
MaxHeight="{Binding Bounds.Height, ElementName=Grid}"
|
||||
Width="{Binding Bounds.Width, ElementName=SearchBar}"
|
||||
IsOpen="{Binding SearchPopupVisible}"
|
||||
Placement="Bottom"
|
||||
PlacementTarget="{Binding ElementName=SearchBar}"
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
|
||||
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
|
||||
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
|
||||
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter"/>
|
||||
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
|
||||
<Grid>
|
||||
|
||||
|
|
@ -402,19 +402,41 @@
|
|||
|
||||
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock>
|
||||
<TextBlock Grid.Row="1" FontSize="15" Margin="0 0 0 5" TextWrapping="Wrap"
|
||||
Text="{Binding SeriesDescription}">
|
||||
Text="{Binding SeriesDescription}" MinWidth="200">
|
||||
</TextBlock>
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" Text="Available Dubs: " />
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListToStringConverter}}"></TextBlock>
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Dubs: "></TextBlock>
|
||||
|
||||
<ui:HighlightingTextBlock
|
||||
Items="{Binding HistorySeriesAvailableDubLang}"
|
||||
Series="{Binding .}"
|
||||
Season=""
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="True"
|
||||
FontSize="15"
|
||||
Opacity="0.8"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" Text="Available Subs: " />
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListToStringConverter}}"></TextBlock>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Subs: "></TextBlock>
|
||||
|
||||
<ui:HighlightingTextBlock
|
||||
Items="{Binding HistorySeriesAvailableSoftSubs}"
|
||||
Series="{Binding .}"
|
||||
Season=""
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="False"
|
||||
FontSize="15"
|
||||
Opacity="0.8"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
|
||||
|
||||
<StackPanel Grid.Row="5" Orientation="Vertical">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
|
||||
|
|
@ -443,7 +465,7 @@
|
|||
Height="30" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
|
||||
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
|
||||
BorderThickness="0" CornerRadius="50"
|
||||
IsVisible="{Binding SeriesFolderPathExists}"
|
||||
|
|
@ -462,13 +484,20 @@
|
|||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Command="{Binding FetchData}" Margin="0 0 5 10">Refresh Series</Button>
|
||||
<Button Content="Refresh Series"
|
||||
Margin="0 0 5 10"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).UpdateData}">
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding />
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
</Button>
|
||||
<ToggleButton x:Name="SeriesEditModeToggle" IsChecked="{Binding EditModeEnabled}" Margin="0 0 5 10">Edit</ToggleButton>
|
||||
|
||||
<Button Margin="0 0 5 10" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}"
|
||||
>
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}">
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding />
|
||||
|
|
@ -483,6 +512,25 @@
|
|||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding IsInactive}"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleInactive}">
|
||||
<ToolTip.Tip>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Set Active"
|
||||
IsVisible="{Binding IsInactive}"
|
||||
FontSize="15" />
|
||||
<TextBlock Text="Set Inactive"
|
||||
IsVisible="{Binding !IsInactive}"
|
||||
FontSize="15" />
|
||||
</StackPanel>
|
||||
</ToolTip.Tip>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<controls:SymbolIcon Symbol="CloudOffline" FontSize="18" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
|
||||
<StackPanel>
|
||||
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
|
|
@ -671,22 +719,45 @@
|
|||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="E"></TextBlock>
|
||||
<TextBlock Text="{Binding Episode}"></TextBlock>
|
||||
<TextBlock Text=" - "></TextBlock>
|
||||
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<ui:EpisodeHighlightTextBlock
|
||||
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
|
||||
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
||||
Episode="{Binding .}"
|
||||
StreamingService="Crunchyroll"
|
||||
MinWidth="50"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8" Text="Dubs: ">
|
||||
</TextBlock>
|
||||
|
||||
<ui:HighlightingTextBlock
|
||||
Items="{Binding HistoryEpisodeAvailableDubLang}"
|
||||
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
|
||||
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="True"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
FontStyle="Italic" />
|
||||
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
|
||||
|
||||
<StackPanel VerticalAlignment="Center"
|
||||
Margin="0 0 5 0"
|
||||
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).SonarrSeriesId, Converter={StaticResource UiSonarrIdToVisibilityConverter}}">
|
||||
|
||||
|
||||
<controls:ImageIcon
|
||||
IsVisible="{Binding SonarrHasFile}"
|
||||
Source="../Assets/sonarr.png"
|
||||
|
|
@ -740,7 +811,7 @@
|
|||
FontSize="18" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
|
||||
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
|
||||
|
|
@ -768,8 +839,13 @@
|
|||
|
||||
<Button Margin="10 0 0 0" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).FetchData}"
|
||||
CommandParameter="{Binding SeasonId}">
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).UpdateData}">
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
|
||||
<Binding Path="SeasonId" />
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Refresh Season" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
|
|
@ -825,7 +901,7 @@
|
|||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleDownloadedMark}">
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding Path="DataContext" ElementName="TableViewScrollViewer" />
|
||||
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
|
||||
<Binding />
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
|
|
@ -835,11 +911,10 @@
|
|||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<Button Margin="10 0 0 0" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}"
|
||||
>
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}">
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ namespace CRD.Views;
|
|||
public partial class MainWindow : AppWindow{
|
||||
private Stack<object> navigationStack = new Stack<object>();
|
||||
|
||||
private static HashSet<string> activeErrors = new HashSet<string>();
|
||||
|
||||
#region Singelton
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ public partial class MainWindow : AppWindow{
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private object selectedNavVieItem;
|
||||
|
||||
private const int TitleBarHeightAdjustment = 31;
|
||||
|
|
@ -55,12 +56,12 @@ public partial class MainWindow : AppWindow{
|
|||
ProgramManager.Instance.StorageProvider = StorageProvider;
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
InitializeComponent();
|
||||
|
||||
|
||||
ExtendClientAreaTitleBarHeightHint = TitleBarHeightAdjustment;
|
||||
TitleBar.Height = TitleBarHeightAdjustment;
|
||||
TitleBar.ExtendsContentIntoTitleBar = true;
|
||||
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
|
||||
|
||||
|
||||
Opened += OnOpened;
|
||||
Closing += OnClosing;
|
||||
|
||||
|
|
@ -72,36 +73,59 @@ public partial class MainWindow : AppWindow{
|
|||
|
||||
//select first element as default
|
||||
var nv = this.FindControl<NavigationView>("NavView");
|
||||
nv.SelectedItem = nv.MenuItems.ElementAt(0);
|
||||
nv.SelectedItem = nv.MenuItems.ElementAt(0);
|
||||
selectedNavVieItem = nv.SelectedItem;
|
||||
|
||||
MessageBus.Current.Listen<NavigationMessage>()
|
||||
.Subscribe(message => {
|
||||
if (message.Refresh){
|
||||
navigationStack.Pop();
|
||||
var viewModel = Activator.CreateInstance(message.ViewModelType);
|
||||
|
||||
if (navigationStack.Count > 0){
|
||||
navigationStack.Pop();
|
||||
}
|
||||
|
||||
navigationStack.Push(viewModel);
|
||||
nv.Content = viewModel;
|
||||
} else if (!message.Back && message.ViewModelType != null){
|
||||
var viewModel = Activator.CreateInstance(message.ViewModelType);
|
||||
|
||||
|
||||
navigationStack.Push(viewModel);
|
||||
nv.Content = viewModel;
|
||||
try{
|
||||
var viewModel = Activator.CreateInstance(message.ViewModelType);
|
||||
navigationStack.Push(viewModel);
|
||||
nv.Content = viewModel;
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"Failed to create or push viewModel: {ex.Message}");
|
||||
}
|
||||
} else if (message is{ Back: false, ViewModelType: not null }){
|
||||
try{
|
||||
var viewModel = Activator.CreateInstance(message.ViewModelType);
|
||||
navigationStack.Push(viewModel);
|
||||
nv.Content = viewModel;
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"Failed to create or push viewModel: {ex.Message}");
|
||||
}
|
||||
} else{
|
||||
navigationStack.Pop();
|
||||
var viewModel = navigationStack.Peek();
|
||||
nv.Content = viewModel;
|
||||
if (navigationStack.Count > 0){
|
||||
navigationStack.Pop();
|
||||
}
|
||||
|
||||
if (navigationStack.Count > 0){
|
||||
var viewModel = navigationStack.Peek();
|
||||
if (viewModel is HistoryPageViewModel historyView){
|
||||
historyView.ApplyFilter();
|
||||
}
|
||||
|
||||
nv.Content = viewModel;
|
||||
} else{
|
||||
Console.Error.WriteLine("Navigation stack is empty. Cannot peek.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MessageBus.Current.Listen<ToastMessage>()
|
||||
.Subscribe(message => ShowToast(message.Message, message.Type, message.Seconds));
|
||||
.Subscribe(message => ShowToast(message.Message ?? string.Empty, message.Type, message.Seconds));
|
||||
}
|
||||
|
||||
public async void ShowError(string message,bool githubWikiButton = false){
|
||||
public async void ShowError(string message, bool githubWikiButton = false){
|
||||
if (activeErrors.Contains(message))
|
||||
return;
|
||||
|
||||
activeErrors.Add(message);
|
||||
|
||||
var dialog = new ContentDialog(){
|
||||
Title = "Error",
|
||||
Content = message,
|
||||
|
|
@ -109,19 +133,22 @@ public partial class MainWindow : AppWindow{
|
|||
};
|
||||
|
||||
if (githubWikiButton){
|
||||
dialog.PrimaryButtonText = "Github Wiki";
|
||||
dialog.PrimaryButtonText = "Github Wiki";
|
||||
}
|
||||
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary){
|
||||
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
|
||||
}
|
||||
|
||||
activeErrors.Remove(message);
|
||||
}
|
||||
|
||||
|
||||
public void ShowToast(string message, ToastType type, int durationInSeconds = 5){
|
||||
this.FindControl<ToastNotification>("Toast").Show(message, type, durationInSeconds);
|
||||
var toastControl = this.FindControl<ToastNotification>("Toast");
|
||||
toastControl?.Show(message, type, durationInSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -172,7 +199,7 @@ public partial class MainWindow : AppWindow{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnOpened(object sender, EventArgs e){
|
||||
if (File.Exists(CfgManager.PathWindowSettings)){
|
||||
var settings = JsonConvert.DeserializeObject<WindowSettings>(File.ReadAllText(CfgManager.PathWindowSettings));
|
||||
|
|
@ -193,7 +220,7 @@ public partial class MainWindow : AppWindow{
|
|||
_restorePosition = new PixelPoint(settings.PosX, settings.PosY);
|
||||
|
||||
// Ensure the window is on the correct screen before maximizing
|
||||
Position = new PixelPoint(settings.PosX, settings.PosY );
|
||||
Position = new PixelPoint(settings.PosX, settings.PosY);
|
||||
}
|
||||
|
||||
if (settings.IsMaximized){
|
||||
|
|
@ -242,7 +269,7 @@ public partial class MainWindow : AppWindow{
|
|||
private void OnPositionChanged(object sender, PixelPointEventArgs e){
|
||||
if (WindowState == WindowState.Normal){
|
||||
var screens = Screens.All;
|
||||
|
||||
|
||||
bool isWithinAnyScreen = screens.Any(screen =>
|
||||
e.Point.X >= screen.WorkingArea.X &&
|
||||
e.Point.X <= screen.WorkingArea.X + screen.WorkingArea.Width &&
|
||||
|
|
|
|||
|
|
@ -57,9 +57,42 @@
|
|||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" FontSize="45" Text="{Binding SelectedSeries.SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock>
|
||||
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock>
|
||||
<TextBlock Grid.Row="3" IsVisible="{Binding SelectedSeries.HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableDubs}"></TextBlock>
|
||||
<TextBlock Grid.Row="4" IsVisible="{Binding SelectedSeries.HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableSubs}"></TextBlock>
|
||||
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}" MinWidth="250"></TextBlock>
|
||||
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding SelectedSeries.HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Dubs: "></TextBlock>
|
||||
|
||||
<ui:HighlightingTextBlock
|
||||
Items="{Binding SelectedSeries.HistorySeriesAvailableDubLang}"
|
||||
Series="{Binding SelectedSeries}"
|
||||
Season=""
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="True"
|
||||
FontSize="15"
|
||||
Opacity="0.8"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding SelectedSeries.HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Subs: "></TextBlock>
|
||||
|
||||
<ui:HighlightingTextBlock
|
||||
Items="{Binding SelectedSeries.HistorySeriesAvailableSoftSubs}"
|
||||
Series="{Binding SelectedSeries}"
|
||||
Season=""
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="False"
|
||||
FontSize="15"
|
||||
Opacity="0.8"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="5" Orientation="Vertical">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
|
||||
|
|
@ -117,6 +150,24 @@
|
|||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding SelectedSeries.IsInactive}" Command="{Binding ToggleInactive}">
|
||||
<ToolTip.Tip>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Set Active"
|
||||
IsVisible="{Binding SelectedSeries.IsInactive}"
|
||||
FontSize="15" />
|
||||
<TextBlock Text="Set Inactive"
|
||||
IsVisible="{Binding !SelectedSeries.IsInactive}"
|
||||
FontSize="15" />
|
||||
</StackPanel>
|
||||
</ToolTip.Tip>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<controls:SymbolIcon Symbol="CloudOffline" FontSize="18" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
|
||||
<StackPanel>
|
||||
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
|
|
@ -184,7 +235,7 @@
|
|||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10" >
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
|
||||
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
|
||||
<StackPanel>
|
||||
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
|
||||
|
|
@ -310,26 +361,53 @@
|
|||
<Grid VerticalAlignment="Center">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="E"></TextBlock>
|
||||
<TextBlock Text="{Binding Episode}"></TextBlock>
|
||||
<TextBlock Text=" - "></TextBlock>
|
||||
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
|
||||
</StackPanel>
|
||||
<controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
|
||||
Margin="0 0 5 0 "
|
||||
Symbol="AlertOn"
|
||||
FontSize="18"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Episode unavailable — it might not be available on the streaming service" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
|
||||
</controls:SymbolIcon>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||
|
||||
<ui:EpisodeHighlightTextBlock
|
||||
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
|
||||
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
||||
Episode="{Binding }"
|
||||
StreamingService="Crunchyroll"
|
||||
MinWidth="50"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8" Text="Dubs: ">
|
||||
</TextBlock>
|
||||
<TextBlock FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8" Text="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListToStringConverter}}" />
|
||||
|
||||
<ui:HighlightingTextBlock
|
||||
Items="{Binding HistoryEpisodeAvailableDubLang}"
|
||||
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
|
||||
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="True"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
FontStyle="Italic" />
|
||||
|
||||
|
||||
</StackPanel>
|
||||
<!-- <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> -->
|
||||
<!-- <TextBlock FontStyle="Italic" -->
|
||||
|
|
@ -343,7 +421,9 @@
|
|||
</StackPanel>
|
||||
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
|
||||
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
|
||||
|
||||
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
|
||||
|
|
|
|||
|
|
@ -14,30 +14,36 @@ public partial class ToastNotification : UserControl{
|
|||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private DispatcherTimer? currentTimer;
|
||||
|
||||
public void Show(string message, ToastType type, int durationInSeconds){
|
||||
this.FindControl<TextBlock>("MessageText").Text = message;
|
||||
var text = this.FindControl<TextBlock>("MessageText");
|
||||
if (text != null) text.Text = message;
|
||||
SetStyle(type);
|
||||
DispatcherTimer timer = new DispatcherTimer{ Interval = TimeSpan.FromSeconds(durationInSeconds) };
|
||||
timer.Tick += (sender, args) => {
|
||||
timer.Stop();
|
||||
this.IsVisible = false;
|
||||
|
||||
currentTimer?.Stop();
|
||||
|
||||
currentTimer = new DispatcherTimer{ Interval = TimeSpan.FromSeconds(durationInSeconds) };
|
||||
currentTimer.Tick += (sender, args) => {
|
||||
currentTimer?.Stop();
|
||||
IsVisible = false;
|
||||
};
|
||||
timer.Start();
|
||||
this.IsVisible = true;
|
||||
currentTimer.Start();
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
private void SetStyle(ToastType type){
|
||||
var border = this.FindControl<Border>("MessageBorder");
|
||||
border.Classes.Clear(); // Clear previous styles
|
||||
border?.Classes.Clear(); // Clear previous styles
|
||||
switch (type){
|
||||
case ToastType.Information:
|
||||
border.Classes.Add("info");
|
||||
border?.Classes.Add("info");
|
||||
break;
|
||||
case ToastType.Error:
|
||||
border.Classes.Add("error");
|
||||
border?.Classes.Add("error");
|
||||
break;
|
||||
case ToastType.Warning:
|
||||
border.Classes.Add("warning");
|
||||
border?.Classes.Add("warning");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,10 +92,14 @@
|
|||
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
|
||||
CornerRadius="10" Margin="2">
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
|
||||
<TextBlock Text="{Binding stringValue}" Margin="5,0" TextTrimming="CharacterEllipsis" MaxWidth="300" TextWrapping="NoWrap">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="{Binding stringValue}" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
</TextBlock>
|
||||
<Button Content="X" FontSize="8" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
||||
Width="15" Height="15" Padding="0"
|
||||
Width="15" Height="15" Padding="0"
|
||||
Command="{Binding $parent[ItemsControl].((utils:ContentDialogEncodingPresetViewModel)DataContext).RemoveAdditionalParam}"
|
||||
CommandParameter="{Binding .}" />
|
||||
</StackPanel>
|
||||
|
|
|
|||
|
|
@ -74,6 +74,30 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Download Retry"
|
||||
Description="Sett the number of retry attempts and the delay between each retry">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Width="100" Text="Retry Attempts" Margin="5 0" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
|
||||
<controls:NumberBox Minimum="1" Maximum="10"
|
||||
Value="{Binding RetryAttempts}"
|
||||
SpinButtonPlacementMode="Hidden"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0 5">
|
||||
<TextBlock Width="100" Text="Retry Delay (s)" Margin="5 0" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
|
||||
<controls:NumberBox Minimum="1" Maximum="30"
|
||||
Value="{Binding RetryDelay}"
|
||||
SpinButtonPlacementMode="Hidden"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Use Temp Download Folder">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
|
|
|||
Loading…
Reference in a new issue