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{
DataContext = new MainWindowViewModel(manager),
};
desktop.MainWindow.Opened += (_, _) => { manager.SetBackgroundImage(); };
}

View file

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

View file

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

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

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@ -114,7 +115,8 @@ public class CrunchyrollManager{
options.QualityVideo = "best";
options.CcTag = "CC";
options.CcSubsFont = "Trebuchet MS";
options.FsRetryTime = 5;
options.RetryDelay = 5;
options.RetryAttempts = 5;
options.Numbers = 2;
options.Timeout = 15000;
options.DubLang = new List<string>(){ "ja-JP" };
@ -409,7 +411,7 @@ public class CrunchyrollManager{
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced,
},
fileNameAndPath);
fileNameAndPath, data);
if (result is{ merger: not null, isMuxed: true }){
mergers.Add(result.merger);
@ -474,7 +476,7 @@ public class CrunchyrollManager{
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced,
},
fileNameAndPath);
fileNameAndPath, data);
syncError = result.syncError;
muxError = !result.isMuxed;
@ -646,7 +648,7 @@ public class CrunchyrollManager{
#endregion
private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename){
private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename, CrunchyEpMeta crunchyEpMeta){
var muxToMp3 = false;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
@ -733,6 +735,16 @@ public class CrunchyrollManager{
bool isMuxed, syncError = false;
if (options is{ SyncTiming: true, DlVideoOnce: true }){
crunchyEpMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
Time = 0,
DownloadSpeed = 0,
Doing = "Muxing Syncing Dub Timings"
};
QueueManager.Instance.Queue.Refresh();
var basePath = merger.options.OnlyVid.First().Path;
var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList();
@ -764,6 +776,16 @@ public class CrunchyrollManager{
syncVideosList.ForEach(syncVideo => {
if (syncVideo.Path != null) Helpers.DeleteFile(syncVideo.Path);
});
crunchyEpMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
Time = 0,
DownloadSpeed = 0,
Doing = "Muxing"
};
QueueManager.Instance.Queue.Refresh();
}
if (!options.Mp4 && !muxToMp3){
@ -777,12 +799,12 @@ public class CrunchyrollManager{
private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){
if (Profile.Username == "???"){
MainWindow.Instance.ShowError("User Account not recognized - are you signed in?");
MainWindow.Instance.ShowError($"User Account not recognized - are you signed in?");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Login problem"
ErrorText = "User Account not recognized - are you signed in?"
};
}
@ -1347,7 +1369,7 @@ public class CrunchyrollManager{
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
string onlyFileName = Path.GetFileNameWithoutExtension(fileName);
string onlyFileName = Path.GetFileName(fileName);
int maxLength = 220;
if (onlyFileName.Length > maxLength){
@ -1361,7 +1383,7 @@ public class CrunchyrollManager{
if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){
titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength);
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
onlyFileName = Path.GetFileNameWithoutExtension(fileName);
onlyFileName = Path.GetFileName(fileName);
if (onlyFileName.Length > maxLength){
fileName = Helpers.LimitFileNameLength(fileName, maxLength);
@ -1372,7 +1394,7 @@ public class CrunchyrollManager{
fileName = Helpers.LimitFileNameLength(fileName, maxLength);
}
Console.Error.WriteLine($"Filename changed to {Path.GetFileNameWithoutExtension(fileName)}");
Console.Error.WriteLine($"Filename changed to {Path.GetFileName(fileName)}");
}
//string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
@ -1704,7 +1726,7 @@ public class CrunchyrollManager{
try{
// Parsing and constructing the file names
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray());
var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray());
if (Path.IsPathRooted(outFile)){
tsFile = outFile;
} else{
@ -1712,14 +1734,14 @@ public class CrunchyrollManager{
}
// Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(outFile);
var isAbsolute = Path.IsPathRooted(outFile);
// Get all directory parts of the path except the last segment (assuming it's a file)
string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty<string>();
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ??[];
// Initialize the cumulative path based on whether the original path is absolute or not
string cumulativePath = isAbsolute ? "" : fileDir;
for (int i = 0; i < directories.Length; i++){
var cumulativePath = isAbsolute ? "" : fileDir;
for (var i = 0; i < directories.Length; i++){
// Build the path incrementally
cumulativePath = Path.Combine(cumulativePath, directories[i]);
@ -2028,7 +2050,8 @@ public class CrunchyrollManager{
M3U8Json = videoJson,
// BaseUrl = chunkPlaylist.BaseUrl,
Threads = options.Partsize,
FsRetryTime = options.FsRetryTime * 1000,
FsRetryTime = options.RetryDelay * 1000,
Retries = options.RetryAttempts,
Override = options.Force,
}, data, true, false);
@ -2085,7 +2108,8 @@ public class CrunchyrollManager{
M3U8Json = audioJson,
// BaseUrl = chunkPlaylist.BaseUrl,
Threads = options.Partsize,
FsRetryTime = options.FsRetryTime * 1000,
FsRetryTime = options.RetryDelay * 1000,
Retries = options.RetryAttempts,
Override = options.Force,
}, data, false, true);

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -20,6 +21,7 @@ using CRD.Utils.Structs.History;
using CRD.ViewModels;
using CRD.ViewModels.Utils;
using CRD.Views.Utils;
using DynamicData;
using FluentAvalonia.UI.Controls;
// ReSharper disable InconsistentNaming
@ -58,7 +60,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private bool _muxToMp4;
[ObservableProperty]
private bool _muxFonts;
@ -209,6 +211,11 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "tv/samsung" }
];
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
[ObservableProperty]
private StringItemWithDisplayName _selectedFFmpegHWAccel;
[ObservableProperty]
private bool _isEncodeEnabled;
@ -275,6 +282,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0];
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
SelectedSubLang.Clear();
@ -391,6 +404,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
CrunchyrollManager.Instance.CrunOptions.FfmpegHwAccelFlag = SelectedFFmpegHWAccel.value;
List<string> softSubs = new List<string>();
foreach (var listBoxItem in SelectedSubLang){
softSubs.Add(listBoxItem.Content + "");
@ -568,4 +583,61 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
}
}
private List<StringItemWithDisplayName> GetAvailableHWAccelOptions(){
try{
using (var process = new Process()){
process.StartInfo.FileName = CfgManager.PathFFMPEG;
process.StartInfo.Arguments = "-hwaccels";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
string output = string.Empty;
process.OutputDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
output += e.Data + Environment.NewLine;
}
};
process.Start();
process.BeginOutputReadLine();
// process.BeginErrorReadLine();
process.WaitForExit();
var lines = output.Split(new[]{ '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var accels = lines.Skip(1).Select(l => l.Trim().ToLower()).ToList();
return MapHWAccelOptions(accels);
}
} catch (Exception e){
Console.WriteLine("Failed to get Available HW Accel Options" + e);
}
return[];
}
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){
var options = new List<StringItemWithDisplayName>{
new(){ DisplayName = "CPU Only", value = "" },
new(){ DisplayName = "Auto", value = "-hwaccel auto " }
};
if (accels.Contains("cuda")) options.Add(new StringItemWithDisplayName{ DisplayName = "NVIDIA (CUDA)", value = "-hwaccel cuda " });
if (accels.Contains("qsv")) options.Add(new StringItemWithDisplayName{ DisplayName = "Intel Quick Sync (QSV)", value = "-hwaccel qsv " });
if (accels.Contains("dxva2")) options.Add(new StringItemWithDisplayName{ DisplayName = "AMD/Intel DXVA2", value = "-hwaccel dxva2" });
if (accels.Contains("d3d11va")) options.Add(new StringItemWithDisplayName{ DisplayName = "AMD/Intel D3D11VA", value = "-hwaccel d3d11va " });
if (accels.Contains("d3d12va")) options.Add(new StringItemWithDisplayName{ DisplayName = "AMD/Intel D3D12VA", value = "-hwaccel d3d12va " });
if (accels.Contains("vaapi")) options.Add(new StringItemWithDisplayName{ DisplayName = "VAAPI (Linux)", value = "-hwaccel vaapi " });
if (accels.Contains("videotoolbox")) options.Add(new StringItemWithDisplayName{ DisplayName = "Apple VideoToolbox", value = "-hwaccel videotoolbox " });
// if (accels.Contains("opencl")) options.Add(new(){DisplayName = "OpenCL (Advanced)", value ="-hwaccel opencl "});
// if (accels.Contains("vulkan")) options.Add(new(){DisplayName = "Vulkan (Experimental)", value ="-hwaccel vulkan "});
return options;
}
}

