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:
Elwador 2025-04-23 22:36:05 +02:00
parent 709647bb99
commit 4f6d0f2257
35 changed files with 1178 additions and 237 deletions

View file

@ -24,6 +24,8 @@ public partial class App : Application{
desktop.MainWindow = new MainWindow{ desktop.MainWindow = new MainWindow{
DataContext = new MainWindowViewModel(manager), DataContext = new MainWindowViewModel(manager),
}; };
desktop.MainWindow.Opened += (_, _) => { manager.SetBackgroundImage(); };
} }

View file

@ -114,16 +114,16 @@ public class CrAuth{
JsonTokenToFileAndVariable(response.ResponseContent, uuid); JsonTokenToFileAndVariable(response.ResponseContent, uuid);
} else{ } else{
if (response.ResponseContent.Contains("invalid_credentials")){ 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>") || } else if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
response.ResponseContent.Contains("<title>Access denied</title>") || response.ResponseContent.Contains("<title>Access denied</title>") ||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") || response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
response.ResponseContent.Trim().Equals("error code: 1020") || response.ResponseContent.Trim().Equals("error code: 1020") ||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){ 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{ } else{
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {response.ResponseContent.Substring(0, response.ResponseContent.Length < 200 ? response.ResponseContent.Length : 200)}", 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); await Console.Error.WriteLineAsync("Full Response: " + response.ResponseContent);
} }
} }
@ -231,6 +231,15 @@ public class CrAuth{
var response = await HttpClientReq.Instance.SendHttpRequest(request); 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){ if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent, uuid); JsonTokenToFileAndVariable(response.ResponseContent, uuid);
@ -242,6 +251,9 @@ public class CrAuth{
} else{ } else{
Console.Error.WriteLine("Token Auth Failed"); Console.Error.WriteLine("Token Auth Failed");
await AuthAnonymous(); await AuthAnonymous();
MainWindow.Instance.ShowError("Login failed. Please check the log for more details.");
} }
} }

View file

