diff --git a/CRD/Assets/crunchy_icon_round.png b/CRD/Assets/crunchy_icon_round.png index 9010c3c..431a4ff 100644 --- a/CRD/Assets/crunchy_icon_round.png +++ b/CRD/Assets/crunchy_icon_round.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60deeb982f931b150ed8512e9ae284061722fd3719acc717ca812098ce4547f4 -size 116598 +oid sha256:eb8049bb754d9e938ac7adf0b7884274c68d265b7ce7b0c4fa2085f84677296b +size 5780 diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs index bee8f6d..83443e0 100644 --- a/CRD/Downloader/CalendarManager.cs +++ b/CRD/Downloader/CalendarManager.cs @@ -181,6 +181,10 @@ public class CalendarManager{ foreach (var crBrowseEpisode in newEpisodes){ var targetDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic; + if (targetDate.Kind == DateTimeKind.Utc){ + targetDate = targetDate.ToLocalTime(); + } + if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && (crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){ continue; diff --git a/CRD/Downloader/Crunchyroll/CRAuth.cs b/CRD/Downloader/Crunchyroll/CRAuth.cs index 014bc92..052b453 100644 --- a/CRD/Downloader/Crunchyroll/CRAuth.cs +++ b/CRD/Downloader/Crunchyroll/CRAuth.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using System.Web; using CRD.Utils; using CRD.Utils.Structs; using Newtonsoft.Json; @@ -13,7 +11,6 @@ using Newtonsoft.Json; namespace CRD.Downloader.Crunchyroll; public class CrAuth{ - private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; public async Task AuthAnonymous(){ @@ -44,9 +41,7 @@ public class CrAuth{ PreferredContentAudioLanguage = "ja-JP", PreferredContentSubtitleLanguage = "de-DE" }; - - // CrunchyrollManager.Instance.CmsToken = new CrCmsToken(); - + } private void JsonTokenToFileAndVariable(string content){ @@ -104,35 +99,36 @@ public class CrAuth{ if (profileTemp != null){ crunInstance.Profile = profileTemp; - + var requestSubs = HttpClientReq.CreateRequestMessage(Api.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null); var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs); - + if (responseSubs.IsOk){ var subsc = Helpers.Deserialize(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings); crunInstance.Profile.Subscription = subsc; - if ( subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){ + if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){ var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First(); var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate; var remaining = expiration - DateTime.UtcNow; crunInstance.Profile.HasPremium = remaining > TimeSpan.Zero; crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero; crunInstance.Profile.Subscription.NextRenewalDate = expiration; - } else if(subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){ + } else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){ var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First(); var remaining = nonRecurringSub.EndDate - DateTime.UtcNow; crunInstance.Profile.HasPremium = remaining > TimeSpan.Zero; crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero; crunInstance.Profile.Subscription.NextRenewalDate = nonRecurringSub.EndDate; + } else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){ + crunInstance.Profile.HasPremium = true; } else{ - crunInstance.Profile.HasPremium = subsc.IsActive; + crunInstance.Profile.HasPremium = subsc.IsActive; } } else{ crunInstance.Profile.HasPremium = false; Console.Error.WriteLine("Failed to check premium subscription status"); } - } } } @@ -167,11 +163,10 @@ public class CrAuth{ if (crunInstance.Token?.refresh_token != null){ HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); - + await GetProfile(); } - - // await GetCmsToken(); + } public async Task RefreshToken(bool needsToken){ @@ -209,53 +204,7 @@ public class CrAuth{ } else{ Console.Error.WriteLine("Refresh Token Auth Failed"); } - - // await GetCmsToken(); + } - - - // public async Task GetCmsToken(){ - // if (crunInstance.Token?.access_token == null){ - // Console.Error.WriteLine($"Missing Access Token: {crunInstance.Token?.access_token}"); - // return; - // } - // - // var request = HttpClientReq.CreateRequestMessage(Api.BetaCmsToken, HttpMethod.Get, true, true, null); - // - // var response = await HttpClientReq.Instance.SendHttpRequest(request); - // - // if (response.IsOk){ - // crunInstance.CmsToken = JsonConvert.DeserializeObject(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); - // } else{ - // Console.Error.WriteLine("CMS Token Auth Failed"); - // } - // } - // - // public async Task GetCmsData(){ - // if (crunInstance.CmsToken?.Cms == null){ - // Console.Error.WriteLine("Missing CMS Token"); - // return; - // } - // - // UriBuilder uriBuilder = new UriBuilder(Api.BetaCms + crunInstance.CmsToken.Cms.Bucket + "/index?"); - // - // NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); - // - // query["preferred_audio_language"] = "ja-JP"; - // query["Policy"] = crunInstance.CmsToken.Cms.Policy; - // query["Signature"] = crunInstance.CmsToken.Cms.Signature; - // query["Key-Pair-Id"] = crunInstance.CmsToken.Cms.KeyPairId; - // - // uriBuilder.Query = query.ToString(); - // - // var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.ToString()); - // - // var response = await HttpClientReq.Instance.SendHttpRequest(request); - // - // if (response.IsOk){ - // Console.WriteLine(response.ResponseContent); - // } else{ - // Console.Error.WriteLine("Failed to Get CMS Index"); - // } - // } + } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 560155e..63d4c70 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -28,7 +28,6 @@ namespace CRD.Downloader.Crunchyroll; public class CrunchyrollManager{ public CrToken? Token; - // public CrCmsToken? CmsToken; public CrProfile Profile = new(); private readonly Lazy _optionsLazy; @@ -44,6 +43,8 @@ public class CrunchyrollManager{ #endregion + public CrBrowseSeriesBase? AllCRSeries; + public string DefaultLocale = "en-US"; @@ -170,9 +171,9 @@ public class CrunchyrollManager{ } else{ HistoryList =[]; } - - SonarrClient.Instance.RefreshSonarr(); } + + SonarrClient.Instance.RefreshSonarr(); } } @@ -215,27 +216,55 @@ public class CrunchyrollManager{ }; QueueManager.Instance.Queue.Refresh(); + + if (CrunOptions is{ DlVideoOnce: false, KeepDubsSeperate: true }){ + var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data); - await MuxStreams(res.Data, - new CrunchyMuxOptions{ - FfmpegOptions = options.FfmpegOptions, - SkipSubMux = options.SkipSubsMux, - Output = res.FileName, - Mp4 = options.Mp4, - VideoTitle = res.VideoTitle, - Novids = options.Novids, - NoCleanup = options.Nocleanup, - DefaultAudio = Languages.FindLang(options.DefaultAudio), - DefaultSub = Languages.FindLang(options.DefaultSub), - MkvmergeOptions = options.MkvmergeOptions, - ForceMuxer = options.Force, - SyncTiming = options.SyncTiming, - CcTag = options.CcTag, - KeepAllVideos = true, - MuxDescription = options.IncludeVideoDescription - }, - res.FileName); - + foreach (var keyValue in groupByDub){ + await MuxStreams(keyValue.Value, + new CrunchyMuxOptions{ + FfmpegOptions = options.FfmpegOptions, + SkipSubMux = options.SkipSubsMux, + Output = res.FileName, + Mp4 = options.Mp4, + VideoTitle = res.VideoTitle, + Novids = options.Novids, + NoCleanup = options.Nocleanup, + DefaultAudio = Languages.FindLang(options.DefaultAudio), + DefaultSub = Languages.FindLang(options.DefaultSub), + MkvmergeOptions = options.MkvmergeOptions, + ForceMuxer = options.Force, + SyncTiming = options.SyncTiming, + CcTag = options.CcTag, + KeepAllVideos = true, + MuxDescription = options.IncludeVideoDescription + }, + res.FileName); + } + + + } else{ + await MuxStreams(res.Data, + new CrunchyMuxOptions{ + FfmpegOptions = options.FfmpegOptions, + SkipSubMux = options.SkipSubsMux, + Output = res.FileName, + Mp4 = options.Mp4, + VideoTitle = res.VideoTitle, + Novids = options.Novids, + NoCleanup = options.Nocleanup, + DefaultAudio = Languages.FindLang(options.DefaultAudio), + DefaultSub = Languages.FindLang(options.DefaultSub), + MkvmergeOptions = options.MkvmergeOptions, + ForceMuxer = options.Force, + SyncTiming = options.SyncTiming, + CcTag = options.CcTag, + KeepAllVideos = true, + MuxDescription = options.IncludeVideoDescription + }, + res.FileName); + } + data.DownloadProgress = new DownloadProgress(){ IsDownloading = true, Done = true, diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index da49666..60b6b4f 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -544,6 +544,8 @@ public class History(){ } } + private static readonly object _lock = new object(); + public async Task MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){ if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){ return; @@ -574,9 +576,13 @@ public class History(){ historyEpisode.SonarrHasFile = episode.HasFile; historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; - episodes.Remove(episode); + lock (_lock) { + episodes.Remove(episode); + } } else{ - failedEpisodes.Add(historyEpisode); + lock (_lock) { + failedEpisodes.Add(historyEpisode); + } } } }); @@ -598,7 +604,9 @@ public class History(){ historyEpisode.SonarrHasFile = episode.HasFile; historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; - episodes.Remove(episode); + lock (_lock) { + episodes.Remove(episode); + } } else{ var episode1 = episodes.Find(ele => { if (ele == null){ @@ -614,7 +622,9 @@ public class History(){ historyEpisode.SonarrHasFile = episode1.HasFile; historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + ""; - episodes.Remove(episode1); + lock (_lock) { + episodes.Remove(episode1); + } } else{ var episode2 = episodes.Find(ele => { if (ele == null){ @@ -629,7 +639,9 @@ public class History(){ historyEpisode.SonarrHasFile = episode2.HasFile; historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + ""; - episodes.Remove(episode2); + lock (_lock) { + episodes.Remove(episode2); + } } else{ Console.Error.WriteLine($"Could not match episode {historyEpisode.EpisodeTitle} to sonarr episode"); } @@ -700,6 +712,27 @@ public class History(){ return highestSimilarity < 0.8 ? null : closestMatch; } + + public CrBrowseSeries? FindClosestMatchCrSeries(List episodeList, string title){ + CrBrowseSeries? closestMatch = null; + double highestSimilarity = 0.0; + object lockObject = new object(); // To synchronize access to shared variables + + Parallel.ForEach(episodeList, episode => { + if (episode != null){ + double similarity = CalculateSimilarity(episode.Title, title); + lock (lockObject) // Ensure thread-safe access to shared variables + { + if (similarity > highestSimilarity){ + highestSimilarity = similarity; + closestMatch = episode; + } + } + } + }); + + return highestSimilarity < 0.8 ? null : closestMatch; + } private double CalculateSimilarity(string source, string target){ int distance = LevenshteinDistance(source, target); diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index b062cd2..4b5695d 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -191,6 +191,17 @@ public enum SortingType{ HistorySeriesAddDate, } +public enum FilterType{ + [EnumMember(Value = "All")] + All, + [EnumMember(Value = "Missing Episodes")] + MissingEpisodes, + [EnumMember(Value = "Missing Episodes Sonarr")] + MissingEpisodesSonarr, + [EnumMember(Value = "Continuing Only")] + ContinuingOnly, +} + public enum SonarrCoverType{ Banner, FanArt, diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 237b6ec..b18f389 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -8,6 +8,7 @@ using System.Runtime.Serialization; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Media.Imaging; +using CRD.Utils.Structs; using Newtonsoft.Json; namespace CRD.Utils; @@ -157,7 +158,7 @@ public class Helpers{ var title = chapters[i].Title; var endTime = (i + 1 < chapters.Count) ? chapters[i + 1].StartTime : startTime + 10000; // Add 10 seconds to the last chapter end time - if (endTime < startTime) { + if (endTime < startTime){ endTime = startTime + 10000; // Correct end time if it is before start time } @@ -210,12 +211,12 @@ public class Helpers{ return (IsOk: false, ErrorCode: -1); } } - + public static void DeleteFile(string filePath){ if (string.IsNullOrEmpty(filePath)){ return; } - + try{ if (File.Exists(filePath)){ File.Delete(filePath); @@ -225,8 +226,8 @@ public class Helpers{ // Handle exceptions if you need to log them or throw } } - - public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command,string workingDir){ + + public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command, string workingDir){ try{ using (var process = new Process()){ process.StartInfo.WorkingDirectory = workingDir; @@ -345,8 +346,8 @@ public class Helpers{ return null; } } - - + + public static async Task LoadImage(string imageUrl){ try{ using (var client = new HttpClient()){ @@ -363,5 +364,18 @@ public class Helpers{ return null; } - + + public static Dictionary> GroupByLanguageWithSubtitles(List allMedia){ + var languageGroups = allMedia + .GroupBy(media => { + if (media.Type == DownloadMediaType.Subtitle && media.RelatedVideoDownloadMedia != null){ + return media.RelatedVideoDownloadMedia.Lang.CrLocale; + } + + return media.Lang.CrLocale; + }) + .ToDictionary(group => group.Key, group => group.ToList()); + + return languageGroups; + } } \ No newline at end of file diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 212a656..4b4a021 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -82,9 +82,9 @@ public class Merger{ args.Add($"-i \"{sub.value.File}\""); metaData.Add($"-map {index}:s"); if (options.Defaults.Sub.Code == sub.value.Language.Code && CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns == sub.value.Signs && sub.value.ClosedCaption == false){ - args.Add($"-disposition:s:{sub.i} default"); + metaData.Add($"-disposition:s:{sub.i} default"); } else{ - args.Add($"-disposition:s:{sub.i} 0"); + metaData.Add($"-disposition:s:{sub.i} 0"); } index++; } diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/CrDownloadOptions.cs index c49ad2e..04422f5 100644 --- a/CRD/Utils/Structs/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/CrDownloadOptions.cs @@ -110,6 +110,9 @@ public class CrDownloadOptions{ [YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)] public bool DlVideoOnce{ get; set; } + + [YamlMember(Alias = "keep_dubs_seperate", ApplyNamingConventions = false)] + public bool KeepDubsSeperate{ get; set; } [YamlIgnore] public bool? Skipmux{ get; set; } @@ -159,6 +162,9 @@ public class CrDownloadOptions{ [YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)] public bool HistoryAddSpecials{ get; set; } + [YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)] + public bool HistoryCountSonarr{ get; set; } + [YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)] public SonarrProperties? SonarrProperties{ get; set; } diff --git a/CRD/Utils/Structs/CrProfile.cs b/CRD/Utils/Structs/CrProfile.cs index 50bca00..1494119 100644 --- a/CRD/Utils/Structs/CrProfile.cs +++ b/CRD/Utils/Structs/CrProfile.cs @@ -46,6 +46,9 @@ public class Subscription{ [JsonProperty("nonrecurring_subscription_products")] public List? NonrecurringSubscriptionProducts{ get; set; } + + [JsonProperty("funimation_subscriptions")] + public List? FunimationSubscriptions{ get; set; } } public class NonRecurringSubscriptionProduct{ diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs index 3d8a315..09fbdcd 100644 --- a/CRD/Utils/Structs/History/HistorySeries.cs +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -63,6 +63,9 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonIgnore] public Bitmap? ThumbnailImage{ get; set; } + [JsonIgnore] + public bool IsImageLoaded{ get; private set; } = false; + [JsonIgnore] public bool FetchingData{ get; set; } @@ -168,17 +171,19 @@ public class HistorySeries : INotifyPropertyChanged{ #endregion public async Task LoadImage(){ + if (IsImageLoaded || string.IsNullOrEmpty(ThumbnailImageUrl)) + return; + try{ - using (var client = new HttpClient()){ - var response = await client.GetAsync(ThumbnailImageUrl); - response.EnsureSuccessStatusCode(); - using (var stream = await response.Content.ReadAsStreamAsync()){ - ThumbnailImage = new Bitmap(stream); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage))); - } - } + using var client = new HttpClient(); + var response = await client.GetAsync(ThumbnailImageUrl); + response.EnsureSuccessStatusCode(); + using var stream = await response.Content.ReadAsStreamAsync(); + ThumbnailImage = new Bitmap(stream); + IsImageLoaded = true; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage))); } catch (Exception ex){ - // Handle exceptions Console.Error.WriteLine("Failed to load image: " + ex.Message); } } @@ -187,50 +192,67 @@ public class HistorySeries : INotifyPropertyChanged{ int count = 0; bool foundWatched = false; var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; + var sonarrEnabled = CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled; - for (int i = Seasons.Count - 1; i >= 0; i--){ - var season = Seasons[i]; + if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr && !string.IsNullOrEmpty(SonarrSeriesId)){ + for (int i = Seasons.Count - 1; i >= 0; i--){ + var season = Seasons[i]; - if (season.SpecialSeason == true){ - if (historyAddSpecials){ - var episodes = season.EpisodesList; - for (int j = episodes.Count - 1; j >= 0; j--){ - if (!episodes[j].WasDownloaded){ - count++; - } + var episodesList = season.EpisodesList; + for (int j = episodesList.Count - 1; j >= 0; j--){ + var episode = episodesList[j]; + + if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){ + count++; } } - - continue; } + } else{ + for (int i = Seasons.Count - 1; i >= 0; i--){ + var season = Seasons[i]; - var episodesList = season.EpisodesList; - for (int j = episodesList.Count - 1; j >= 0; j--){ - var episode = episodesList[j]; - - if (episode.SpecialEpisode){ - if (historyAddSpecials && !episode.WasDownloaded){ - count++; + if (season.SpecialSeason == true){ + if (historyAddSpecials){ + var episodes = season.EpisodesList; + for (int j = episodes.Count - 1; j >= 0; j--){ + if (!episodes[j].WasDownloaded){ + count++; + } + } } continue; } - if (!episode.WasDownloaded && !foundWatched){ - count++; - } else{ - foundWatched = true; - if (!historyAddSpecials){ - break; + var episodesList = season.EpisodesList; + for (int j = episodesList.Count - 1; j >= 0; j--){ + var episode = episodesList[j]; + + if (episode.SpecialEpisode){ + if (historyAddSpecials && !episode.WasDownloaded){ + count++; + } + + continue; + } + + if (!episode.WasDownloaded && !foundWatched){ + count++; + } else{ + foundWatched = true; + if (!historyAddSpecials){ + break; + } } } - } - if (foundWatched && !historyAddSpecials){ - break; + if (foundWatched && !historyAddSpecials){ + break; + } } } + NewEpisodes = count; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); } diff --git a/CRD/ViewModels/AccountPageViewModel.cs b/CRD/ViewModels/AccountPageViewModel.cs index 8099f78..b131e5f 100644 --- a/CRD/ViewModels/AccountPageViewModel.cs +++ b/CRD/ViewModels/AccountPageViewModel.cs @@ -31,6 +31,7 @@ public partial class AccountPageViewModel : ViewModelBase{ private static DispatcherTimer? _timer; private DateTime _targetTime; private bool IsCancelled = false; + private bool UnknownEndDate = false; public AccountPageViewModel(){ UpdatetProfile(); @@ -68,6 +69,9 @@ public partial class AccountPageViewModel : ViewModelBase{ } }else if(CrunchyrollManager.Instance.Profile.Subscription?.NonrecurringSubscriptionProducts.Count >= 1){ IsCancelled = true; + }else if(CrunchyrollManager.Instance.Profile.Subscription?.FunimationSubscriptions.Count >= 1){ + IsCancelled = true; + UnknownEndDate = true; } if (CrunchyrollManager.Instance.Profile.Subscription?.NextRenewalDate != null){ @@ -92,6 +96,10 @@ public partial class AccountPageViewModel : ViewModelBase{ } } + + if (UnknownEndDate){ + RemainingTime = "Unknown Subscription end date"; + } } [RelayCommand] diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs index e4f7872..3a1f2ca 100644 --- a/CRD/ViewModels/HistoryPageViewModel.cs +++ b/CRD/ViewModels/HistoryPageViewModel.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; @@ -14,85 +16,116 @@ using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; +using CRD.Utils.Sonarr; using CRD.Utils.Structs; using CRD.Utils.Structs.History; using CRD.Views; using DynamicData; +using HarfBuzzSharp; using ReactiveUI; namespace CRD.ViewModels; public partial class HistoryPageViewModel : ViewModelBase{ public ObservableCollection Items{ get; } + public ObservableCollection FilteredItems{ get; } [ObservableProperty] private static bool _fetchingData; [ObservableProperty] - public HistorySeries _selectedSeries; + private HistorySeries _selectedSeries; [ObservableProperty] - public static bool _editMode; + private static bool _editMode; [ObservableProperty] - public double _scaleValue; + private double _scaleValue; [ObservableProperty] - public ComboBoxItem _selectedView; + private ComboBoxItem? _selectedView; public ObservableCollection ViewsList{ get; } =[]; [ObservableProperty] - public SortingListElement _selectedSorting; + private SortingListElement? _selectedSorting; public ObservableCollection SortingList{ get; } =[]; [ObservableProperty] - public double _posterWidth; + private FilterListElement? _selectedFilter; + + public ObservableCollection FilterList{ get; } =[]; [ObservableProperty] - public double _posterHeight; + private double _posterWidth; [ObservableProperty] - public double _posterImageWidth; + private double _posterHeight; [ObservableProperty] - public double _posterImageHeight; + private double _posterImageWidth; [ObservableProperty] - public double _posterTextSize; + private double _posterImageHeight; [ObservableProperty] - public Thickness _cornerMargin; - - private HistoryViewType currentViewType = HistoryViewType.Posters; + private double _posterTextSize; [ObservableProperty] - public bool _isPosterViewSelected = false; + private Thickness _cornerMargin; + [ObservableProperty] - public bool _isTableViewSelected = false; + private bool _isPosterViewSelected = false; [ObservableProperty] - public static bool _viewSelectionOpen; + private bool _isTableViewSelected = false; [ObservableProperty] - public static bool _sortingSelectionOpen; + private static bool _viewSelectionOpen; + + [ObservableProperty] + private static bool _sortingSelectionOpen; + + [ObservableProperty] + private static bool _addingMissingSonarrSeries; + + [ObservableProperty] + private static bool _sonarrOptionsOpen; private IStorageProvider _storageProvider; - private SortingType currentSortingType = SortingType.NextAirDate; + private HistoryViewType currentViewType; + + private SortingType currentSortingType; + + private FilterType currentFilterType; [ObservableProperty] - public static bool _sortDir = false; + private static bool _sortDir = false; + + [ObservableProperty] + private static bool _sonarrAvailable; + [ObservableProperty] + private static string _progressText; + public HistoryPageViewModel(){ + if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){ + SonarrAvailable = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled; + } else{ + SonarrAvailable = false; + } + Items = CrunchyrollManager.Instance.HistoryList; + FilteredItems = new ObservableCollection(); HistoryPageProperties? properties = CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties; currentViewType = properties?.SelectedView ?? HistoryViewType.Posters; currentSortingType = properties?.SelectedSorting ?? SortingType.SeriesTitle; + currentFilterType = properties?.SelectedFilter ?? FilterType.All; ScaleValue = properties?.ScaleValue ?? 0.73; SortDir = properties?.Ascending ?? false; @@ -112,6 +145,19 @@ public partial class HistoryPageViewModel : ViewModelBase{ } } + foreach (FilterType filterType in Enum.GetValues(typeof(FilterType))){ + + if (!SonarrAvailable && (filterType == FilterType.MissingEpisodesSonarr || filterType == FilterType.ContinuingOnly)){ + continue; + } + + var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType }; + FilterList.Add(item); + if (filterType == currentFilterType){ + SelectedFilter = item; + } + } + IsPosterViewSelected = currentViewType == HistoryViewType.Posters; IsTableViewSelected = currentViewType == HistoryViewType.Table; @@ -172,6 +218,10 @@ public partial class HistoryPageViewModel : ViewModelBase{ currentSortingType = newValue.SelectedSorting; if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType; CrunchyrollManager.Instance.History.SortItems(); + if (SelectedFilter != null){ + OnSelectedFilterChanged(SelectedFilter); + } + } else{ Console.Error.WriteLine("Invalid viewtype selected"); } @@ -180,17 +230,46 @@ public partial class HistoryPageViewModel : ViewModelBase{ UpdateSettings(); } - private bool TryParseEnum(string value, out T result) where T : struct, Enum{ - foreach (var field in typeof(T).GetFields()){ - var attribute = field.GetCustomAttribute(); - if (attribute != null && attribute.Value == value){ - result = (T)field.GetValue(null); - return true; - } - } - result = default; - return false; + partial void OnSelectedFilterChanged(FilterListElement? value){ + + if (value == null){ + return; + } + + currentFilterType = value.SelectedType; + if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType; + + + switch (currentFilterType){ + case FilterType.All: + FilteredItems.Clear(); + FilteredItems.AddRange(Items); + break; + case FilterType.MissingEpisodes: + List filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList(); + FilteredItems.Clear(); + FilteredItems.AddRange(filteredItems); + break; + case FilterType.MissingEpisodesSonarr: + + var missingSonarrFiltered = Items.Where(historySeries => + !string.IsNullOrEmpty(historySeries.SonarrSeriesId) && // Check series ID + historySeries.Seasons.Any(season => // Check each season + season.EpisodesList.Any(historyEpisode => // Check each episode + !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile))) // Filter condition + .ToList(); + + FilteredItems.Clear(); + FilteredItems.AddRange(missingSonarrFiltered); + + break; + case FilterType.ContinuingOnly: + List continuingFiltered = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList(); + FilteredItems.Clear(); + FilteredItems.AddRange(continuingFiltered); + break; + } } @@ -230,6 +309,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ if (objectToRemove != null){ CrunchyrollManager.Instance.HistoryList.Remove(objectToRemove); Items.Remove(objectToRemove); + FilteredItems.Remove(objectToRemove); CfgManager.UpdateHistoryFile(); } } @@ -245,18 +325,18 @@ public partial class HistoryPageViewModel : ViewModelBase{ } [RelayCommand] - public async void RefreshAll(){ + public async Task RefreshAll(){ FetchingData = true; RaisePropertyChanged(nameof(FetchingData)); - for (int i = 0; i < Items.Count; i++){ - Items[i].SetFetchingData(); + foreach (var item in FilteredItems){ + item.SetFetchingData(); } - for (int i = 0; i < Items.Count; i++){ + for (int i = 0; i < FilteredItems.Count; i++){ FetchingData = true; RaisePropertyChanged(nameof(FetchingData)); - await Items[i].FetchData(""); - Items[i].UpdateNewEpisodes(); + await FilteredItems[i].FetchData(""); + FilteredItems[i].UpdateNewEpisodes(); } FetchingData = false; @@ -266,8 +346,76 @@ public partial class HistoryPageViewModel : ViewModelBase{ [RelayCommand] public async void AddMissingToQueue(){ - for (int i = 0; i < Items.Count; i++){ - await Items[i].AddNewMissingToDownloads(); + var tasks = FilteredItems + .Select(item => item.AddNewMissingToDownloads()); + + await Task.WhenAll(tasks); + } + + [RelayCommand] + public async Task DownloadMissingSonarr(){ + await Task.WhenAll( + FilteredItems.Where(series => !string.IsNullOrEmpty(series.SonarrSeriesId)) + .SelectMany(item => item.Seasons) + .SelectMany(season => season.EpisodesList) + .Where(historyEpisode => !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile) + .Select(historyEpisode => historyEpisode.DownloadEpisode()) + ); + } + + [RelayCommand] + public async Task AddMissingSonarrSeriesToHistory(){ + SonarrOptionsOpen = false; + AddingMissingSonarrSeries = true; + FetchingData = true; + + var crInstance = CrunchyrollManager.Instance; + + if (crInstance.AllCRSeries == null){ + crInstance.AllCRSeries = await crInstance.CrSeries.GetAllSeries(string.IsNullOrEmpty(crInstance.CrunOptions.HistoryLang) ? crInstance.DefaultLocale : crInstance.CrunOptions.HistoryLang); + } + + if (crInstance.AllCRSeries?.Data is{ Count: > 0 }){ + var concurrentSeriesIds = new ConcurrentBag(); + + Parallel.ForEach(SonarrClient.Instance.SonarrSeries, series => { + if (crInstance.HistoryList.All(historySeries => historySeries.SonarrSeriesId != series.Id.ToString())){ + var match = crInstance.History.FindClosestMatchCrSeries(crInstance.AllCRSeries.Data, series.Title); + + if (match != null){ + Console.WriteLine($"[Sonarr Match] Found match with {series.Title} and CR - {match.Title}"); + if (!string.IsNullOrEmpty(match.Id)){ + concurrentSeriesIds.Add(match.Id); + } else{ + Console.Error.WriteLine($"[Sonarr Match] Series ID empty for {series.Title}"); + } + } else{ + Console.Error.WriteLine($"[Sonarr Match] Could not match {series.Title}"); + } + } else{ + Console.Error.WriteLine($"[Sonarr Match] {series.Title} already matched"); + } + }); + + var seriesIds = concurrentSeriesIds.ToList(); + var totalSeries = seriesIds.Count; + + for (int count = 0; count < totalSeries; count++){ + ProgressText = $"{count + 1}/{totalSeries}"; + + // Await the CRUpdateSeries task for each seriesId + await crInstance.History.CRUpdateSeries(seriesIds[count], ""); + } + + // var updateTasks = seriesIds.Select(seriesId => crInstance.History.CRUpdateSeries(seriesId, "")); + // await Task.WhenAll(updateTasks); + } + + ProgressText = ""; + AddingMissingSonarrSeries = false; + FetchingData = false; + if (SelectedFilter != null){ + OnSelectedFilterChanged(SelectedFilter); } } @@ -292,7 +440,6 @@ public partial class HistoryPageViewModel : ViewModelBase{ season.SeasonDownloadPath = selectedFolder.Path.LocalPath; CfgManager.UpdateHistoryFile(); } - } } @@ -319,26 +466,31 @@ public partial class HistoryPageViewModel : ViewModelBase{ } } } - + [RelayCommand] public async Task DownloadSeasonAll(HistorySeason season){ - foreach (var historyEpisode in season.EpisodesList){ - await historyEpisode.DownloadEpisode(); - } + var downloadTasks = season.EpisodesList + .Select(episode => episode.DownloadEpisode()); + + await Task.WhenAll(downloadTasks); } - + [RelayCommand] public async Task DownloadSeasonMissing(HistorySeason season){ - foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.WasDownloaded)){ - await historyEpisode.DownloadEpisode(); - } + var downloadTasks = season.EpisodesList + .Where(episode => !episode.WasDownloaded) + .Select(episode => episode.DownloadEpisode()); + + await Task.WhenAll(downloadTasks); } - + [RelayCommand] public async Task DownloadSeasonMissingSonarr(HistorySeason season){ - foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.SonarrHasFile)){ - await historyEpisode.DownloadEpisode(); - } + var downloadTasks = season.EpisodesList + .Where(episode => !episode.SonarrHasFile) + .Select(episode => episode.DownloadEpisode()); + + await Task.WhenAll(downloadTasks); } @@ -350,6 +502,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ public class HistoryPageProperties(){ public SortingType? SelectedSorting{ get; set; } public HistoryViewType SelectedView{ get; set; } + public FilterType SelectedFilter{ get; set; } public double? ScaleValue{ get; set; } public bool Ascending{ get; set; } @@ -358,4 +511,9 @@ public class HistoryPageProperties(){ public class SortingListElement(){ public SortingType SelectedSorting{ get; set; } public string? SortingTitle{ get; set; } +} + +public class FilterListElement(){ + public FilterType SelectedType{ get; set; } + public string? FilterTitle{ get; set; } } \ No newline at end of file diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index cea88f2..e3cc3f1 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -82,23 +82,28 @@ public partial class SeriesPageViewModel : ViewModelBase{ [RelayCommand] public async Task DownloadSeasonAll(HistorySeason season){ - foreach (var historyEpisode in season.EpisodesList){ - await historyEpisode.DownloadEpisode(); - } + var downloadTasks = season.EpisodesList + .Select(episode => episode.DownloadEpisode()); + + await Task.WhenAll(downloadTasks); } - + [RelayCommand] public async Task DownloadSeasonMissing(HistorySeason season){ - foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.WasDownloaded)){ - await historyEpisode.DownloadEpisode(); - } + var downloadTasks = season.EpisodesList + .Where(episode => !episode.WasDownloaded) + .Select(episode => episode.DownloadEpisode()); + + await Task.WhenAll(downloadTasks); } - + [RelayCommand] public async Task DownloadSeasonMissingSonarr(HistorySeason season){ - foreach (var historyEpisode in season.EpisodesList.Where(historyEpisode => !historyEpisode.SonarrHasFile)){ - await historyEpisode.DownloadEpisode(); - } + var downloadTasks = season.EpisodesList + .Where(episode => !episode.SonarrHasFile) + .Select(episode => episode.DownloadEpisode()); + + await Task.WhenAll(downloadTasks); } [RelayCommand] diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index d4e0b20..ae4fc53 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -68,6 +68,9 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _downloadVideoForEveryDub; + + [ObservableProperty] + private bool _keepDubsSeparate; [ObservableProperty] private bool _skipSubMux; @@ -78,6 +81,9 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _historyAddSpecials; + [ObservableProperty] + private bool _historyCountSonarr; + [ObservableProperty] private double? _leadingNumbers; @@ -388,6 +394,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ DefaultSubForcedDisplay = options.DefaultSubForcedDisplay; DefaultSubSigns = options.DefaultSubSigns; HistoryAddSpecials = options.HistoryAddSpecials; + HistoryCountSonarr = options.HistoryCountSonarr; DownloadSpeed = options.DownloadSpeedLimit; IncludeEpisodeDescription = options.IncludeVideoDescription; FileTitle = options.VideoTitle ?? ""; @@ -395,6 +402,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ DownloadVideo = !options.Novids; DownloadAudio = !options.Noaudio; DownloadVideoForEveryDub = !options.DlVideoOnce; + KeepDubsSeparate = options.KeepDubsSeperate; DownloadChapters = options.Chapters; MuxToMp4 = options.Mp4; SyncTimings = options.SyncTiming; @@ -457,10 +465,12 @@ public partial class SettingsPageViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay = DefaultSubForcedDisplay; CrunchyrollManager.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription; CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials; + CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr; CrunchyrollManager.Instance.CrunOptions.VideoTitle = FileTitle; CrunchyrollManager.Instance.CrunOptions.Novids = !DownloadVideo; CrunchyrollManager.Instance.CrunOptions.Noaudio = !DownloadAudio; CrunchyrollManager.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub; + CrunchyrollManager.Instance.CrunOptions.KeepDubsSeperate = KeepDubsSeparate; CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters; CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4; CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings; diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml index cf970f0..f1ddb25 100644 --- a/CRD/Views/HistoryPageView.axaml +++ b/CRD/Views/HistoryPageView.axaml @@ -7,6 +7,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:history="clr-namespace:CRD.Utils.Structs.History" xmlns:structs="clr-namespace:CRD.Utils.Structs" + xmlns:local="clr-namespace:CRD.Utils" x:DataType="vm:HistoryPageViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="CRD.Views.HistoryPageView"> @@ -63,6 +64,46 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -131,18 +172,38 @@ - + + + + + + + + + + + + + + + + + + + + + - @@ -235,12 +296,13 @@ - - - + + + + + + - + @@ -529,7 +593,6 @@ Width="25" Height="25" /> - @@ -819,11 +882,9 @@ - - - - + + diff --git a/CRD/Views/SettingsPageView.axaml b/CRD/Views/SettingsPageView.axaml index abee0c6..ab2b05a 100644 --- a/CRD/Views/SettingsPageView.axaml +++ b/CRD/Views/SettingsPageView.axaml @@ -149,6 +149,12 @@ + + + + + + @@ -213,7 +219,11 @@ - + + + + +