View file

@ -286,7 +286,7 @@
<CheckBox IsChecked="{Binding DownloadChapters}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Mark as watched" Description="Mark the downloaded episodes as watched on Crunchyroll">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MarkAsWatched}"> </CheckBox>
@ -377,7 +377,7 @@
<CheckBox IsChecked="{Binding DefaultSubSigns}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include Fonts" Description="Includes the fonts in the mkv">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxFonts}"> </CheckBox>
@ -409,7 +409,23 @@
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Sync Timings" Description="Does not work for all episodes but for the ones that only have a different intro">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SyncTimings}"> </CheckBox>
<StackPanel HorizontalAlignment="Right">
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding SyncTimings}"> </CheckBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding SyncTimings}">
<TextBlock VerticalAlignment="Center" Text="Video Processing Method" Margin=" 0 0 5 0" ></TextBlock>
<ComboBox HorizontalAlignment="Right"
ItemsSource="{Binding FFmpegHWAccel}"
SelectedItem="{Binding SelectedFFmpegHWAccel}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}"></TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
@ -438,7 +454,11 @@
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
<TextBlock Text="{Binding stringValue}" Margin="5,0" TextTrimming="CharacterEllipsis" MaxWidth="300" TextWrapping="NoWrap">
<ToolTip.Tip>
<TextBlock Text="{Binding stringValue}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<Button Content="X" FontSize="10" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveMkvMergeParam}"
@ -476,7 +496,11 @@
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
<TextBlock Text="{Binding stringValue}" Margin="5,0" TextTrimming="CharacterEllipsis" MaxWidth="300" TextWrapping="NoWrap">
<ToolTip.Tip>
<TextBlock Text="{Binding stringValue}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<Button Content="X" FontSize="10" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveFfmpegParam}"

View file