@ -40,18 +40,22 @@ public class CrMovies{
return null; return null;
} }
if (movie.Total == 1 && movie.Data != null){ if (movie is{ Total: 1, Data: not null }){
return movie.Data.First(); var movieRes = movie.Data.First();
return movieRes.type != "movie" ? null : movieRes;
} }
Console.Error.WriteLine("Multiple movie returned with one ID?"); 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; return null;
} }
public CrunchyEpMeta? EpisodeMeta(CrunchyMovie episodeP, List<string> dubLang){ public CrunchyEpMeta? EpisodeMeta(CrunchyMovie episodeP, List<string> dubLang){
if (!string.IsNullOrEmpty(episodeP.AudioLocale) && !dubLang.Contains(episodeP.AudioLocale)){ if (!string.IsNullOrEmpty(episodeP.AudioLocale) && !dubLang.Contains(episodeP.AudioLocale)){
Console.Error.WriteLine("Movie not available in the selected dub lang"); Console.Error.WriteLine("Movie not available in the selected dub lang");
return null; return null;
@ -78,7 +82,7 @@ public class CrMovies{
Time = 0, Time = 0,
DownloadSpeed = 0 DownloadSpeed = 0
}; };
epMeta.AvailableSubs = new List<string>(); epMeta.AvailableSubs = [];
epMeta.Description = episodeP.Description; epMeta.Description = episodeP.Description;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang; epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;

View file

@ -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 musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang);
var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/concerts", crLocale, forcedLang); var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/concerts", crLocale, forcedLang);

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -114,7 +115,8 @@ public class CrunchyrollManager{
options.QualityVideo = "best"; options.QualityVideo = "best";
options.CcTag = "CC"; options.CcTag = "CC";
options.CcSubsFont = "Trebuchet MS"; options.CcSubsFont = "Trebuchet MS";
options.FsRetryTime = 5; options.RetryDelay = 5;
options.RetryAttempts = 5;
options.Numbers = 2; options.Numbers = 2;
options.Timeout = 15000; options.Timeout = 15000;
options.DubLang = new List<string>(){ "ja-JP" }; options.DubLang = new List<string>(){ "ja-JP" };
@ -409,7 +411,7 @@ public class CrunchyrollManager{
CcSubsMuxingFlag = options.CcSubsMuxingFlag, CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced, SignsSubsAsForced = options.SignsSubsAsForced,
}, },
fileNameAndPath); fileNameAndPath, data);
if (result is{ merger: not null, isMuxed: true }){ if (result is{ merger: not null, isMuxed: true }){
mergers.Add(result.merger); mergers.Add(result.merger);
@ -474,7 +476,7 @@ public class CrunchyrollManager{
CcSubsMuxingFlag = options.CcSubsMuxingFlag, CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced, SignsSubsAsForced = options.SignsSubsAsForced,
}, },
fileNameAndPath); fileNameAndPath, data);
syncError = result.syncError; syncError = result.syncError;
muxError = !result.isMuxed; muxError = !result.isMuxed;
@ -646,7 +648,7 @@ public class CrunchyrollManager{
#endregion #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; var muxToMp3 = false;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
@ -733,6 +735,16 @@ public class CrunchyrollManager{
bool isMuxed, syncError = false; bool isMuxed, syncError = false;
if (options is{ SyncTiming: true, DlVideoOnce: true }){ 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 basePath = merger.options.OnlyVid.First().Path;
var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList(); var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList();
@ -764,6 +776,16 @@ public class CrunchyrollManager{
syncVideosList.ForEach(syncVideo => { syncVideosList.ForEach(syncVideo => {
if (syncVideo.Path != null) Helpers.DeleteFile(syncVideo.Path); 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){ if (!options.Mp4 && !muxToMp3){
@ -777,12 +799,12 @@ public class CrunchyrollManager{
private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){ private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){
if (Profile.Username == "???"){ 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{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
FileName = "./unknown", 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()); 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; int maxLength = 220;
if (onlyFileName.Length > maxLength){ if (onlyFileName.Length > maxLength){
@ -1361,7 +1383,7 @@ public class CrunchyrollManager{
if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){ if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){
titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(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()); 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){ if (onlyFileName.Length > maxLength){
fileName = Helpers.LimitFileNameLength(fileName, maxLength); fileName = Helpers.LimitFileNameLength(fileName, maxLength);
@ -1372,7 +1394,7 @@ public class CrunchyrollManager{
fileName = Helpers.LimitFileNameLength(fileName, maxLength); 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()); //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{ try{
// Parsing and constructing the file names // Parsing and constructing the file names
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); 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)){ if (Path.IsPathRooted(outFile)){
tsFile = outFile; tsFile = outFile;
} else{ } else{
@ -1712,14 +1734,14 @@ public class CrunchyrollManager{
} }
// Check if the path is absolute // 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) // 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 // Initialize the cumulative path based on whether the original path is absolute or not
string cumulativePath = isAbsolute ? "" : fileDir; var cumulativePath = isAbsolute ? "" : fileDir;
for (int i = 0; i < directories.Length; i++){ for (var i = 0; i < directories.Length; i++){
// Build the path incrementally // Build the path incrementally
cumulativePath = Path.Combine(cumulativePath, directories[i]); cumulativePath = Path.Combine(cumulativePath, directories[i]);
@ -2028,7 +2050,8 @@ public class CrunchyrollManager{
M3U8Json = videoJson, M3U8Json = videoJson,
// BaseUrl = chunkPlaylist.BaseUrl, // BaseUrl = chunkPlaylist.BaseUrl,
Threads = options.Partsize, Threads = options.Partsize,
FsRetryTime = options.FsRetryTime * 1000, FsRetryTime = options.RetryDelay * 1000,
Retries = options.RetryAttempts,
Override = options.Force, Override = options.Force,
}, data, true, false); }, data, true, false);
@ -2085,7 +2108,8 @@ public class CrunchyrollManager{
M3U8Json = audioJson, M3U8Json = audioJson,
// BaseUrl = chunkPlaylist.BaseUrl, // BaseUrl = chunkPlaylist.BaseUrl,
Threads = options.Partsize, Threads = options.Partsize,
FsRetryTime = options.FsRetryTime * 1000, FsRetryTime = options.RetryDelay * 1000,
Retries = options.RetryAttempts,
Override = options.Force, Override = options.Force,
}, data, false, true); }, data, false, true);

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -20,6 +21,7 @@ using CRD.Utils.Structs.History;
using CRD.ViewModels; using CRD.ViewModels;
using CRD.ViewModels.Utils; using CRD.ViewModels.Utils;
using CRD.Views.Utils; using CRD.Views.Utils;
using DynamicData;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -209,6 +211,11 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "tv/samsung" } new(){ Content = "tv/samsung" }
]; ];
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
[ObservableProperty]
private StringItemWithDisplayName _selectedFFmpegHWAccel;
[ObservableProperty] [ObservableProperty]
private bool _isEncodeEnabled; 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; ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0]; 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(); var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
SelectedSubLang.Clear(); SelectedSubLang.Clear();
@ -391,6 +404,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection(); CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
CrunchyrollManager.Instance.CrunOptions.FfmpegHwAccelFlag = SelectedFFmpegHWAccel.value;
List<string> softSubs = new List<string>(); List<string> softSubs = new List<string>();
foreach (var listBoxItem in SelectedSubLang){ foreach (var listBoxItem in SelectedSubLang){
softSubs.Add(listBoxItem.Content + ""); softSubs.Add(listBoxItem.Content + "");
@ -568,4 +583,61 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0]; 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;
}
} }

View file

@ -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 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> <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.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
@ -438,7 +454,11 @@
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1" <Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2"> CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5"> <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" <Button Content="X" FontSize="10" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="15" Height="15" Padding="0" HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveMkvMergeParam}" Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveMkvMergeParam}"
@ -476,7 +496,11 @@
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1" <Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2"> CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5"> <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" <Button Content="X" FontSize="10" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="15" Height="15" Padding="0" HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveFfmpegParam}" Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveFfmpegParam}"

View file

@ -20,9 +20,30 @@ namespace CRD.Downloader;
public class History{ public class History{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; 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); 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); CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true);
if (parsedSeries == null){ if (parsedSeries == null){
@ -31,6 +52,7 @@ public class History{
} }
if (parsedSeries.Data != null){ if (parsedSeries.Data != null){
var result = false;
foreach (var s in parsedSeries.Data){ foreach (var s in parsedSeries.Data){
var sId = s.Id; var sId = s.Id;
if (s.Versions is{ Count: > 0 }){ 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); 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>());
}
} }
historySeries ??= crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){ if (historySeries != null){
MatchHistorySeriesWithSonarr(false); MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries); await MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
return true; return result;
} }
} }
@ -115,7 +139,6 @@ public class History{
historySeries.SeriesStreamingService = StreamingService.Crunchyroll; historySeries.SeriesStreamingService = StreamingService.Crunchyroll;
await RefreshSeriesData(seriesId, historySeries); await RefreshSeriesData(seriesId, historySeries);
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.GetSeasonId()); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.GetSeasonId());
if (historySeason != null){ if (historySeason != null){
@ -140,7 +163,8 @@ public class History{
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(), HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(), HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType() EpisodeType = historySource.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true,
}; };
historySeason.EpisodesList.Add(newHistoryEpisode); historySeason.EpisodesList.Add(newHistoryEpisode);
@ -154,6 +178,7 @@ public class History{
historyEpisode.EpisodeSeasonNum = historySource.GetSeasonNum(); historyEpisode.EpisodeSeasonNum = historySource.GetSeasonNum();
historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate(); historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate();
historyEpisode.EpisodeType = historySource.GetEpisodeType(); historyEpisode.EpisodeType = historySource.GetEpisodeType();
historyEpisode.IsEpisodeAvailableOnStreamingService = true;
historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(); historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang();
historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(); 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){ public HistoryEpisode? GetHistoryEpisode(string? seriesId, string? seasonId, string episodeId){
@ -277,11 +302,11 @@ public class History{
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesDubLangOverride.Count > 0){ if (historySeries.HistorySeriesDubLangOverride.Count > 0){
dublist = historySeries.HistorySeriesDubLangOverride; dublist = historySeries.HistorySeriesDubLangOverride.ToList();
} }
if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){ if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){
sublist = historySeries.HistorySeriesSoftSubsOverride; sublist = historySeries.HistorySeriesSoftSubsOverride.ToList();
} }
if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){ if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){
@ -295,11 +320,11 @@ public class History{
if (historySeason != null){ if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historySeason.HistorySeasonDubLangOverride.Count > 0){ if (historySeason.HistorySeasonDubLangOverride.Count > 0){
dublist = historySeason.HistorySeasonDubLangOverride; dublist = historySeason.HistorySeasonDubLangOverride.ToList();
} }
if (historySeason.HistorySeasonSoftSubsOverride.Count > 0){ if (historySeason.HistorySeasonSoftSubsOverride.Count > 0){
sublist = historySeason.HistorySeasonSoftSubsOverride; sublist = historySeason.HistorySeasonSoftSubsOverride.ToList();
} }
if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){ if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){
@ -327,11 +352,11 @@ public class History{
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesDubLangOverride.Count > 0){ if (historySeries.HistorySeriesDubLangOverride.Count > 0){
dublist = historySeries.HistorySeriesDubLangOverride; dublist = historySeries.HistorySeriesDubLangOverride.ToList();
} }
if (historySeason is{ HistorySeasonDubLangOverride.Count: > 0 }){ if (historySeason is{ HistorySeasonDubLangOverride.Count: > 0 }){
dublist = historySeason.HistorySeasonDubLangOverride; dublist = historySeason.HistorySeasonDubLangOverride.ToList();
} }
} }
@ -347,7 +372,7 @@ public class History{
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){ if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){
sublist = historySeries.HistorySeriesSoftSubsOverride; sublist = historySeries.HistorySeriesSoftSubsOverride.ToList();
} }
if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){ if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){
@ -355,7 +380,7 @@ public class History{
} }
if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){ if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){
sublist = historySeason.HistorySeasonSoftSubsOverride; sublist = historySeason.HistorySeasonSoftSubsOverride.ToList();
} }
if (historySeason != null && !string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){ if (historySeason != null && !string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){
@ -551,7 +576,8 @@ public class History{
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(), HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(), HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType() EpisodeType = historySource.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true
}; };
newSeason.EpisodesList.Add(newHistoryEpisode); newSeason.EpisodesList.Add(newHistoryEpisode);
@ -566,13 +592,22 @@ public class History{
} }
foreach (var historySeries in crunInstance.HistoryList){ foreach (var historySeries in crunInstance.HistoryList){
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ if (string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty); var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty);
if (sonarrSeries != null){ if (sonarrSeries != null){
historySeries.SonarrSeriesId = sonarrSeries.Id + ""; historySeries.SonarrSeriesId = sonarrSeries.Id + "";
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + ""; historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug; 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}");
}
} }
} }
} }