@ -20,9 +20,30 @@ namespace CRD.Downloader;
public class History{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task<bool> CrUpdateSeries(string seriesId, string? seasonId){
public async Task<bool> CrUpdateSeries(string? seriesId, string? seasonId){
if (string.IsNullOrEmpty(seriesId)){
return false;
}
await crunInstance.CrAuth.RefreshToken(true);
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){
if (string.IsNullOrEmpty(seasonId)){
foreach (var historySeriesSeason in historySeries.Seasons){
foreach (var historyEpisode in historySeriesSeason.EpisodesList){
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
}
}
} else{
foreach (var historyEpisode in historySeries.Seasons.First(historySeason => historySeason.SeasonId == seasonId).EpisodesList){
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
}
}
}
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true);
if (parsedSeries == null){
@ -31,6 +52,7 @@ public class History{
}
if (parsedSeries.Data != null){
var result = false;
foreach (var s in parsedSeries.Data){
var sId = s.Id;
if (s.Versions is{ Count: > 0 }){
@ -48,17 +70,19 @@ public class History{
var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
if (seasonData.Data is{ Count: > 0 }) await UpdateWithSeasonData(seasonData.Data.ToList<IHistorySource>());
if (seasonData.Data is{ Count: > 0 }){
result = true;
await UpdateWithSeasonData(seasonData.Data.ToList<IHistorySource>());
}
}
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
historySeries ??= crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile();
return true;
return result;
}
}
@ -115,7 +139,6 @@ public class History{
historySeries.SeriesStreamingService = StreamingService.Crunchyroll;
await RefreshSeriesData(seriesId, historySeries);
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.GetSeasonId());
if (historySeason != null){
@ -140,7 +163,8 @@ public class History{
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType()
EpisodeType = historySource.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true,
};
historySeason.EpisodesList.Add(newHistoryEpisode);
@ -154,6 +178,7 @@ public class History{
historyEpisode.EpisodeSeasonNum = historySource.GetSeasonNum();
historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate();
historyEpisode.EpisodeType = historySource.GetEpisodeType();
historyEpisode.IsEpisodeAvailableOnStreamingService = true;
historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang();
historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs();
@ -218,7 +243,7 @@ public class History{
}
}
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 2));
}
public HistoryEpisode? GetHistoryEpisode(string? seriesId, string? seasonId, string episodeId){
@ -277,11 +302,11 @@ public class History{
if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesDubLangOverride.Count > 0){
dublist = historySeries.HistorySeriesDubLangOverride;
dublist = historySeries.HistorySeriesDubLangOverride.ToList();
}
if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){
sublist = historySeries.HistorySeriesSoftSubsOverride;
sublist = historySeries.HistorySeriesSoftSubsOverride.ToList();
}
if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){
@ -295,11 +320,11 @@ public class History{
if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historySeason.HistorySeasonDubLangOverride.Count > 0){
dublist = historySeason.HistorySeasonDubLangOverride;
dublist = historySeason.HistorySeasonDubLangOverride.ToList();
}
if (historySeason.HistorySeasonSoftSubsOverride.Count > 0){
sublist = historySeason.HistorySeasonSoftSubsOverride;
sublist = historySeason.HistorySeasonSoftSubsOverride.ToList();
}
if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){
@ -327,11 +352,11 @@ public class History{
if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesDubLangOverride.Count > 0){
dublist = historySeries.HistorySeriesDubLangOverride;
dublist = historySeries.HistorySeriesDubLangOverride.ToList();
}
if (historySeason is{ HistorySeasonDubLangOverride.Count: > 0 }){
dublist = historySeason.HistorySeasonDubLangOverride;
dublist = historySeason.HistorySeasonDubLangOverride.ToList();
}
}
@ -347,7 +372,7 @@ public class History{
if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeries.HistorySeriesSoftSubsOverride.Count > 0){
sublist = historySeries.HistorySeriesSoftSubsOverride;
sublist = historySeries.HistorySeriesSoftSubsOverride.ToList();
}
if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){
@ -355,7 +380,7 @@ public class History{
}
if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){
sublist = historySeason.HistorySeasonSoftSubsOverride;
sublist = historySeason.HistorySeasonSoftSubsOverride.ToList();
}
if (historySeason != null && !string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){
@ -551,7 +576,8 @@ public class History{
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType()
EpisodeType = historySource.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true
};
newSeason.EpisodesList.Add(newHistoryEpisode);
@ -566,13 +592,22 @@ public class History{
}
foreach (var historySeries in crunInstance.HistoryList){
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
if (string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty);
if (sonarrSeries != null){
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
}
} else if (updateAll){
var sonarrSeries = SonarrClient.Instance.SonarrSeries.FirstOrDefault(series => series.Id + "" == historySeries.SonarrSeriesId);
if (sonarrSeries != null){
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
} else{
Console.Error.WriteLine($"Unable to find sonarr series for {historySeries.SeriesTitle}");
}
}
}
}
@ -716,7 +751,7 @@ public class History{
if (string.IsNullOrEmpty(title)){
return null;
}
SonarrSeries? closestMatch = null;
double highestSimilarity = 0.0;

View file

@ -13,7 +13,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Utils.Updater;
using ExtendedXmlSerializer.Core.Sources;
using FluentAvalonia.Styling;
namespace CRD.Downloader;
@ -66,22 +68,42 @@ public partial class ProgramManager : ObservableObject{
private readonly FluentAvaloniaTheme? _faTheme;
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
#region Startup Param Variables
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
bool historyRefreshAdded = false;
private bool exitOnTaskFinish;
#endregion
public IStorageProvider StorageProvider;
public ProgramManager(){
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme;
foreach (var arg in Environment.GetCommandLineArgs()){
if (arg == "--historyRefreshAll"){
taskQueue.Enqueue(RefreshAll);
} else if (arg == "--historyAddToQueue"){
taskQueue.Enqueue(AddMissingToQueue);
} else if (arg == "--exit"){
exitOnTaskFinish = true;
switch (arg){
case "--historyRefreshAll":
if (!historyRefreshAdded){
taskQueue.Enqueue(() => RefreshHistory(FilterType.All));
historyRefreshAdded = true;
}
break;
case "--historyRefreshActive":
if (!historyRefreshAdded){
taskQueue.Enqueue(() => RefreshHistory(FilterType.Active));
historyRefreshAdded = true;
}
break;
case "--historyAddToQueue":
taskQueue.Enqueue(AddMissingToQueue);
break;
case "--exit":
exitOnTaskFinish = true;
break;
}
}
@ -90,16 +112,54 @@ public partial class ProgramManager : ObservableObject{
CleanUpOldUpdater();
}
private async Task RefreshAll(){
private async Task RefreshHistory(FilterType filterType){
FetchingData = true;
foreach (var item in CrunchyrollManager.Instance.HistoryList){
List<HistorySeries> filteredItems;
var historyList = CrunchyrollManager.Instance.HistoryList;
switch (filterType){
case FilterType.All:
filteredItems = historyList.ToList();
break;
case FilterType.MissingEpisodes:
filteredItems = historyList.Where(item => item.NewEpisodes > 0).ToList();
break;
case FilterType.MissingEpisodesSonarr:
filteredItems = historyList.Where(historySeries =>
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) &&
historySeries.Seasons.Any(season =>
season.EpisodesList.Any(historyEpisode =>
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile &&
(!CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored || historyEpisode.SonarrIsMonitored))))
.ToList();
break;
case FilterType.ContinuingOnly:
filteredItems = historyList.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
break;
case FilterType.Active:
filteredItems = historyList.Where(item => !item.IsInactive).ToList();
break;
case FilterType.Inactive:
filteredItems = historyList.Where(item => item.IsInactive).ToList();
break;
default:
filteredItems = new List<HistorySeries>();
break;
}
foreach (var item in filteredItems){
item.SetFetchingData();
}
for (int i = 0; i < CrunchyrollManager.Instance.HistoryList.Count; i++){
await CrunchyrollManager.Instance.HistoryList[i].FetchData("");
CrunchyrollManager.Instance.HistoryList[i].UpdateNewEpisodes();
for (int i = 0; i < filteredItems.Count; i++){
await filteredItems[i].FetchData("");
filteredItems[i].UpdateNewEpisodes();
}
FetchingData = false;
@ -115,10 +175,16 @@ public partial class ProgramManager : ObservableObject{
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress.Done != true)){
Console.WriteLine("Waiting for downloads to complete...");
await Task.Delay(2000); // Wait for 2 second before checking again
await Task.Delay(2000);
}
}
public void SetBackgroundImage(){
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){
Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity,
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius);
}
}
private async Task Init(){
CrunchyrollManager.Instance.InitOptions();
@ -142,12 +208,7 @@ public partial class ProgramManager : ObservableObject{
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
}
}
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){
Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity,
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius);
}
await CrunchyrollManager.Instance.Init();
FinishedLoading = true;

View file

@ -25,7 +25,7 @@ public partial class QueueManager : ObservableObject{
public int ActiveDownloads;
#endregion
[ObservableProperty]
private bool _hasFailedItem;
@ -92,9 +92,8 @@ public partial class QueueManager : ObservableObject{
}
HasFailedItem = Queue.Any(item => item.DownloadProgress.Error);
}
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){
if (string.IsNullOrEmpty(epId)){
@ -190,6 +189,7 @@ public partial class QueueManager : ObservableObject{
Queue.Add(selected);
if (selected.Data.Count < dubLang.Count && !CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub){
Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs");
Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: ");
@ -214,38 +214,44 @@ public partial class QueueManager : ObservableObject{
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
}
} else{
Console.WriteLine("Couldn't find episode trying to find movie with id");
var movie = await CrunchyrollManager.Instance.CrMovies.ParseMovieById(epId, crLocale);
return;
}
if (movie != null){
var movieMeta = CrunchyrollManager.Instance.CrMovies.EpisodeMeta(movie, dubLang);
Console.WriteLine("Couldn't find episode trying to find movie with id");
if (movieMeta != null){
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
movieMeta.OnlySubs = onlySubs;
var movie = await CrunchyrollManager.Instance.CrMovies.ParseMovieById(epId, crLocale);
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
if (movie != null){
var movieMeta = CrunchyrollManager.Instance.CrMovies.EpisodeMeta(movie, dubLang);
if (movieMeta.OnlySubs){
newOptions.Novids = true;
newOptions.Noaudio = true;
}
if (movieMeta != null){
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
movieMeta.OnlySubs = onlySubs;
newOptions.DubLang = dubLang;
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
movieMeta.DownloadSettings = newOptions;
movieMeta.VideoQuality = CrunchyrollManager.Instance.CrunOptions.QualityVideo;
Queue.Add(movieMeta);
Console.WriteLine("Added Movie to Queue");
MessageBus.Current.SendMessage(new ToastMessage($"Added Movie to Queue", ToastType.Information, 1));
if (movieMeta.OnlySubs){
newOptions.Novids = true;
newOptions.Noaudio = true;
}
newOptions.DubLang = dubLang;
movieMeta.DownloadSettings = newOptions;
movieMeta.VideoQuality = CrunchyrollManager.Instance.CrunOptions.QualityVideo;
Queue.Add(movieMeta);
Console.WriteLine("Added Movie to Queue");
MessageBus.Current.SendMessage(new ToastMessage($"Added Movie to Queue", ToastType.Information, 1));
return;
}
}
Console.Error.WriteLine($"No episode or movie found with the id: {epId}");
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue - No episode or movie found with the id: {epId}", ToastType.Error, 3));
}

View file

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

View file

@ -361,7 +361,7 @@ public class HlsDownloader{
throw new Exception("Failed to download key");
_data.Keys[kUri] = rkey;
} catch (Exception ex){
throw new Exception($"Error at segment {p}: {ex.Message}", ex);
throw new Exception($"Key Error at segment {p}: {ex.Message}", ex);
}
}
@ -431,7 +431,7 @@ public class HlsDownloader{
// Set default user-agent if not provided
if (!request.Headers.Contains("User-Agent")){
request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0");
request.Headers.Add("User-Agent", ApiUrls.FirefoxUserAgent);
}
return await SendRequestWithRetry(request, partIndex, segOffset, isKey, retryCount);
@ -445,7 +445,7 @@ public class HlsDownloader{
response = await HttpClientReq.Instance.GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
return await ReadContentAsByteArrayAsync(response.Content);
} catch (HttpRequestException ex){
} catch (Exception ex) when (ex is HttpRequestException or IOException){
// Log retry attempts
string partType = isKey ? "Key" : "Part";
int partIndx = partIndex + 1 + segOffset;
@ -453,8 +453,14 @@ public class HlsDownloader{
Console.WriteLine($"\tError: {ex.Message}");
if (attempt == retryCount)
throw; // rethrow after last retry
await Task.Delay(_data.WaitTime);
}catch (Exception ex) {
Console.WriteLine($"Unexpected exception at part {partIndex + 1 + segOffset}:");
Console.WriteLine($"\tType: {ex.GetType()}");
Console.WriteLine($"\tMessage: {ex.Message}");
throw;
}
}
}

View file

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

View file

@ -271,5 +271,6 @@ public static class ApiUrls{
public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE=";
public static readonly string MobileUserAgent = "Crunchyroll/3.78.3 Android/15 okhttp/4.12.0";
public static readonly string MobileUserAgent = "Crunchyroll/3.79.0 Android/15 okhttp/4.12.0";
public static readonly string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0";
}

View file

@ -23,7 +23,7 @@ public class LocaleConverter : JsonConverter{
return locale;
}
return Locale.Unknown; // Default to defaulT if no match is found
return Locale.Unknown;
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer){

View file

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

View file

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using SixLabors.ImageSharp;
@ -17,7 +18,8 @@ namespace CRD.Utils.Muxing;
public class SyncingHelper{
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
var ffmpegPath = CfgManager.PathFFMPEG;
var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -fps_mode vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%05d.png\"";
var arguments =
$"{CrunchyrollManager.Instance.CrunOptions.FfmpegHwAccelFlag}-ss {offset} -t {duration} -i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -vsync vfr -frame_pts true \"{outputDir}\\frame%05d.jpg\"";
var output = "";
@ -86,8 +88,8 @@ public class SyncingHelper{
var2 /= count - 1;
covariance /= count - 1;
double c1 = 0.01 * 0.01 * 255 * 255;
double c2 = 0.03 * 0.03 * 255 * 255;
double c1 = 0.01 * 0.01;
double c2 = 0.03 * 0.03;
double ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) /
((mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2));
@ -103,7 +105,8 @@ public class SyncingHelper{
for (int y = 0; y < accessor.Height; y++){
Span<Rgba32> row = accessor.GetRowSpan(y);
for (int x = 0; x < row.Length; x++){
pixels[index++] = row[x].R;
pixels[index++] = row[x].R / 255f;
;
}
}
});
@ -130,16 +133,17 @@ public class SyncingHelper{
float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight);
// Check if any frame is completely black, if so, skip SSIM calculation
if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2)){
if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2) ||
IsMonochromaticFrame(pixels1) || IsMonochromaticFrame(pixels2)){
// Return a negative value or zero to indicate no SSIM comparison for black frames.
return (-1.0,99);
return (-1.0, 99);
}
// Compute SSIM
return (CalculateSSIM(pixels1, pixels2),CalculatePixelDifference(pixels1,pixels2));
return (CalculateSSIM(pixels1, pixels2), CalculatePixelDifference(pixels1, pixels2));
}
}
private static double CalculatePixelDifference(float[] pixels1, float[] pixels2){
double totalDifference = 0;
int count = pixels1.Length;
@ -151,40 +155,84 @@ public class SyncingHelper{
return totalDifference / count; // Average difference
}
private static bool IsBlackFrame(float[] pixels, float threshold = 1.0f){
private static bool IsBlackFrame(float[] pixels, float threshold = 0.02f){
// Check if all pixel values are below the threshold, indicating a black frame.
return pixels.All(p => p <= threshold);
}
private static bool IsMonochromaticFrame(float[] pixels, float stdDevThreshold = 0.05f){
float avg = pixels.Average();
double variance = pixels.Average(p => Math.Pow(p - avg, 2));
double stdDev = Math.Sqrt(variance);
return stdDev < stdDevThreshold;
}
public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){
var (ssim, pixelDiff) = ComputeSSIM(imagePath1, imagePath2, 256, 256);
var (ssim, pixelDiff) = ComputeSSIM(imagePath1, imagePath2, 256, 144);
// Console.WriteLine($"SSIM: {ssim}");
// Console.WriteLine(pixelDiff);
return ssim > ssimThreshold && pixelDiff < 10;
return ssim > ssimThreshold && pixelDiff < 0.04;
}
public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames,bool reverseCompare = false, double ssimThreshold = 0.9){
public static float[] GetPixelsArray(string imagePath, int targetWidth = 256, int targetHeight = 144){
using var image = Image.Load<Rgba32>(imagePath);
image.Mutate(x => x.Resize(new ResizeOptions{
Size = new Size(targetWidth, targetHeight),
Mode = ResizeMode.Max
}).Grayscale());
return ExtractPixels(image, targetWidth, targetHeight);
}
public static bool AreFramesSimilarPreprocessed(float[] image1, float[] image2, double ssimThreshold){
if (IsBlackFrame(image1) || IsBlackFrame(image2) ||
IsMonochromaticFrame(image1) || IsMonochromaticFrame(image2)){
return false;
}
var pixelDiff = CalculatePixelDifference(image1, image2);
if (pixelDiff > 0.04){
return false;
}
var ssim = CalculateSSIM(image1, image2);
return ssim > ssimThreshold && pixelDiff < 0.04;
}
public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames, bool reverseCompare = false, double ssimThreshold = 0.9){
if (reverseCompare){
baseFrames.Reverse();
compareFrames.Reverse();
}
var preprocessedCompareFrames = compareFrames.Select(f => new{
Frame = f,
Pixels = GetPixelsArray(f.FilePath)
}).ToList();
var delay = 0.0;
foreach (var baseFrame in baseFrames){
var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold));
var baseFramePixels = GetPixelsArray(baseFrame.FilePath);
var matchingFrame = preprocessedCompareFrames.AsParallel()
.WithExecutionMode(ParallelExecutionMode.ForceParallelism).FirstOrDefault(f => AreFramesSimilarPreprocessed(baseFramePixels, f.Pixels, ssimThreshold));
if (matchingFrame != null){
Console.WriteLine($"Matched Frame:");
Console.WriteLine($"\t Base Frame Path: {baseFrame.FilePath} Time: {baseFrame.Time},");
Console.WriteLine($"\t Compare Frame Path: {matchingFrame.FilePath} Time: {matchingFrame.Time}");
return baseFrame.Time - matchingFrame.Time;
Console.WriteLine($"\t Compare Frame Path: {matchingFrame.Frame.FilePath} Time: {matchingFrame.Frame.Time}");
delay = baseFrame.Time - matchingFrame.Frame.Time;
break;
} else{
// Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");
Debug.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");
}
}
return 0;
preprocessedCompareFrames.Clear();
GC.Collect(); // Segment float arrays to avoid calling GC.Collect ?
return delay;
}
}