View file

@ -13,7 +13,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Utils.Updater; using CRD.Utils.Updater;
using ExtendedXmlSerializer.Core.Sources;
using FluentAvalonia.Styling; using FluentAvalonia.Styling;
namespace CRD.Downloader; namespace CRD.Downloader;
@ -66,22 +68,42 @@ public partial class ProgramManager : ObservableObject{
private readonly FluentAvaloniaTheme? _faTheme; 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; private bool exitOnTaskFinish;
#endregion
public IStorageProvider StorageProvider; public IStorageProvider StorageProvider;
public ProgramManager(){ public ProgramManager(){
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme; _faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme;
foreach (var arg in Environment.GetCommandLineArgs()){ foreach (var arg in Environment.GetCommandLineArgs()){
if (arg == "--historyRefreshAll"){ switch (arg){
taskQueue.Enqueue(RefreshAll); case "--historyRefreshAll":
} else if (arg == "--historyAddToQueue"){ 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); taskQueue.Enqueue(AddMissingToQueue);
} else if (arg == "--exit"){ break;
case "--exit":
exitOnTaskFinish = true; exitOnTaskFinish = true;
break;
} }
} }
@ -90,16 +112,54 @@ public partial class ProgramManager : ObservableObject{
CleanUpOldUpdater(); CleanUpOldUpdater();
} }
private async Task RefreshAll(){ private async Task RefreshHistory(FilterType filterType){
FetchingData = true; 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(); item.SetFetchingData();
} }
for (int i = 0; i < CrunchyrollManager.Instance.HistoryList.Count; i++){ for (int i = 0; i < filteredItems.Count; i++){
await CrunchyrollManager.Instance.HistoryList[i].FetchData(""); await filteredItems[i].FetchData("");
CrunchyrollManager.Instance.HistoryList[i].UpdateNewEpisodes(); filteredItems[i].UpdateNewEpisodes();
} }
FetchingData = false; FetchingData = false;
@ -115,10 +175,16 @@ public partial class ProgramManager : ObservableObject{
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress.Done != true)){ while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress.Done != true)){
Console.WriteLine("Waiting for downloads to complete..."); 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(){ private async Task Init(){
CrunchyrollManager.Instance.InitOptions(); CrunchyrollManager.Instance.InitOptions();
@ -143,11 +209,6 @@ public partial class ProgramManager : ObservableObject{
} }
} }
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(); await CrunchyrollManager.Instance.Init();
FinishedLoading = true; FinishedLoading = true;

View file

@ -92,7 +92,6 @@ public partial class QueueManager : ObservableObject{
} }
HasFailedItem = Queue.Any(item => item.DownloadProgress.Error); HasFailedItem = Queue.Any(item => item.DownloadProgress.Error);
} }
@ -190,6 +189,7 @@ public partial class QueueManager : ObservableObject{
Queue.Add(selected); Queue.Add(selected);
if (selected.Data.Count < dubLang.Count && !CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub){ if (selected.Data.Count < dubLang.Count && !CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub){
Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs"); 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: "); Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: ");
@ -214,7 +214,10 @@ public partial class QueueManager : ObservableObject{
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]"); 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)); MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
} }
} else{
return;
}
Console.WriteLine("Couldn't find episode trying to find movie with id"); Console.WriteLine("Couldn't find episode trying to find movie with id");
var movie = await CrunchyrollManager.Instance.CrMovies.ParseMovieById(epId, crLocale); var movie = await CrunchyrollManager.Instance.CrMovies.ParseMovieById(epId, crLocale);
@ -243,9 +246,12 @@ public partial class QueueManager : ObservableObject{
Console.WriteLine("Added Movie to Queue"); Console.WriteLine("Added Movie to Queue");
MessageBus.Current.SendMessage(new ToastMessage($"Added Movie to Queue", ToastType.Information, 1)); 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));
} }

View file

@ -2,24 +2,37 @@
using System.Runtime.Serialization; using System.Runtime.Serialization;
using CRD.Utils.JsonConv; using CRD.Utils.JsonConv;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace CRD.Utils; namespace CRD.Utils;
[JsonConverter(typeof(StringEnumConverter))]
public enum StreamingService{ public enum StreamingService{
[EnumMember(Value = "Crunchyroll")]
Crunchyroll, Crunchyroll,
[EnumMember(Value = "Unknown")]
Unknown Unknown
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum EpisodeType{ public enum EpisodeType{
[EnumMember(Value = "MusicVideo")]
MusicVideo, MusicVideo,
[EnumMember(Value = "Concert")]
Concert, Concert,
[EnumMember(Value = "Episode")]
Episode, Episode,
[EnumMember(Value = "Unknown")]
Unknown Unknown
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum SeriesType{ public enum SeriesType{
[EnumMember(Value = "Artist")]
Artist, Artist,
[EnumMember(Value = "Series")]
Series, Series,
[EnumMember(Value = "Unknown")]
Unknown Unknown
} }
@ -171,17 +184,25 @@ public enum DownloadMediaType{
Description, Description,
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum ScaledBorderAndShadowSelection{ public enum ScaledBorderAndShadowSelection{
[EnumMember(Value = "Dont Add")]
DontAdd, DontAdd,
[EnumMember(Value = "ScaledBorderAndShadow Yes")]
ScaledBorderAndShadowYes, ScaledBorderAndShadowYes,
[EnumMember(Value = "ScaledBorderAndShadow No")]
ScaledBorderAndShadowNo, ScaledBorderAndShadowNo,
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum HistoryViewType{ public enum HistoryViewType{
[EnumMember(Value = "Posters")]
Posters, Posters,
[EnumMember(Value = "Table")]
Table, Table,
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum SortingType{ public enum SortingType{
[EnumMember(Value = "Series Title")] [EnumMember(Value = "Series Title")]
SeriesTitle, SeriesTitle,
@ -193,6 +214,7 @@ public enum SortingType{
HistorySeriesAddDate, HistorySeriesAddDate,
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum FilterType{ public enum FilterType{
[EnumMember(Value = "All")] [EnumMember(Value = "All")]
All, All,
@ -205,14 +227,27 @@ public enum FilterType{
[EnumMember(Value = "Continuing Only")] [EnumMember(Value = "Continuing Only")]
ContinuingOnly, ContinuingOnly,
[EnumMember(Value = "Active")]
Active,
[EnumMember(Value = "Inactive")]
Inactive,
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum CrunchyUrlType{ public enum CrunchyUrlType{
[EnumMember(Value = "Artist")]
Artist, Artist,
[EnumMember(Value = "MusicVideo")]
MusicVideo, MusicVideo,
[EnumMember(Value = "Concert")]
Concert, Concert,
[EnumMember(Value = "Episode")]
Episode, Episode,
[EnumMember(Value = "Series")]
Series, Series,
[EnumMember(Value = "Unknown")]
Unknown Unknown
} }

View file

@ -361,7 +361,7 @@ public class HlsDownloader{
throw new Exception("Failed to download key"); throw new Exception("Failed to download key");
_data.Keys[kUri] = rkey; _data.Keys[kUri] = rkey;
} catch (Exception ex){ } 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 // Set default user-agent if not provided
if (!request.Headers.Contains("User-Agent")){ 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); 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 = await HttpClientReq.Instance.GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
return await ReadContentAsByteArrayAsync(response.Content); return await ReadContentAsByteArrayAsync(response.Content);
} catch (HttpRequestException ex){ } catch (Exception ex) when (ex is HttpRequestException or IOException){
// Log retry attempts // Log retry attempts
string partType = isKey ? "Key" : "Part"; string partType = isKey ? "Key" : "Part";
int partIndx = partIndex + 1 + segOffset; int partIndx = partIndex + 1 + segOffset;
@ -455,6 +455,12 @@ public class HlsDownloader{
throw; // rethrow after last retry throw; // rethrow after last retry
await Task.Delay(_data.WaitTime); 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;
} }
} }
} }

View file

@ -406,6 +406,7 @@ public class Helpers{
// If something went wrong, delete the temporary output file // If something went wrong, delete the temporary output file
File.Delete(tempOutputFilePath); File.Delete(tempOutputFilePath);
Console.Error.WriteLine("FFmpeg processing failed."); Console.Error.WriteLine("FFmpeg processing failed.");
Console.Error.WriteLine($"Command: {ffmpegCommand}");
} }
return (IsOk: isSuccess, ErrorCode: process.ExitCode); return (IsOk: isSuccess, ErrorCode: process.ExitCode);
@ -774,7 +775,7 @@ public class Helpers{
AutoDownload = yaml.AutoDownload, AutoDownload = yaml.AutoDownload,
RemoveFinishedDownload = yaml.RemoveFinishedDownload, RemoveFinishedDownload = yaml.RemoveFinishedDownload,
Timeout = yaml.Timeout, Timeout = yaml.Timeout,
FsRetryTime = yaml.FsRetryTime, RetryDelay = yaml.FsRetryTime,
Force = yaml.Force, Force = yaml.Force,
SimultaneousDownloads = yaml.SimultaneousDownloads, SimultaneousDownloads = yaml.SimultaneousDownloads,
Theme = yaml.Theme, Theme = yaml.Theme,

View file

@ -271,5 +271,6 @@ public static class ApiUrls{
public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE="; 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";
} }

View file

@ -23,7 +23,7 @@ public class LocaleConverter : JsonConverter{
return locale; 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){ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer){

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -312,6 +313,7 @@ public class Merger{
return -100; return -100;
} }
// Load frames from start of the videos // Load frames from start of the videos
var baseFramesStart = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData{ var baseFramesStart = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData{
FilePath = fp, FilePath = fp,
@ -401,9 +403,10 @@ public class Merger{
var result = await Helpers.ExecuteCommandAsync(type, bin, command); var result = await Helpers.ExecuteCommandAsync(type, bin, command);
if (!result.IsOk && type == "mkvmerge" && result.ErrorCode == 1){ 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){ } else if (!result.IsOk){
Console.Error.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}"); Console.Error.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}");
Console.Error.WriteLine($"[{type}] Merging failed command: {command}");
return false; return false;
} else{ } else{
Console.WriteLine($"[{type} Done]"); Console.WriteLine($"[{type} Done]");

View file

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@ -17,7 +18,8 @@ namespace CRD.Utils.Muxing;
public class SyncingHelper{ public class SyncingHelper{
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){ public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
var ffmpegPath = CfgManager.PathFFMPEG; 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 = ""; var output = "";
@ -86,8 +88,8 @@ public class SyncingHelper{
var2 /= count - 1; var2 /= count - 1;
covariance /= count - 1; covariance /= count - 1;
double c1 = 0.01 * 0.01 * 255 * 255; double c1 = 0.01 * 0.01;
double c2 = 0.03 * 0.03 * 255 * 255; double c2 = 0.03 * 0.03;
double ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) / double ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) /
((mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2)); ((mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2));
@ -103,7 +105,8 @@ public class SyncingHelper{
for (int y = 0; y < accessor.Height; y++){ for (int y = 0; y < accessor.Height; y++){
Span<Rgba32> row = accessor.GetRowSpan(y); Span<Rgba32> row = accessor.GetRowSpan(y);
for (int x = 0; x < row.Length; x++){ for (int x = 0; x < row.Length; x++){
pixels[index++] = row[x].R; pixels[index++] = row[x].R / 255f;
;
} }
} }
}); });
@ -130,13 +133,14 @@ public class SyncingHelper{
float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight); float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight);
// Check if any frame is completely black, if so, skip SSIM calculation // 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 a negative value or zero to indicate no SSIM comparison for black frames.
return (-1.0,99); return (-1.0, 99);
} }
// Compute SSIM // Compute SSIM
return (CalculateSSIM(pixels1, pixels2),CalculatePixelDifference(pixels1,pixels2)); return (CalculateSSIM(pixels1, pixels2), CalculatePixelDifference(pixels1, pixels2));
} }
} }
@ -151,40 +155,84 @@ public class SyncingHelper{
return totalDifference / count; // Average difference 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. // Check if all pixel values are below the threshold, indicating a black frame.
return pixels.All(p => p <= threshold); 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){ 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($"SSIM: {ssim}");
// Console.WriteLine(pixelDiff); // Console.WriteLine(pixelDiff);
return ssim > ssimThreshold && pixelDiff < 10; return ssim > ssimThreshold && pixelDiff < 0.04;
} }
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 double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames,bool reverseCompare = false, double ssimThreshold = 0.9){ 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){ if (reverseCompare){
baseFrames.Reverse(); baseFrames.Reverse();
compareFrames.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){ 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){ if (matchingFrame != null){
Console.WriteLine($"Matched Frame:"); Console.WriteLine($"Matched Frame:");
Console.WriteLine($"\t Base Frame Path: {baseFrame.FilePath} Time: {baseFrame.Time},"); Console.WriteLine($"\t Base Frame Path: {baseFrame.FilePath} Time: {baseFrame.Time},");
Console.WriteLine($"\t Compare Frame Path: {matchingFrame.FilePath} Time: {matchingFrame.Time}"); Console.WriteLine($"\t Compare Frame Path: {matchingFrame.Frame.FilePath} Time: {matchingFrame.Frame.Time}");
return baseFrame.Time - matchingFrame.Time; delay = baseFrame.Time - matchingFrame.Frame.Time;
break;
} else{ } else{
// Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}"); // Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");
Debug.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;
} }
} }