View file

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

View file

@ -95,4 +95,8 @@ public class CrunchyMovie{
[JsonProperty("premium_date")]
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 class StringItemWithDisplayName{
public string DisplayName{ get; set; }
public string value{ get; set; }
}
public class WindowSettings{
public double Width{ get; set; }
public double Height{ get; set; }

View file

@ -34,6 +34,9 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_special_episode")]
public bool SpecialEpisode{ get; set; }
[JsonProperty("episode_available_on_streaming_service")]
public bool IsEpisodeAvailableOnStreamingService{ get; set; }
[JsonProperty("episode_type")]
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown;
@ -61,6 +64,22 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("history_episode_available_dub_lang")]
public List<string> HistoryEpisodeAvailableDubLang{ get; set; } =[];
[JsonIgnore]
public string ReleaseDateFormated{
get{
if (!EpisodeCrPremiumAirDate.HasValue ||
EpisodeCrPremiumAirDate.Value == DateTime.MinValue ||
EpisodeCrPremiumAirDate.Value.Date == new DateTime(1970, 1, 1))
return string.Empty;
var cultureInfo = System.Globalization.CultureInfo.InvariantCulture;
string monthAbbreviation = cultureInfo.DateTimeFormat.GetAbbreviatedMonthName(EpisodeCrPremiumAirDate.Value.Month);
return string.Format("{0:00}.{1}.{2}", EpisodeCrPremiumAirDate.Value.Day, monthAbbreviation, EpisodeCrPremiumAirDate.Value.Year);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public void ToggleWasDownloaded(){

View file

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

View file

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

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.Structs;
using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData;
using ReactiveUI;
@ -265,7 +266,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
ApplyFilter();
}
private void ApplyFilter(){
public void ApplyFilter(){
List<HistorySeries> filteredItems;
switch (currentFilterType){
@ -282,13 +283,20 @@ public partial class HistoryPageViewModel : ViewModelBase{
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) &&
historySeries.Seasons.Any(season =>
season.EpisodesList.Any(historyEpisode =>
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile)))
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile &&
(!CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored || historyEpisode.SonarrIsMonitored))))
.ToList();
break;
case FilterType.ContinuingOnly:
filteredItems = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
break;
case FilterType.Active:
filteredItems = Items.Where(item => !item.IsInactive).ToList();
break;
case FilterType.Inactive:
filteredItems = Items.Where(item => item.IsInactive).ToList();
break;
default:
filteredItems = new List<HistorySeries>();
@ -532,6 +540,20 @@ public partial class HistoryPageViewModel : ViewModelBase{
seriesArgs.Series?.UpdateNewEpisodes();
}
[RelayCommand]
public async Task UpdateData(SeasonDialogArgs seriesArgs){
if (seriesArgs.Series != null){
var result = await seriesArgs.Series.FetchData(seriesArgs.Season?.SeasonId);
MessageBus.Current.SendMessage(result
? new ToastMessage(string.IsNullOrEmpty(seriesArgs.Season?.SeasonId) ? $"Series Refreshed" : $"Season Refreshed", ToastType.Information, 2)
: new ToastMessage(string.IsNullOrEmpty(seriesArgs.Season?.SeasonId) ? $"Series Refresh Failed" : $"Season Refreshed Failed", ToastType.Error, 2));
} else{
MessageBus.Current.SendMessage(new ToastMessage(string.IsNullOrEmpty(seriesArgs.Season?.SeasonId) ? $"Refresh Failed" : $"Season Refresh Failed", ToastType.Error, 2));
Console.Error.WriteLine("Failed to get Series Data from View Tree - report issue");
}
}
[RelayCommand]
public void OpenFolderPath(HistorySeries? series){
try{
@ -544,6 +566,11 @@ public partial class HistoryPageViewModel : ViewModelBase{
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
}
}
[RelayCommand]
public void ToggleInactive(){
CfgManager.UpdateHistoryFile();
}
}
public class HistoryPageProperties{

View file

@ -29,7 +29,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty]
public static bool _sonarrAvailable;
[ObservableProperty]
public static bool _showMonitoredBookmark;
@ -38,12 +38,6 @@ public partial class SeriesPageViewModel : ViewModelBase{
private IStorageProvider? _storageProvider;
[ObservableProperty]
private string _availableDubs;
[ObservableProperty]
private string _availableSubs;
public SeriesPageViewModel(){
_storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider));
@ -58,25 +52,20 @@ public partial class SeriesPageViewModel : ViewModelBase{
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected;
if (SonarrAvailable){
ShowMonitoredBookmark = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored;
}
} else{
SonarrAvailable = false;
}
} else{
SonarrConnected = SonarrAvailable = false;
}
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
SelectedSeries.UpdateSeriesFolderPath();
}
[RelayCommand]
public async Task OpenFolderDialogAsync(HistorySeason? season){
@ -188,13 +177,14 @@ public partial class SeriesPageViewModel : ViewModelBase{
[RelayCommand]
public async Task UpdateData(string? season){
await SelectedSeries.FetchData(season);
var result = await SelectedSeries.FetchData(season);
MessageBus.Current.SendMessage(result
? new ToastMessage(string.IsNullOrEmpty(season) ? $"Series Refreshed" : $"Season Refreshed", ToastType.Information, 2)
: new ToastMessage(string.IsNullOrEmpty(season) ? $"Series Refresh Failed" : $"Season Refresh Failed", ToastType.Error, 2));
SelectedSeries.Seasons.Refresh();
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
// MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
}
@ -210,6 +200,11 @@ public partial class SeriesPageViewModel : ViewModelBase{
}
[RelayCommand]
public void ToggleInactive(){
CfgManager.UpdateHistoryFile();
}
[RelayCommand]
public void NavBack(){
SelectedSeries.UpdateNewEpisodes();

View file

@ -53,6 +53,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
[ObservableProperty]
private double? _downloadSpeed;
[ObservableProperty]
private double? _retryAttempts;
[ObservableProperty]
private double? _retryDelay;
[ObservableProperty]
private ComboBoxItem _selectedHistoryLang;
@ -258,6 +264,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
HistorySkipUnmonitored = options.HistorySkipUnmonitored;
HistoryCountSonarr = options.HistoryCountSonarr;
DownloadSpeed = options.DownloadSpeedLimit;
RetryAttempts = Math.Clamp((options.RetryAttempts), 1, 10);
RetryDelay = Math.Clamp((options.RetryDelay), 1, 30);
DownloadToTempFolder = options.DownloadToTempFolder;
SimultaneousDownloads = options.SimultaneousDownloads;
LogMode = options.LogMode;
@ -283,6 +291,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1);
CrunchyrollManager.Instance.CrunOptions.RetryAttempts = Math.Clamp((int)(RetryAttempts ?? 0), 1, 10);
CrunchyrollManager.Instance.CrunOptions.RetryDelay = Math.Clamp((int)(RetryDelay ?? 0), 1, 30);
CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder;
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials;

View file

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

View file

@ -16,9 +16,9 @@
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter"/>
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter" />
</UserControl.Resources>
<Grid>
@ -402,19 +402,41 @@
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock>
<TextBlock Grid.Row="1" FontSize="15" Margin="0 0 0 5" TextWrapping="Wrap"
Text="{Binding SeriesDescription}">
Text="{Binding SeriesDescription}" MinWidth="200">
</TextBlock>
<StackPanel Grid.Row="3" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" Text="Available Dubs: " />
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListToStringConverter}}"></TextBlock>
<StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Dubs: "></TextBlock>
<ui:HighlightingTextBlock
Items="{Binding HistorySeriesAvailableDubLang}"
Series="{Binding .}"
Season=""
StreamingService="Crunchyroll"
CheckDubs="True"
FontSize="15"
Opacity="0.8"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="4" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" Text="Available Subs: " />
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListToStringConverter}}"></TextBlock>
<StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Subs: "></TextBlock>
<ui:HighlightingTextBlock
Items="{Binding HistorySeriesAvailableSoftSubs}"
Series="{Binding .}"
Season=""
StreamingService="Crunchyroll"
CheckDubs="False"
FontSize="15"
Opacity="0.8"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="5" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
@ -443,7 +465,7 @@
Height="30" />
</Grid>
</Button>
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
IsVisible="{Binding SeriesFolderPathExists}"
@ -462,13 +484,20 @@
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding FetchData}" Margin="0 0 5 10">Refresh Series</Button>
<Button Content="Refresh Series"
Margin="0 0 5 10"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).UpdateData}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding />
</MultiBinding>
</Button.CommandParameter>
</Button>
<ToggleButton x:Name="SeriesEditModeToggle" IsChecked="{Binding EditModeEnabled}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}"
>
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding />
@ -483,6 +512,25 @@
</StackPanel>
</Button>
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
IsChecked="{Binding IsInactive}"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleInactive}">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
<TextBlock Text="Set Active"
IsVisible="{Binding IsInactive}"
FontSize="15" />
<TextBlock Text="Set Inactive"
IsVisible="{Binding !IsInactive}"
FontSize="15" />
</StackPanel>
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="CloudOffline" FontSize="18" />
</StackPanel>
</ToggleButton>
<StackPanel>
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
@ -671,22 +719,45 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="E"></TextBlock>
<TextBlock Text="{Binding Episode}"></TextBlock>
<TextBlock Text=" - "></TextBlock>
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
<StackPanel VerticalAlignment="Center">
<ui:EpisodeHighlightTextBlock
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
Episode="{Binding .}"
StreamingService="Crunchyroll"
MinWidth="50"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontStyle="Italic"
FontSize="12"
Opacity="0.8" Text="Dubs: ">
</TextBlock>
<ui:HighlightingTextBlock
Items="{Binding HistoryEpisodeAvailableDubLang}"
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
StreamingService="Crunchyroll"
CheckDubs="True"
FontSize="12"
Opacity="0.8"
FontStyle="Italic" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel VerticalAlignment="Center"
Margin="0 0 5 0"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).SonarrSeriesId, Converter={StaticResource UiSonarrIdToVisibilityConverter}}">
<controls:ImageIcon
IsVisible="{Binding SonarrHasFile}"
Source="../Assets/sonarr.png"
@ -740,7 +811,7 @@
FontSize="18" />
</StackPanel>
</Button>
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
@ -768,8 +839,13 @@
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).FetchData}"
CommandParameter="{Binding SeasonId}">
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).UpdateData}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
<Binding Path="SeasonId" />
</MultiBinding>
</Button.CommandParameter>
<ToolTip.Tip>
<TextBlock Text="Refresh Season" FontSize="15" />
</ToolTip.Tip>
@ -825,7 +901,7 @@
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleDownloadedMark}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding Path="DataContext" ElementName="TableViewScrollViewer" />
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
<Binding />
</MultiBinding>
</Button.CommandParameter>
@ -835,11 +911,10 @@
</Border>
</Popup>
</StackPanel>
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}"
>
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />

View file

@ -22,6 +22,7 @@ namespace CRD.Views;
public partial class MainWindow : AppWindow{
private Stack<object> navigationStack = new Stack<object>();
private static HashSet<string> activeErrors = new HashSet<string>();
#region Singelton
@ -43,7 +44,7 @@ public partial class MainWindow : AppWindow{
}
#endregion
private object selectedNavVieItem;
private const int TitleBarHeightAdjustment = 31;
@ -55,12 +56,12 @@ public partial class MainWindow : AppWindow{
ProgramManager.Instance.StorageProvider = StorageProvider;
AvaloniaXamlLoader.Load(this);
InitializeComponent();
ExtendClientAreaTitleBarHeightHint = TitleBarHeightAdjustment;
TitleBar.Height = TitleBarHeightAdjustment;
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
Opened += OnOpened;
Closing += OnClosing;
@ -72,36 +73,59 @@ public partial class MainWindow : AppWindow{
//select first element as default
var nv = this.FindControl<NavigationView>("NavView");
nv.SelectedItem = nv.MenuItems.ElementAt(0);
nv.SelectedItem = nv.MenuItems.ElementAt(0);
selectedNavVieItem = nv.SelectedItem;
MessageBus.Current.Listen<NavigationMessage>()
.Subscribe(message => {
if (message.Refresh){
navigationStack.Pop();
var viewModel = Activator.CreateInstance(message.ViewModelType);
if (navigationStack.Count > 0){
navigationStack.Pop();
}
navigationStack.Push(viewModel);
nv.Content = viewModel;
} else if (!message.Back && message.ViewModelType != null){
var viewModel = Activator.CreateInstance(message.ViewModelType);
navigationStack.Push(viewModel);
nv.Content = viewModel;
try{
var viewModel = Activator.CreateInstance(message.ViewModelType);
navigationStack.Push(viewModel);
nv.Content = viewModel;
} catch (Exception ex){
Console.Error.WriteLine($"Failed to create or push viewModel: {ex.Message}");
}
} else if (message is{ Back: false, ViewModelType: not null }){
try{
var viewModel = Activator.CreateInstance(message.ViewModelType);
navigationStack.Push(viewModel);
nv.Content = viewModel;
} catch (Exception ex){
Console.Error.WriteLine($"Failed to create or push viewModel: {ex.Message}");
}
} else{
navigationStack.Pop();
var viewModel = navigationStack.Peek();
nv.Content = viewModel;
if (navigationStack.Count > 0){
navigationStack.Pop();
}
if (navigationStack.Count > 0){
var viewModel = navigationStack.Peek();
if (viewModel is HistoryPageViewModel historyView){
historyView.ApplyFilter();
}
nv.Content = viewModel;
} else{
Console.Error.WriteLine("Navigation stack is empty. Cannot peek.");
}
}
});
MessageBus.Current.Listen<ToastMessage>()
.Subscribe(message => ShowToast(message.Message, message.Type, message.Seconds));
.Subscribe(message => ShowToast(message.Message ?? string.Empty, message.Type, message.Seconds));
}
public async void ShowError(string message,bool githubWikiButton = false){
public async void ShowError(string message, bool githubWikiButton = false){
if (activeErrors.Contains(message))
return;
activeErrors.Add(message);
var dialog = new ContentDialog(){
Title = "Error",
Content = message,
@ -109,19 +133,22 @@ public partial class MainWindow : AppWindow{
};
if (githubWikiButton){
dialog.PrimaryButtonText = "Github Wiki";
dialog.PrimaryButtonText = "Github Wiki";
}
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary){
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
}
activeErrors.Remove(message);
}
public void ShowToast(string message, ToastType type, int durationInSeconds = 5){
this.FindControl<ToastNotification>("Toast").Show(message, type, durationInSeconds);
var toastControl = this.FindControl<ToastNotification>("Toast");
toastControl?.Show(message, type, durationInSeconds);
}
@ -172,7 +199,7 @@ public partial class MainWindow : AppWindow{
}
}
}
private void OnOpened(object sender, EventArgs e){
if (File.Exists(CfgManager.PathWindowSettings)){
var settings = JsonConvert.DeserializeObject<WindowSettings>(File.ReadAllText(CfgManager.PathWindowSettings));
@ -193,7 +220,7 @@ public partial class MainWindow : AppWindow{
_restorePosition = new PixelPoint(settings.PosX, settings.PosY);
// Ensure the window is on the correct screen before maximizing
Position = new PixelPoint(settings.PosX, settings.PosY );
Position = new PixelPoint(settings.PosX, settings.PosY);
}
if (settings.IsMaximized){
@ -242,7 +269,7 @@ public partial class MainWindow : AppWindow{
private void OnPositionChanged(object sender, PixelPointEventArgs e){
if (WindowState == WindowState.Normal){
var screens = Screens.All;
bool isWithinAnyScreen = screens.Any(screen =>
e.Point.X >= screen.WorkingArea.X &&
e.Point.X <= screen.WorkingArea.X + screen.WorkingArea.Width &&

View file

@ -57,9 +57,42 @@
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="45" Text="{Binding SelectedSeries.SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock>
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock>
<TextBlock Grid.Row="3" IsVisible="{Binding SelectedSeries.HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableDubs}"></TextBlock>
<TextBlock Grid.Row="4" IsVisible="{Binding SelectedSeries.HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableSubs}"></TextBlock>
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}" MinWidth="250"></TextBlock>
<StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding SelectedSeries.HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Dubs: "></TextBlock>
<ui:HighlightingTextBlock
Items="{Binding SelectedSeries.HistorySeriesAvailableDubLang}"
Series="{Binding SelectedSeries}"
Season=""
StreamingService="Crunchyroll"
CheckDubs="True"
FontSize="15"
Opacity="0.8"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding SelectedSeries.HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Subs: "></TextBlock>
<ui:HighlightingTextBlock
Items="{Binding SelectedSeries.HistorySeriesAvailableSoftSubs}"
Series="{Binding SelectedSeries}"
Season=""
StreamingService="Crunchyroll"
CheckDubs="False"
FontSize="15"
Opacity="0.8"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="5" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
@ -117,6 +150,24 @@
</StackPanel>
</Button>
<ToggleButton Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
IsChecked="{Binding SelectedSeries.IsInactive}" Command="{Binding ToggleInactive}">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
<TextBlock Text="Set Active"
IsVisible="{Binding SelectedSeries.IsInactive}"
FontSize="15" />
<TextBlock Text="Set Inactive"
IsVisible="{Binding !SelectedSeries.IsInactive}"
FontSize="15" />
</StackPanel>
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="CloudOffline" FontSize="18" />
</StackPanel>
</ToggleButton>
<StackPanel>
<ToggleButton x:Name="SeriesOverride" Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
@ -184,7 +235,7 @@
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10" >
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -310,26 +361,53 @@
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="E"></TextBlock>
<TextBlock Text="{Binding Episode}"></TextBlock>
<TextBlock Text=" - "></TextBlock>
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
</StackPanel>
<controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
Margin="0 0 5 0 "
Symbol="AlertOn"
FontSize="18"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ToolTip.Tip>
<TextBlock Text="Episode unavailable — it might not be available on the streaming service" FontSize="15" />
</ToolTip.Tip>
</controls:SymbolIcon>
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left">
<ui:EpisodeHighlightTextBlock
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
Episode="{Binding }"
StreamingService="Crunchyroll"
MinWidth="50"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontStyle="Italic"
FontSize="12"
Opacity="0.8" Text="Dubs: ">
</TextBlock>
<TextBlock FontStyle="Italic"
FontSize="12"
Opacity="0.8" Text="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListToStringConverter}}" />
<ui:HighlightingTextBlock
Items="{Binding HistoryEpisodeAvailableDubLang}"
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
StreamingService="Crunchyroll"
CheckDubs="True"
FontSize="12"
Opacity="0.8"
FontStyle="Italic" />
</StackPanel>
<!-- <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> -->
<!-- <TextBlock FontStyle="Italic" -->
@ -343,7 +421,9 @@
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">

View file

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

View file

@ -92,10 +92,14 @@
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
<TextBlock Text="{Binding stringValue}" Margin="5,0" TextTrimming="CharacterEllipsis" MaxWidth="300" TextWrapping="NoWrap">
<ToolTip.Tip>
<TextBlock Text="{Binding stringValue}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<Button Content="X" FontSize="8" VerticalAlignment="Center" HorizontalAlignment="Center"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Width="15" Height="15" Padding="0"
Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((utils:ContentDialogEncodingPresetViewModel)DataContext).RemoveAdditionalParam}"
CommandParameter="{Binding .}" />
</StackPanel>

View file

@ -74,6 +74,30 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Retry"
Description="Sett the number of retry attempts and the delay between each retry">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Width="100" Text="Retry Attempts" Margin="5 0" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
<controls:NumberBox Minimum="1" Maximum="10"
Value="{Binding RetryAttempts}"
SpinButtonPlacementMode="Hidden"
HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 5">
<TextBlock Width="100" Text="Retry Delay (s)" Margin="5 0" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
<controls:NumberBox Minimum="1" Maximum="30"
Value="{Binding RetryDelay}"
SpinButtonPlacementMode="Hidden"
HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use Temp Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">