View file

@ -18,8 +18,11 @@ public class CrDownloadOptions{
[JsonIgnore] [JsonIgnore]
public int Timeout{ get; set; } public int Timeout{ get; set; }
[JsonIgnore] [JsonProperty("retry_delay")]
public int FsRetryTime{ get; set; } public int RetryDelay{ get; set; }
[JsonProperty("retry_attempts")]
public int RetryAttempts{ get; set; }
[JsonIgnore] [JsonIgnore]
public string Force{ get; set; } = ""; public string Force{ get; set; } = "";
@ -233,6 +236,9 @@ public class CrDownloadOptions{
[JsonProperty("mux_sync_dubs")] [JsonProperty("mux_sync_dubs")]
public bool SyncTiming{ get; set; } public bool SyncTiming{ get; set; }
[JsonProperty("mux_sync_hwaccel")]
public string? FfmpegHwAccelFlag{ get; set; }
[JsonProperty("encode_enabled")] [JsonProperty("encode_enabled")]
public bool IsEncodeEnabled{ get; set; } public bool IsEncodeEnabled{ get; set; }

View file

@ -95,4 +95,8 @@ public class CrunchyMovie{
[JsonProperty("premium_date")] [JsonProperty("premium_date")]
public DateTime PremiumDate{ get; set; } public DateTime PremiumDate{ get; set; }
[JsonProperty("type")]
public string type{ get; set; }
} }

View file

@ -108,6 +108,11 @@ public class StringItem{
public string stringValue{ get; set; } public string stringValue{ get; set; }
} }
public class StringItemWithDisplayName{
public string DisplayName{ get; set; }
public string value{ get; set; }
}
public class WindowSettings{ public class WindowSettings{
public double Width{ get; set; } public double Width{ get; set; }
public double Height{ get; set; } public double Height{ get; set; }

View file

@ -34,6 +34,9 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_special_episode")] [JsonProperty("episode_special_episode")]
public bool SpecialEpisode{ get; set; } public bool SpecialEpisode{ get; set; }
[JsonProperty("episode_available_on_streaming_service")]
public bool IsEpisodeAvailableOnStreamingService{ get; set; }
[JsonProperty("episode_type")] [JsonProperty("episode_type")]
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown; public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown;
@ -61,6 +64,22 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("history_episode_available_dub_lang")] [JsonProperty("history_episode_available_dub_lang")]
public List<string> HistoryEpisodeAvailableDubLang{ get; set; } =[]; 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 event PropertyChangedEventHandler? PropertyChanged;
public void ToggleWasDownloaded(){ public void ToggleWasDownloaded(){

View file

@ -34,10 +34,10 @@ public class HistorySeason : INotifyPropertyChanged{
public string HistorySeasonVideoQualityOverride{ get; set; } = ""; public string HistorySeasonVideoQualityOverride{ get; set; } = "";
[JsonProperty("history_season_soft_subs_override")] [JsonProperty("history_season_soft_subs_override")]
public List<string> HistorySeasonSoftSubsOverride{ get; set; } =[]; public ObservableCollection<string> HistorySeasonSoftSubsOverride{ get; set; } =[];
[JsonProperty("history_season_dub_lang_override")] [JsonProperty("history_season_dub_lang_override")]
public List<string> HistorySeasonDubLangOverride{ get; set; } =[]; public ObservableCollection<string> HistorySeasonDubLangOverride{ get; set; } =[];
[JsonIgnore] [JsonIgnore]
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}"; public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";

View file

@ -10,7 +10,9 @@ using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.CustomList; using CRD.Utils.CustomList;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Views;
using Newtonsoft.Json; using Newtonsoft.Json;
using ReactiveUI;
namespace CRD.Utils.Structs.History; namespace CRD.Utils.Structs.History;
@ -21,6 +23,9 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_type")] [JsonProperty("series_type")]
public SeriesType SeriesType{ get; set; } = SeriesType.Unknown; public SeriesType SeriesType{ get; set; } = SeriesType.Unknown;
[JsonProperty("series_is_inactive")]
public bool IsInactive{ get; set; }
[JsonProperty("series_title")] [JsonProperty("series_title")]
public string? SeriesTitle{ get; set; } public string? SeriesTitle{ get; set; }
@ -67,10 +72,10 @@ public class HistorySeries : INotifyPropertyChanged{
public List<string> HistorySeriesAvailableDubLang{ get; set; } =[]; public List<string> HistorySeriesAvailableDubLang{ get; set; } =[];
[JsonProperty("history_series_soft_subs_override")] [JsonProperty("history_series_soft_subs_override")]
public List<string> HistorySeriesSoftSubsOverride{ get; set; } =[]; public ObservableCollection<string> HistorySeriesSoftSubsOverride{ get; set; } =[];
[JsonProperty("history_series_dub_lang_override")] [JsonProperty("history_series_dub_lang_override")]
public List<string> HistorySeriesDubLangOverride{ get; set; } =[]; public ObservableCollection<string> HistorySeriesDubLangOverride{ get; set; } =[];
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@ -460,10 +465,11 @@ public class HistorySeries : INotifyPropertyChanged{
} }
} }
public async Task FetchData(string? seasonId){ public async Task<bool> FetchData(string? seasonId){
Console.WriteLine($"Fetching Data for: {SeriesTitle}"); Console.WriteLine($"Fetching Data for: {SeriesTitle}");
FetchingData = true; FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
var isOk = true;
switch (SeriesType){ switch (SeriesType){
case SeriesType.Artist: case SeriesType.Artist:
@ -471,6 +477,7 @@ public class HistorySeries : INotifyPropertyChanged{
await CrunchyrollManager.Instance.CrMusic.ParseArtistVideosByIdAsync(SeriesId, await CrunchyrollManager.Instance.CrMusic.ParseArtistVideosByIdAsync(SeriesId,
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, true, true); string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, true, true);
} catch (Exception e){ } catch (Exception e){
isOk = false;
Console.Error.WriteLine("Failed to update History artist"); Console.Error.WriteLine("Failed to update History artist");
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
} }
@ -480,8 +487,9 @@ public class HistorySeries : INotifyPropertyChanged{
case SeriesType.Unknown: case SeriesType.Unknown:
default: default:
try{ try{
await CrunchyrollManager.Instance.History.CrUpdateSeries(SeriesId, seasonId); isOk = await CrunchyrollManager.Instance.History.CrUpdateSeries(SeriesId, seasonId);
} catch (Exception e){ } catch (Exception e){
isOk = false;
Console.Error.WriteLine("Failed to update History series"); Console.Error.WriteLine("Failed to update History series");
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
} }
@ -495,6 +503,8 @@ public class HistorySeries : INotifyPropertyChanged{
UpdateNewEpisodes(); UpdateNewEpisodes();
FetchingData = false; FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
return isOk;
} }
public void RemoveSeason(string? season){ public void RemoveSeason(string? season){

View 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);
}
}
}

View 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);
}
}

View file

@ -18,6 +18,7 @@ using CRD.Utils.Files;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData; using DynamicData;
using ReactiveUI; using ReactiveUI;
@ -265,7 +266,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
ApplyFilter(); ApplyFilter();
} }
private void ApplyFilter(){ public void ApplyFilter(){
List<HistorySeries> filteredItems; List<HistorySeries> filteredItems;
switch (currentFilterType){ switch (currentFilterType){
@ -282,13 +283,20 @@ public partial class HistoryPageViewModel : ViewModelBase{
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) && !string.IsNullOrEmpty(historySeries.SonarrSeriesId) &&
historySeries.Seasons.Any(season => historySeries.Seasons.Any(season =>
season.EpisodesList.Any(historyEpisode => season.EpisodesList.Any(historyEpisode =>
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile))) !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile &&
(!CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored || historyEpisode.SonarrIsMonitored))))
.ToList(); .ToList();
break; break;
case FilterType.ContinuingOnly: case FilterType.ContinuingOnly:
filteredItems = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList(); filteredItems = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
break; break;
case FilterType.Active:
filteredItems = Items.Where(item => !item.IsInactive).ToList();
break;
case FilterType.Inactive:
filteredItems = Items.Where(item => item.IsInactive).ToList();
break;
default: default:
filteredItems = new List<HistorySeries>(); filteredItems = new List<HistorySeries>();
@ -532,6 +540,20 @@ public partial class HistoryPageViewModel : ViewModelBase{
seriesArgs.Series?.UpdateNewEpisodes(); 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] [RelayCommand]
public void OpenFolderPath(HistorySeries? series){ public void OpenFolderPath(HistorySeries? series){
try{ try{
@ -544,6 +566,11 @@ public partial class HistoryPageViewModel : ViewModelBase{
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}"); Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
} }
} }
[RelayCommand]
public void ToggleInactive(){
CfgManager.UpdateHistoryFile();
}
} }
public class HistoryPageProperties{ public class HistoryPageProperties{

View file

@ -38,12 +38,6 @@ public partial class SeriesPageViewModel : ViewModelBase{
private IStorageProvider? _storageProvider; private IStorageProvider? _storageProvider;
[ObservableProperty]
private string _availableDubs;
[ObservableProperty]
private string _availableSubs;
public SeriesPageViewModel(){ public SeriesPageViewModel(){
_storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider)); _storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider));
@ -62,7 +56,6 @@ public partial class SeriesPageViewModel : ViewModelBase{
if (SonarrAvailable){ if (SonarrAvailable){
ShowMonitoredBookmark = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored; ShowMonitoredBookmark = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored;
} }
} else{ } else{
SonarrAvailable = false; SonarrAvailable = false;
} }
@ -70,14 +63,10 @@ public partial class SeriesPageViewModel : ViewModelBase{
SonarrConnected = SonarrAvailable = false; SonarrConnected = SonarrAvailable = false;
} }
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
SelectedSeries.UpdateSeriesFolderPath(); SelectedSeries.UpdateSeriesFolderPath();
} }
[RelayCommand] [RelayCommand]
public async Task OpenFolderDialogAsync(HistorySeason? season){ public async Task OpenFolderDialogAsync(HistorySeason? season){
if (_storageProvider == null){ if (_storageProvider == null){
@ -188,13 +177,14 @@ public partial class SeriesPageViewModel : ViewModelBase{
[RelayCommand] [RelayCommand]
public async Task UpdateData(string? season){ 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(); 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)); // 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] [RelayCommand]
public void NavBack(){ public void NavBack(){
SelectedSeries.UpdateNewEpisodes(); SelectedSeries.UpdateNewEpisodes();

View file

@ -54,6 +54,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private double? _downloadSpeed; private double? _downloadSpeed;
[ObservableProperty]
private double? _retryAttempts;
[ObservableProperty]
private double? _retryDelay;
[ObservableProperty] [ObservableProperty]
private ComboBoxItem _selectedHistoryLang; private ComboBoxItem _selectedHistoryLang;
@ -258,6 +264,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
HistorySkipUnmonitored = options.HistorySkipUnmonitored; HistorySkipUnmonitored = options.HistorySkipUnmonitored;
HistoryCountSonarr = options.HistoryCountSonarr; HistoryCountSonarr = options.HistoryCountSonarr;
DownloadSpeed = options.DownloadSpeedLimit; DownloadSpeed = options.DownloadSpeedLimit;
RetryAttempts = Math.Clamp((options.RetryAttempts), 1, 10);
RetryDelay = Math.Clamp((options.RetryDelay), 1, 30);
DownloadToTempFolder = options.DownloadToTempFolder; DownloadToTempFolder = options.DownloadToTempFolder;
SimultaneousDownloads = options.SimultaneousDownloads; SimultaneousDownloads = options.SimultaneousDownloads;
LogMode = options.LogMode; LogMode = options.LogMode;
@ -284,6 +292,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40); CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1); 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.DownloadToTempFolder = DownloadToTempFolder;
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials; CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials;
CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists = HistoryIncludeCrArtists; CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists = HistoryIncludeCrArtists;

View file

@ -36,6 +36,7 @@
<Popup IsLightDismissEnabled="True" <Popup IsLightDismissEnabled="True"
MaxWidth="{Binding Bounds.Width, ElementName=SearchBar}" MaxWidth="{Binding Bounds.Width, ElementName=SearchBar}"
MaxHeight="{Binding Bounds.Height, ElementName=Grid}" MaxHeight="{Binding Bounds.Height, ElementName=Grid}"
Width="{Binding Bounds.Width, ElementName=SearchBar}"
IsOpen="{Binding SearchPopupVisible}" IsOpen="{Binding SearchPopupVisible}"
Placement="Bottom" Placement="Bottom"
PlacementTarget="{Binding ElementName=SearchBar}" PlacementTarget="{Binding ElementName=SearchBar}"

View file

@ -16,7 +16,7 @@
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" /> <ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
<ui:UiListToStringConverter x:Key="UiListToStringConverter" /> <ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" /> <ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter"/> <ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter" />
</UserControl.Resources> </UserControl.Resources>
@ -402,16 +402,38 @@
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock> <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" <TextBlock Grid.Row="1" FontSize="15" Margin="0 0 0 5" TextWrapping="Wrap"
Text="{Binding SeriesDescription}"> Text="{Binding SeriesDescription}" MinWidth="200">
</TextBlock> </TextBlock>
<StackPanel Grid.Row="3" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}"> <StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center"
<TextBlock FontSize="15" Opacity="0.8" Text="Available Dubs: " /> IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListToStringConverter}}"></TextBlock> <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>
<StackPanel Grid.Row="4" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"> <StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center"
<TextBlock FontSize="15" Opacity="0.8" Text="Available Subs: " /> IsVisible="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListToStringConverter}}"></TextBlock> <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>
@ -462,13 +484,20 @@
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal"> <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> <ToggleButton x:Name="SeriesEditModeToggle" IsChecked="{Binding EditModeEnabled}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic" <Button Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}" Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}">
>
<Button.CommandParameter> <Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}"> <MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding /> <Binding />
@ -483,6 +512,25 @@
</StackPanel> </StackPanel>
</Button> </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> <StackPanel>
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic" <ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -671,17 +719,40 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" <StackPanel VerticalAlignment="Center">
VerticalAlignment="Center"> <ui:EpisodeHighlightTextBlock
<TextBlock Text="E"></TextBlock> Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
<TextBlock Text="{Binding Episode}"></TextBlock> Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
<TextBlock Text=" - "></TextBlock> Episode="{Binding .}"
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock> 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>
<StackPanel Grid.Column="1" <StackPanel Grid.Column="1"
Orientation="Horizontal" Orientation="Horizontal"
VerticalAlignment="Center"> VerticalAlignment="Center">
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel VerticalAlignment="Center" <StackPanel VerticalAlignment="Center"
Margin="0 0 5 0" Margin="0 0 5 0"
@ -768,8 +839,13 @@
<Button Margin="10 0 0 0" FontStyle="Italic" <Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).FetchData}" Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).UpdateData}">
CommandParameter="{Binding SeasonId}"> <Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
<Binding Path="SeasonId" />
</MultiBinding>
</Button.CommandParameter>
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="Refresh Season" FontSize="15" /> <TextBlock Text="Refresh Season" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
@ -825,7 +901,7 @@
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleDownloadedMark}"> Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleDownloadedMark}">
<Button.CommandParameter> <Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}"> <MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding Path="DataContext" ElementName="TableViewScrollViewer" /> <Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
<Binding /> <Binding />
</MultiBinding> </MultiBinding>
</Button.CommandParameter> </Button.CommandParameter>
@ -838,8 +914,7 @@
<Button Margin="10 0 0 0" FontStyle="Italic" <Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}" Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}">
>
<Button.CommandParameter> <Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}"> <MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" /> <Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />

View file

@ -22,6 +22,7 @@ namespace CRD.Views;
public partial class MainWindow : AppWindow{ public partial class MainWindow : AppWindow{
private Stack<object> navigationStack = new Stack<object>(); private Stack<object> navigationStack = new Stack<object>();
private static HashSet<string> activeErrors = new HashSet<string>();
#region Singelton #region Singelton
@ -78,30 +79,53 @@ public partial class MainWindow : AppWindow{
MessageBus.Current.Listen<NavigationMessage>() MessageBus.Current.Listen<NavigationMessage>()
.Subscribe(message => { .Subscribe(message => {
if (message.Refresh){ if (message.Refresh){
if (navigationStack.Count > 0){
navigationStack.Pop(); navigationStack.Pop();
}
try{
var viewModel = Activator.CreateInstance(message.ViewModelType); var viewModel = Activator.CreateInstance(message.ViewModelType);
navigationStack.Push(viewModel); navigationStack.Push(viewModel);
nv.Content = viewModel; nv.Content = viewModel;
} else if (!message.Back && message.ViewModelType != null){ } 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); var viewModel = Activator.CreateInstance(message.ViewModelType);
navigationStack.Push(viewModel); navigationStack.Push(viewModel);
nv.Content = viewModel;
} catch (Exception ex){
Console.Error.WriteLine($"Failed to create or push viewModel: {ex.Message}");
}
} else{
if (navigationStack.Count > 0){
navigationStack.Pop();
}
if (navigationStack.Count > 0){
var viewModel = navigationStack.Peek();
if (viewModel is HistoryPageViewModel historyView){
historyView.ApplyFilter();
}
nv.Content = viewModel; nv.Content = viewModel;
} else{ } else{
navigationStack.Pop(); Console.Error.WriteLine("Navigation stack is empty. Cannot peek.");
var viewModel = navigationStack.Peek(); }
nv.Content = viewModel;
} }
}); });
MessageBus.Current.Listen<ToastMessage>() 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(){ var dialog = new ContentDialog(){
Title = "Error", Title = "Error",
Content = message, Content = message,
@ -117,11 +141,14 @@ public partial class MainWindow : AppWindow{
if (result == ContentDialogResult.Primary){ if (result == ContentDialogResult.Primary){
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki"); Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
} }
activeErrors.Remove(message);
} }
public void ShowToast(string message, ToastType type, int durationInSeconds = 5){ 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);
} }
@ -193,7 +220,7 @@ public partial class MainWindow : AppWindow{
_restorePosition = new PixelPoint(settings.PosX, settings.PosY); _restorePosition = new PixelPoint(settings.PosX, settings.PosY);
// Ensure the window is on the correct screen before maximizing // 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){ if (settings.IsMaximized){

View file

@ -57,9 +57,42 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="45" Text="{Binding SelectedSeries.SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock> <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="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}" MinWidth="250"></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> <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 Grid.Row="5" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0 10 10 10"> <StackPanel Orientation="Horizontal" Margin="0 10 10 10">
@ -117,6 +150,24 @@
</StackPanel> </StackPanel>
</Button> </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> <StackPanel>
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic" <ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -184,7 +235,7 @@
</StackPanel> </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> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -310,26 +361,53 @@
<Grid VerticalAlignment="Center"> <Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center"> <controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> Margin="0 0 5 0 "
<TextBlock Text="E"></TextBlock> Symbol="AlertOn"
<TextBlock Text="{Binding Episode}"></TextBlock> FontSize="18"
<TextBlock Text=" - "></TextBlock> HorizontalAlignment="Center"
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock> VerticalAlignment="Center">
</StackPanel> <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" <StackPanel Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}"> IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontStyle="Italic" <TextBlock FontStyle="Italic"
FontSize="12" FontSize="12"
Opacity="0.8" Text="Dubs: "> Opacity="0.8" Text="Dubs: ">
</TextBlock> </TextBlock>
<TextBlock FontStyle="Italic"
<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" FontSize="12"
Opacity="0.8" Text="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListToStringConverter}}" /> Opacity="0.8"
FontStyle="Italic" />
</StackPanel> </StackPanel>
<!-- <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> --> <!-- <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> -->
<!-- <TextBlock FontStyle="Italic" --> <!-- <TextBlock FontStyle="Italic" -->
@ -343,7 +421,9 @@
</StackPanel> </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" <StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}"> IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">

View file

@ -14,30 +14,36 @@ public partial class ToastNotification : UserControl{
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private DispatcherTimer? currentTimer;
public void Show(string message, ToastType type, int durationInSeconds){ 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); SetStyle(type);
DispatcherTimer timer = new DispatcherTimer{ Interval = TimeSpan.FromSeconds(durationInSeconds) };
timer.Tick += (sender, args) => { currentTimer?.Stop();
timer.Stop();
this.IsVisible = false; currentTimer = new DispatcherTimer{ Interval = TimeSpan.FromSeconds(durationInSeconds) };
currentTimer.Tick += (sender, args) => {
currentTimer?.Stop();
IsVisible = false;
}; };
timer.Start(); currentTimer.Start();
this.IsVisible = true; IsVisible = true;
} }
private void SetStyle(ToastType type){ private void SetStyle(ToastType type){
var border = this.FindControl<Border>("MessageBorder"); var border = this.FindControl<Border>("MessageBorder");
border.Classes.Clear(); // Clear previous styles border?.Classes.Clear(); // Clear previous styles
switch (type){ switch (type){
case ToastType.Information: case ToastType.Information:
border.Classes.Add("info"); border?.Classes.Add("info");
break; break;
case ToastType.Error: case ToastType.Error:
border.Classes.Add("error"); border?.Classes.Add("error");
break; break;
case ToastType.Warning: case ToastType.Warning:
border.Classes.Add("warning"); border?.Classes.Add("warning");
break; break;
} }
} }

View file

@ -92,7 +92,11 @@
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1" <Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2"> CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5"> <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" <Button Content="X" FontSize="8" VerticalAlignment="Center" HorizontalAlignment="Center"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Width="15" Height="15" Padding="0" Width="15" Height="15" Padding="0"

View file

@ -74,6 +74,30 @@
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </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 Content="Use Temp Download Folder">
<controls:SettingsExpanderItem.Footer> <controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">