From 93244a749f4c0f879ccdaeff58bdf0db6b9d455c Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Wed, 29 Jan 2025 19:57:20 +0100 Subject: [PATCH] Add - Added **Skip Unmonitored Sonarr Episodes** option to settings Add - Added **Include Crunchyroll Artists (Music)** in history to settings for expanded tracking Add - Added **filters to history tab** to hide series or artists for a cleaner view Add - Added a **toggle to include featured music** in series search results Chg - Made small changes to **sync timing** to more accurately detect delays Chg - Migrated settings to json file Fix - Fixed a **sync timing issue** with longer comparison videos to ensure proper synchronization Fix - Fixed issues with artist urls --- CRD/Downloader/CalendarManager.cs | 118 ++--- CRD/Downloader/Crunchyroll/CRAuth.cs | 52 +- CRD/Downloader/Crunchyroll/CrEpisode.cs | 44 +- CRD/Downloader/Crunchyroll/CrMovies.cs | 6 +- CRD/Downloader/Crunchyroll/CrMusic.cs | 114 ++++- CRD/Downloader/Crunchyroll/CrSeries.cs | 128 +++-- .../Crunchyroll/CrunchyrollManager.cs | 196 ++++++-- .../CrunchyrollSettingsViewModel.cs | 175 +++---- .../Views/CrunchyrollSettingsView.axaml | 16 + CRD/Downloader/History.cs | 453 +++++++++--------- CRD/Downloader/ProgramManager.cs | 10 +- CRD/Downloader/QueueManager.cs | 29 +- CRD/Utils/DRM/Session.cs | 2 +- CRD/Utils/DRM/Widevine.cs | 1 + CRD/Utils/Enums/EnumCollection.cs | 18 + CRD/Utils/Files/CfgManager.cs | 227 +++++---- CRD/Utils/HLS/HLSDownloader.cs | 4 +- CRD/Utils/Helpers.cs | 93 ++++ CRD/Utils/Http/HttpClientReq.cs | 22 +- CRD/Utils/Muxing/FontsManager.cs | 1 + CRD/Utils/Muxing/Merger.cs | 14 +- CRD/Utils/Muxing/SyncingHelper.cs | 35 +- CRD/Utils/Parser/MPDTransformer.cs | 2 +- CRD/Utils/Structs/Chapters.cs | 6 +- .../Structs/Crunchyroll/CrDownloadOptions.cs | 304 +++++++++++- CRD/Utils/Structs/Crunchyroll/CrMovie.cs | 2 +- .../Crunchyroll/Episode/CrBrowseEpisode.cs | 2 +- .../Crunchyroll/Episode/EpisodeStructs.cs | 145 +++++- .../Structs/Crunchyroll/Music/CrArtist.cs | 35 ++ .../Structs/Crunchyroll/Music/CrMusicVideo.cs | 167 +++++-- .../Crunchyroll/Series/CrSeriesBase.cs | 2 +- .../Crunchyroll/Series/CrSeriesSearch.cs | 4 +- .../Structs/{Structs.cs => HelperClasses.cs} | 18 +- CRD/Utils/Structs/History/HistoryEpisode.cs | 27 +- CRD/Utils/Structs/History/HistorySeason.cs | 1 + CRD/Utils/Structs/History/HistorySeries.cs | 210 ++++++-- CRD/Utils/Structs/History/IHistorySource.cs | 30 ++ CRD/Utils/Structs/History/SeriesDataCache.cs | 17 + CRD/Utils/Structs/Languages.cs | 8 +- CRD/Utils/UI/UiListHasElementsConverter.cs | 26 + CRD/Utils/Updater/Updater.cs | 1 + CRD/ViewModels/AccountPageViewModel.cs | 10 +- CRD/ViewModels/AddDownloadPageViewModel.cs | 147 ++++-- CRD/ViewModels/CalendarPageViewModel.cs | 33 +- CRD/ViewModels/DownloadsPageViewModel.cs | 35 +- CRD/ViewModels/HistoryPageViewModel.cs | 131 +++-- CRD/ViewModels/SeriesPageViewModel.cs | 30 +- CRD/ViewModels/SettingsPageViewModel.cs | 5 +- .../UpcomingSeasonsPageViewModel.cs | 26 +- .../ContentDialogEncodingPresetViewModel.cs | 1 + .../Utils/GeneralSettingsViewModel.cs | 26 +- CRD/Views/AddDownloadPageView.axaml | 1 + CRD/Views/HistoryPageView.axaml | 47 +- CRD/Views/SeriesPageView.axaml | 43 +- CRD/Views/SeriesPageView.axaml.cs | 1 - CRD/Views/UpcomingSeasonsPageView.axaml.cs | 6 +- CRD/Views/Utils/GeneralSettingsView.axaml | 14 +- 57 files changed, 2311 insertions(+), 1010 deletions(-) create mode 100644 CRD/Utils/Structs/Crunchyroll/Music/CrArtist.cs rename CRD/Utils/Structs/{Structs.cs => HelperClasses.cs} (90%) create mode 100644 CRD/Utils/Structs/History/IHistorySource.cs create mode 100644 CRD/Utils/Structs/History/SeriesDataCache.cs create mode 100644 CRD/Utils/UI/UiListHasElementsConverter.cs diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs index 8ecf115..37dfbf5 100644 --- a/CRD/Downloader/CalendarManager.cs +++ b/CRD/Downloader/CalendarManager.cs @@ -11,7 +11,6 @@ using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.Structs; using CRD.Utils.Structs.History; -using DynamicData; using HtmlAgilityPack; using Newtonsoft.Json; @@ -90,56 +89,60 @@ public class CalendarManager{ foreach (var day in dayNodes){ // Extract the date and day name var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date"); - DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind); + if (date != null){ + DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind); - if (week.FirstDayOfWeek == DateTime.MinValue){ - week.FirstDayOfWeek = dayDateTime; - week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd"); - } - - var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim(); - - CalendarDay calDay = new CalendarDay(); - - calDay.CalendarEpisodes = new List(); - calDay.DayName = dayName; - calDay.DateTime = dayDateTime; - - // Iterate through each episode listed under this day - var episodes = day.SelectNodes(".//article[contains(@class, 'release')]"); - if (episodes != null){ - foreach (var episode in episodes){ - var episodeTimeStr = episode.SelectSingleNode(".//time[contains(@class, 'available-time')]")?.GetAttributeValue("datetime", null); - DateTime episodeTime = DateTime.Parse(episodeTimeStr, null, DateTimeStyles.RoundtripKind); - var hasPassed = DateTime.Now > episodeTime; - - var episodeName = episode.SelectSingleNode(".//h1[contains(@class, 'episode-name')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim(); - var seasonLink = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.GetAttributeValue("href", "No link"); - var episodeLink = episode.SelectSingleNode(".//a[contains(@class, 'available-episode-link')]")?.GetAttributeValue("href", "No link"); - var thumbnailUrl = episode.SelectSingleNode(".//img[contains(@class, 'thumbnail')]")?.GetAttributeValue("src", "No image"); - var isPremiumOnly = episode.SelectSingleNode(".//svg[contains(@class, 'premium-flag')]") != null; - var isPremiere = episode.SelectSingleNode(".//div[contains(@class, 'premiere-flag')]") != null; - var seasonName = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim(); - var episodeNumber = episode.SelectSingleNode(".//meta[contains(@itemprop, 'episodeNumber')]")?.GetAttributeValue("content", "?"); - - CalendarEpisode calEpisode = new CalendarEpisode(); - - calEpisode.DateTime = episodeTime; - calEpisode.HasPassed = hasPassed; - calEpisode.EpisodeName = episodeName; - calEpisode.SeriesUrl = seasonLink; - calEpisode.EpisodeUrl = episodeLink; - calEpisode.ThumbnailUrl = thumbnailUrl; - calEpisode.IsPremiumOnly = isPremiumOnly; - calEpisode.IsPremiere = isPremiere; - calEpisode.SeasonName = seasonName; - calEpisode.EpisodeNumber = episodeNumber; - - calDay.CalendarEpisodes.Add(calEpisode); + if (week.FirstDayOfWeek == DateTime.MinValue){ + week.FirstDayOfWeek = dayDateTime; + week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd"); } - } - week.CalendarDays.Add(calDay); + var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim(); + + CalendarDay calDay = new CalendarDay(); + + calDay.CalendarEpisodes = new List(); + calDay.DayName = dayName; + calDay.DateTime = dayDateTime; + + // Iterate through each episode listed under this day + var episodes = day.SelectNodes(".//article[contains(@class, 'release')]"); + if (episodes != null){ + foreach (var episode in episodes){ + var episodeTimeStr = episode.SelectSingleNode(".//time[contains(@class, 'available-time')]")?.GetAttributeValue("datetime", null); + if (episodeTimeStr != null){ + DateTime episodeTime = DateTime.Parse(episodeTimeStr, null, DateTimeStyles.RoundtripKind); + var hasPassed = DateTime.Now > episodeTime; + + var episodeName = episode.SelectSingleNode(".//h1[contains(@class, 'episode-name')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim(); + var seasonLink = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.GetAttributeValue("href", "No link"); + var episodeLink = episode.SelectSingleNode(".//a[contains(@class, 'available-episode-link')]")?.GetAttributeValue("href", "No link"); + var thumbnailUrl = episode.SelectSingleNode(".//img[contains(@class, 'thumbnail')]")?.GetAttributeValue("src", "No image"); + var isPremiumOnly = episode.SelectSingleNode(".//svg[contains(@class, 'premium-flag')]") != null; + var isPremiere = episode.SelectSingleNode(".//div[contains(@class, 'premiere-flag')]") != null; + var seasonName = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim(); + var episodeNumber = episode.SelectSingleNode(".//meta[contains(@itemprop, 'episodeNumber')]")?.GetAttributeValue("content", "?"); + + CalendarEpisode calEpisode = new CalendarEpisode(); + + calEpisode.DateTime = episodeTime; + calEpisode.HasPassed = hasPassed; + calEpisode.EpisodeName = episodeName; + calEpisode.SeriesUrl = seasonLink; + calEpisode.EpisodeUrl = episodeLink; + calEpisode.ThumbnailUrl = thumbnailUrl; + calEpisode.IsPremiumOnly = isPremiumOnly; + calEpisode.IsPremiere = isPremiere; + calEpisode.SeasonName = seasonName; + calEpisode.EpisodeNumber = episodeNumber; + + calDay.CalendarEpisodes.Add(calEpisode); + } + } + } + + week.CalendarDays.Add(calDay); + } } } else{ Console.Error.WriteLine("No days found in the HTML document."); @@ -260,7 +263,7 @@ public class CalendarManager{ calEpisode.EpisodeName = crBrowseEpisode.Title; calEpisode.SeriesUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId; calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/watch/{crBrowseEpisode.Id}/"; - calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault().Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg + calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault()?.Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly; calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1"; calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle; @@ -268,10 +271,10 @@ public class CalendarManager{ calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId; var existingEpisode = calendarDay.CalendarEpisodes - ?.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName); + .FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName); if (existingEpisode != null){ - if (!int.TryParse(existingEpisode.EpisodeNumber, out var num)){ + if (!int.TryParse(existingEpisode.EpisodeNumber, out _)){ existingEpisode.EpisodeNumber = "..."; } else{ var existingNumbers = existingEpisode.EpisodeNumber @@ -300,7 +303,7 @@ public class CalendarManager{ existingEpisode.CalendarEpisodes.Add(calEpisode); } else{ - calendarDay.CalendarEpisodes?.Add(calEpisode); + calendarDay.CalendarEpisodes.Add(calEpisode); } } } @@ -429,8 +432,6 @@ public class CalendarManager{ calEp.EpisodeNumber = anilistEle.Episode.ToString(); calEp.AnilistEpisode = true; - var crunchyrollID = ""; - if (anilistEle.Media?.ExternalLinks != null){ var url = anilistEle.Media.ExternalLinks.First(external => string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase)).Url; @@ -438,10 +439,11 @@ public class CalendarManager{ string pattern = @"series\/([^\/]+)"; Match match = Regex.Match(url, pattern); + string crunchyrollId; if (match.Success){ - crunchyrollID = match.Groups[1].Value; + crunchyrollId = match.Groups[1].Value; - AdjustReleaseTimeToHistory(calEp, crunchyrollID); + AdjustReleaseTimeToHistory(calEp, crunchyrollId); } else{ Uri uri = new Uri(url); @@ -462,9 +464,9 @@ public class CalendarManager{ Match match2 = Regex.Match(finalUrl ?? string.Empty, pattern); if (match2.Success){ - crunchyrollID = match2.Groups[1].Value; + crunchyrollId = match2.Groups[1].Value; - AdjustReleaseTimeToHistory(calEp, crunchyrollID); + AdjustReleaseTimeToHistory(calEp, crunchyrollId); } } } diff --git a/CRD/Downloader/Crunchyroll/CRAuth.cs b/CRD/Downloader/Crunchyroll/CRAuth.cs index 1b7db37..7d33e7a 100644 --- a/CRD/Downloader/Crunchyroll/CRAuth.cs +++ b/CRD/Downloader/Crunchyroll/CRAuth.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll; using CRD.Views; @@ -15,6 +16,11 @@ namespace CRD.Downloader.Crunchyroll; public class CrAuth{ private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; + private readonly string authorization = ApiUrls.authBasicMob; + private readonly string userAgent = ApiUrls.MobileUserAgent; + private const string DeviceType = "OnePlus CPH2449"; + private const string DeviceName = "CPH2449"; + public async Task AuthAnonymous(){ string uuid = Guid.NewGuid().ToString(); @@ -22,17 +28,18 @@ public class CrAuth{ { "grant_type", "client_id" }, { "scope", "offline_access" }, { "device_id", uuid }, - { "device_type", "Chrome on Windows" } + { "device_name", DeviceName }, + { "device_type", DeviceType }, }; var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", ApiUrls.authBasicSwitch }, - { "User-Agent", ApiUrls.ChromeUserAgent } + { "Authorization", authorization }, + { "User-Agent", userAgent } }; - var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; @@ -63,7 +70,7 @@ public class CrAuth{ crunInstance.Token.device_id = deviceId; crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in); - CfgManager.WriteTokenToYamlFile(crunInstance.Token, CfgManager.PathCrToken); + CfgManager.WriteJsonToFile(CfgManager.PathCrToken, crunInstance.Token); } } @@ -76,17 +83,18 @@ public class CrAuth{ { "grant_type", "password" }, { "scope", "offline_access" }, { "device_id", uuid }, - { "device_type", "Chrome on Windows" } + { "device_name", DeviceName }, + { "device_type", DeviceType }, }; var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", ApiUrls.authBasicSwitch }, - { "User-Agent", ApiUrls.ChromeUserAgent } + { "Authorization", authorization }, + { "User-Agent", userAgent } }; - var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; @@ -120,7 +128,7 @@ public class CrAuth{ return; } - var request = HttpClientReq.CreateRequestMessage(ApiUrls.BetaProfile, HttpMethod.Get, true, true, null); + var request = HttpClientReq.CreateRequestMessage(ApiUrls.Profile, HttpMethod.Get, true, true, null); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -183,18 +191,19 @@ public class CrAuth{ { "refresh_token", crunInstance.Token.refresh_token }, { "scope", "offline_access" }, { "device_id", uuid }, - { "device_type", "Chrome on Windows" }, - { "grant_type", "refresh_token" } + { "grant_type", "refresh_token" }, + { "device_name", DeviceName }, + { "device_type", DeviceType }, }; var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", ApiUrls.authBasicSwitch }, - { "User-Agent", ApiUrls.ChromeUserAgent } + { "Authorization", authorization }, + { "User-Agent", userAgent } }; - var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; @@ -237,21 +246,22 @@ public class CrAuth{ string uuid = Guid.NewGuid().ToString(); var formData = new Dictionary{ - { "refresh_token", crunInstance.Token.refresh_token }, + { "refresh_token", crunInstance.Token?.refresh_token ?? "" }, { "grant_type", "refresh_token" }, { "scope", "offline_access" }, { "device_id", uuid }, - { "device_type", "Chrome on Windows" } + { "device_name", DeviceName }, + { "device_type", DeviceType }, }; var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", ApiUrls.authBasicSwitch }, - { "User-Agent", ApiUrls.ChromeUserAgent } + { "Authorization", authorization }, + { "User-Agent", userAgent } }; - var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ + var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; @@ -259,7 +269,7 @@ public class CrAuth{ request.Headers.Add(header.Key, header.Value); } - HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); + HttpClientReq.Instance.SetETPCookie(crunInstance.Token?.refresh_token ?? string.Empty); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index fb067ad..1f7bb2f 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Structs; namespace CRD.Downloader.Crunchyroll; @@ -35,9 +36,9 @@ public class CrEpisode(){ return null; } - CrunchyEpisodeList epsidoe = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + CrunchyEpisodeList epsidoe = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyEpisodeList(); - if (epsidoe.Total < 1){ + if (epsidoe is{ Total: < 1 }){ return null; } @@ -59,7 +60,7 @@ public class CrEpisode(){ CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData(); if (crunInstance.CrunOptions.History && updateHistory){ - await crunInstance.History.UpdateWithSeasonData(new List(){dlEpisode},false); + await crunInstance.History.UpdateWithEpisodeList([dlEpisode]); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == dlEpisode.SeriesId); if (historySeries != null){ CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(false); @@ -81,7 +82,13 @@ public class CrEpisode(){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){ // Push to arrays if there are no duplicates of the same language episode.EpisodeAndLanguages.Items.Add(dlEpisode); - episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale)); + episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem{ + CrLocale = "und", + Locale = "un", + Code = "und", + Name = string.Empty, + Language = string.Empty + }); } } } else{ @@ -91,7 +98,13 @@ public class CrEpisode(){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){ // Push to arrays if there are no duplicates of the same language episode.EpisodeAndLanguages.Items.Add(dlEpisode); - episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale)); + episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? new LanguageItem{ + CrLocale = "und", + Locale = "un", + Code = "und", + Name = string.Empty, + Language = string.Empty + }); } } @@ -100,7 +113,7 @@ public class CrEpisode(){ int epIndex = 1; - var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special). + var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special). string newKey; if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){ newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id); @@ -110,14 +123,14 @@ public class CrEpisode(){ episode.Key = newKey; - var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle + var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)"))?.SeasonTitle ?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); var title = episode.EpisodeAndLanguages.Items[0].Title; var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString(); var languages = episode.EpisodeAndLanguages.Items.Select((a, index) => - $"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆ + $"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆ Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]"); @@ -125,7 +138,7 @@ public class CrEpisode(){ if (!serieshasversions){ Console.WriteLine("Couldn\'t find versions on episode, added languages with language array."); } - + return episode; } @@ -160,17 +173,17 @@ public class CrEpisode(){ var epMeta = new CrunchyEpMeta(); epMeta.Data = new List{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } }; - epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? + epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle))?.SeriesTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(); - epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? + epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle))?.SeasonTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); epMeta.EpisodeNumber = item.Episode; epMeta.EpisodeTitle = item.Title; epMeta.SeasonId = item.SeasonId; epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; - epMeta.ShowId = item.SeriesId; + epMeta.SeriesId = item.SeriesId; epMeta.AbsolutEpisodeNumberE = epNum; - epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; + epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, @@ -197,7 +210,7 @@ public class CrEpisode(){ } } - if (retMeta.Data != null){ + if (retMeta.Data is{ Count: > 0 }){ epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index]; retMeta.Data.Add(epMetaData); } else{ @@ -215,7 +228,7 @@ public class CrEpisode(){ return retMeta; } - public async Task GetNewEpisodes(string? crLocale, int requestAmount,DateTime? firstWeekDay = null , bool forcedLang = false){ + public async Task GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){ await crunInstance.CrAuth.RefreshToken(true); CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase(); complete.Data =[]; @@ -257,7 +270,6 @@ public class CrEpisode(){ requestAmount += 50; } } - } } else{ break; diff --git a/CRD/Downloader/Crunchyroll/CrMovies.cs b/CRD/Downloader/Crunchyroll/CrMovies.cs index 3789d5e..60bda91 100644 --- a/CRD/Downloader/Crunchyroll/CrMovies.cs +++ b/CRD/Downloader/Crunchyroll/CrMovies.cs @@ -34,7 +34,7 @@ public class CrMovies{ return null; } - CrunchyMovieList movie = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + CrunchyMovieList movie = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyMovieList(); if (movie.Total < 1){ return null; @@ -67,9 +67,9 @@ public class CrMovies{ epMeta.EpisodeTitle = episodeP.Title; epMeta.SeasonId = ""; epMeta.Season = ""; - epMeta.ShowId = ""; + epMeta.SeriesId = ""; epMeta.AbsolutEpisodeNumberE = ""; - epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; + epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, diff --git a/CRD/Downloader/Crunchyroll/CrMusic.cs b/CRD/Downloader/Crunchyroll/CrMusic.cs index 0076fc8..d46275a 100644 --- a/CRD/Downloader/Crunchyroll/CrMusic.cs +++ b/CRD/Downloader/Crunchyroll/CrMusic.cs @@ -14,44 +14,119 @@ namespace CRD.Downloader.Crunchyroll; public class CrMusic{ private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; - public async Task ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false){ - return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos"); + public async Task ParseFeaturedMusicVideoByIdAsync(string seriesId, string crLocale, bool forcedLang = false, bool updateHistory = false){ + var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/featured/{seriesId}", crLocale, forcedLang); + + if (musicVideos.Data is{ Count: > 0 } && updateHistory){ + await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); + } + + return musicVideos; + } + + public async Task ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){ + var musicVideo = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos"); + + if (musicVideo != null && updateHistory){ + await crunInstance.History.UpdateWithMusicEpisodeList([musicVideo]); + } + + return musicVideo; } - public async Task ParseConcertByIdAsync(string id, string crLocale, bool forcedLang = false){ - return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/concerts"); + public async Task ParseConcertByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){ + var concert = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/concerts"); + + if (concert != null){ + concert.EpisodeType = EpisodeType.Concert; + if (updateHistory){ + await crunInstance.History.UpdateWithMusicEpisodeList([concert]); + } + } + + return concert; } - public async Task ParseArtistMusicVideosByIdAsync(string id, string crLocale, bool forcedLang = false){ - var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/music_videos", crLocale, forcedLang); - var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/concerts", crLocale, forcedLang); + public async Task ParseArtistMusicVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){ + var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang); + + if (updateHistory){ + await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); + } + + return musicVideos; + } + + public async Task ParseArtistConcertVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){ + var concerts = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/concerts", crLocale, forcedLang); + + if (concerts.Data.Count > 0){ + foreach (var crunchyConcertVideo in concerts.Data){ + crunchyConcertVideo.EpisodeType = EpisodeType.Concert; + } + } + + if (updateHistory){ + await crunInstance.History.UpdateWithMusicEpisodeList(concerts.Data); + } + + return concerts; + } + + + public async Task ParseArtistVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){ + var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang); + var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/concerts", crLocale, forcedLang); await Task.WhenAll(musicVideosTask, concertsTask); var musicVideos = await musicVideosTask; var concerts = await concertsTask; - - musicVideos.Total += concerts.Total; - musicVideos.Data ??= new List(); - if (concerts.Data != null){ + musicVideos.Total += concerts.Total; + + if (concerts.Data.Count > 0){ + foreach (var crunchyConcertVideo in concerts.Data){ + crunchyConcertVideo.EpisodeType = EpisodeType.Concert; + } + musicVideos.Data.AddRange(concerts.Data); } + if (updateHistory){ + await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); + } + return musicVideos; } + public async Task ParseArtistByIdAsync(string id, string crLocale, bool forcedLang = false){ + var query = CreateQueryParameters(crLocale, forcedLang); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/music/artists/{id}", HttpMethod.Get, true, true, query); + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine($"Request to {ApiUrls.Content}/music/artists/{id} failed"); + return new CrArtist(); + } + + var artistList = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyArtistList(); + + return artistList.Data.FirstOrDefault() ?? new CrArtist(); + } + private async Task ParseMediaByIdAsync(string id, string crLocale, bool forcedLang, string endpoint){ var mediaList = await FetchMediaListAsync($"{ApiUrls.Content}/{endpoint}/{id}", crLocale, forcedLang); switch (mediaList.Total){ case < 1: return null; - case 1 when mediaList.Data != null: + case 1 when mediaList.Data.Count > 0: return mediaList.Data.First(); default: Console.Error.WriteLine($"Multiple items returned for endpoint {endpoint} with ID {id}"); - return mediaList.Data?.First(); + return mediaList.Data.First(); } } @@ -66,7 +141,7 @@ public class CrMusic{ return new CrunchyMusicVideoList(); } - return Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + return Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyMusicVideoList(); } private NameValueCollection CreateQueryParameters(string crLocale, bool forcedLang){ @@ -88,15 +163,16 @@ public class CrMusic{ public CrunchyEpMeta EpisodeMeta(CrunchyMusicVideo episodeP){ var images = (episodeP.Images?.Thumbnail ?? new List{ new Image{ Source = "/notFound.png" } }); + var epMeta = new CrunchyEpMeta(); epMeta.Data = new List{ new(){ MediaId = episodeP.Id, Versions = null } }; - epMeta.SeriesTitle = "Music"; - epMeta.SeasonTitle = episodeP.DisplayArtistName; + epMeta.SeriesTitle = episodeP.GetSeriesTitle(); + epMeta.SeasonTitle = episodeP.GetSeasonTitle(); epMeta.EpisodeNumber = episodeP.SequenceNumber + ""; - epMeta.EpisodeTitle = episodeP.Title; - epMeta.SeasonId = ""; + epMeta.EpisodeTitle = episodeP.GetEpisodeTitle(); + epMeta.SeasonId = episodeP.GetSeasonId(); epMeta.Season = ""; - epMeta.ShowId = ""; + epMeta.SeriesId = episodeP.GetSeriesId(); epMeta.AbsolutEpisodeNumberE = ""; epMeta.Image = images[images.Count / 2].Source; epMeta.DownloadProgress = new DownloadProgress(){ diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index da9edbf..c1f89fb 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -7,13 +7,14 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Views; using ReactiveUI; namespace CRD.Downloader.Crunchyroll; -public class CrSeries(){ +public class CrSeries{ private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; public Dictionary ItemSelectMultiDub(Dictionary eps, List dubLang, bool? but, bool? all, List? e){ @@ -61,15 +62,15 @@ public class CrSeries(){ var epMeta = new CrunchyEpMeta(); epMeta.Data = new List{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } }; - epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(); - epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); + epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle))?.SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(); + epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle))?.SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); epMeta.EpisodeNumber = item.Episode; epMeta.EpisodeTitle = item.Title; epMeta.SeasonId = item.SeasonId; epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; - epMeta.ShowId = item.SeriesId; + epMeta.SeriesId = item.SeriesId; epMeta.AbsolutEpisodeNumberE = epNum; - epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; + epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source ?? ""; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, @@ -98,7 +99,7 @@ public class CrSeries(){ if (all is true || e != null && e.Contains(epNum)){ if (ret.TryGetValue(key, out var epMe)){ epMetaData.Lang = episode.Langs[index]; - epMe.Data?.Add(epMetaData); + epMe.Data.Add(epMetaData); } else{ epMetaData.Lang = episode.Langs[index]; epMeta.Data[0] = epMetaData; @@ -122,7 +123,7 @@ public class CrSeries(){ bool serieshasversions = true; - CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale,forcedLocale); + CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale, forcedLocale); if (parsedSeries == null || parsedSeries.Data == null){ Console.Error.WriteLine("Parse Data Invalid"); @@ -131,66 +132,59 @@ public class CrSeries(){ // var result = ParseSeriesResult(parsedSeries); Dictionary episodes = new Dictionary(); - + if (crunInstance.CrunOptions.History){ - crunInstance.History.CRUpdateSeries(id,""); + _ = crunInstance.History.CrUpdateSeries(id, ""); } - var cachedSeasonID = ""; + var cachedSeasonId = ""; var seasonData = new CrunchyEpisodeList(); - + foreach (var s in parsedSeries.Data){ - if (data?.S != null && s.Id != data.Value.S) continue; - int fallbackIndex = 0; - if (cachedSeasonID != s.Id){ - seasonData = await GetSeasonDataById(s.Id, forcedLocale ? crLocale : ""); - cachedSeasonID = s.Id; - } - - if (seasonData.Data != null){ - + if (data?.S != null && s.Id != data.S) continue; + int fallbackIndex = 0; + if (cachedSeasonId != s.Id){ + seasonData = await GetSeasonDataById(s.Id, forcedLocale ? crLocale : ""); + cachedSeasonId = s.Id; + } - foreach (var episode in seasonData.Data){ - // Prepare the episode array - EpisodeAndLanguage item; + if (seasonData.Data != null){ + foreach (var episode in seasonData.Data){ + // Prepare the episode array + EpisodeAndLanguage item; - string episodeNum = (episode.Episode != String.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}")) ?? string.Empty; + string episodeNum = (episode.Episode != String.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}")) ?? string.Empty; - var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier) ? s.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}"; - var episodeKey = $"{seasonIdentifier}E{episodeNum}"; + var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier) ? s.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}"; + var episodeKey = $"{seasonIdentifier}E{episodeNum}"; - if (!episodes.ContainsKey(episodeKey)){ - item = new EpisodeAndLanguage{ - Items = new List(), - Langs = new List() - }; - episodes[episodeKey] = item; - } else{ - item = episodes[episodeKey]; - } + if (!episodes.ContainsKey(episodeKey)){ + item = new EpisodeAndLanguage{ + Items = new List(), + Langs = new List() + }; + episodes[episodeKey] = item; + } else{ + item = episodes[episodeKey]; + } - if (episode.Versions != null){ - foreach (var version in episode.Versions){ - // Ensure there is only one of the same language - if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){ - // Push to arrays if there are no duplicates of the same language - item.Items.Add(episode); - item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale)); - } - } - } else{ - // Episode didn't have versions, mark it as such to be logged. - serieshasversions = false; - // Ensure there is only one of the same language - if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){ - // Push to arrays if there are no duplicates of the same language + if (episode.Versions != null){ + foreach (var version in episode.Versions){ + if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){ item.Items.Add(episode); - item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale)); + item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem()); } } + } else{ + serieshasversions = false; + if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){ + item.Items.Add(episode); + item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale) ?? new LanguageItem()); + } } } + } } if (crunInstance.CrunOptions.History){ @@ -215,21 +209,21 @@ public class CrSeries(){ string newKey; if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){ - newKey = $"SP{specialIndex}_" + item.Items[0].Episode ?? "SP" + (specialIndex + " " + item.Items[0].Id); + newKey = $"SP{specialIndex}_" + item.Items[0].Episode;// ?? "SP" + (specialIndex + " " + item.Items[0].Id); } else{ newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}"; } - + episodes.Remove(key); - + int counter = 1; string originalKey = newKey; while (episodes.ContainsKey(newKey)){ newKey = originalKey + "_" + counter; counter++; } - + episodes.Add(newKey, item); if (isSpecial){ @@ -249,14 +243,14 @@ public class CrSeries(){ var key = kvp.Key; var item = kvp.Value; - var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle + var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)"))?.SeasonTitle ?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); var title = item.Items[0].Title; var seasonNumber = Helpers.ExtractNumberAfterS(item.Items[0].Identifier) ?? item.Items[0].SeasonNumber.ToString(); var languages = item.Items.Select((a, index) => - $"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆ + $"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆ Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]"); } @@ -275,7 +269,7 @@ public class CrSeries(){ var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0); var langList = value.Langs.Select(a => a.CrLocale).ToList(); Languages.SortListByLangList(langList); - + return new Episode{ E = key.StartsWith("E") ? key.Substring(1) : key, Lang = langList, @@ -285,8 +279,9 @@ public class CrSeries(){ SeasonTitle = Regex.Replace(value.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(), EpisodeNum = key.StartsWith("SP") ? key : value.Items[0].EpisodeNumber?.ToString() ?? value.Items[0].Episode ?? "?", Id = value.Items[0].SeasonId, - Img = images[images.Count / 2].FirstOrDefault().Source, + Img = images[images.Count / 2].FirstOrDefault()?.Source ?? "", Description = value.Items[0].Description, + EpisodeType = EpisodeType.Episode, Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds. }; }).ToList(); @@ -294,7 +289,7 @@ public class CrSeries(){ return crunchySeriesList; } - public async Task GetSeasonDataById(string seasonID, string? crLocale, bool forcedLang = false, bool log = false){ + public async Task GetSeasonDataById(string seasonId, string? crLocale, bool forcedLang = false, bool log = false){ CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List(), Total = 0, Meta = new Meta() }; NameValueCollection query; @@ -309,7 +304,7 @@ public class CrSeries(){ } } - var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}", HttpMethod.Get, true, true, query); + var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(showRequest); @@ -330,14 +325,15 @@ public class CrSeries(){ } } - var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}/episodes", HttpMethod.Get, true, true, query); + var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}/episodes", HttpMethod.Get, true, true, query); var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest); if (!episodeRequestResponse.IsOk){ Console.Error.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}"); } else{ - episodeList = Helpers.Deserialize(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + episodeList = Helpers.Deserialize(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? + new CrunchyEpisodeList(){ Data =[], Total = 0, Meta = new Meta() }; } if (episodeList.Total < 1){ @@ -351,6 +347,8 @@ public class CrSeries(){ var ret = new Dictionary>(); int i = 0; + if (seasonsList.Data == null) return ret; + foreach (var item in seasonsList.Data){ i++; foreach (var lang in Languages.languages){ @@ -482,12 +480,12 @@ public class CrSeries(){ } } } - + return series; } public async Task GetAllSeries(string? crLocale){ - CrBrowseSeriesBase? complete = new CrBrowseSeriesBase(); + CrBrowseSeriesBase complete = new CrBrowseSeriesBase(); complete.Data =[]; var i = 0; diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index b0fb9e6..6e86638 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -21,6 +21,7 @@ using CRD.Utils.Sonarr; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.History; +using CRD.ViewModels; using CRD.ViewModels.Utils; using CRD.Views; using CRD.Views.Utils; @@ -130,9 +131,75 @@ public class CrunchyrollManager{ options.BackgroundImageOpacity = 0.5; options.BackgroundImageBlurRadius = 10; + options.HistoryPageProperties = new HistoryPageProperties{ + SelectedView = HistoryViewType.Posters, + SelectedSorting = SortingType.SeriesTitle, + SelectedFilter = FilterType.All, + ScaleValue = 0.73, + Ascending = false, + ShowSeries = true, + ShowArtists = true + }; + options.History = true; - CfgManager.UpdateSettingsFromFile(options); + if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){ + var optionsYaml = new CrDownloadOptionsYaml(); + + optionsYaml.AutoDownload = false; + optionsYaml.RemoveFinishedDownload = false; + optionsYaml.Chapters = true; + optionsYaml.Hslang = "none"; + optionsYaml.Force = "Y"; + optionsYaml.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]"; + optionsYaml.Partsize = 10; + optionsYaml.DlSubs = new List{ "en-US" }; + optionsYaml.SkipMuxing = false; + optionsYaml.MkvmergeOptions = new List{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" }; + optionsYaml.FfmpegOptions = new(); + optionsYaml.DefaultAudio = "ja-JP"; + optionsYaml.DefaultSub = "en-US"; + optionsYaml.QualityAudio = "best"; + optionsYaml.QualityVideo = "best"; + optionsYaml.CcTag = "CC"; + optionsYaml.CcSubsFont = "Trebuchet MS"; + optionsYaml.FsRetryTime = 5; + optionsYaml.Numbers = 2; + optionsYaml.Timeout = 15000; + optionsYaml.DubLang = new List(){ "ja-JP" }; + optionsYaml.SimultaneousDownloads = 2; + // options.AccentColor = Colors.SlateBlue.ToString(); + optionsYaml.Theme = "System"; + optionsYaml.SelectedCalendarLanguage = "en-us"; + optionsYaml.CalendarDubFilter = "none"; + optionsYaml.CustomCalendar = true; + optionsYaml.DlVideoOnce = true; + optionsYaml.StreamEndpoint = "web/firefox"; + optionsYaml.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; + optionsYaml.HistoryLang = DefaultLocale; + + optionsYaml.BackgroundImageOpacity = 0.5; + optionsYaml.BackgroundImageBlurRadius = 10; + + optionsYaml.HistoryPageProperties = new HistoryPageProperties{ + SelectedView = HistoryViewType.Posters, + SelectedSorting = SortingType.SeriesTitle, + SelectedFilter = FilterType.All, + ScaleValue = 0.73, + Ascending = false, + ShowSeries = true, + ShowArtists = true + }; + + optionsYaml.History = true; + + CfgManager.UpdateSettingsFromFileYAML(optionsYaml); + + options = Helpers.MigrateSettings(optionsYaml); + } else{ + CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions); + } + return options; } @@ -147,7 +214,6 @@ public class CrunchyrollManager{ CrMusic = new CrMusic(); History = new History(); - Profile = new CrProfile{ Username = "???", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", @@ -155,6 +221,11 @@ public class CrunchyrollManager{ PreferredContentSubtitleLanguage = DefaultLocale, HasPremium = false, }; + + if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){ + CfgManager.WriteCrSettings(); + Helpers.DeleteFile(CfgManager.PathCrDownloadOptionsOld); + } } public async Task Init(){ @@ -183,8 +254,11 @@ public class CrunchyrollManager{ } if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){ - Token = CfgManager.DeserializeFromFile(CfgManager.PathCrToken); + Token = CfgManager.ReadJsonFromFile(CfgManager.PathCrToken); await CrAuth.LoginWithToken(); + if (Path.Exists(CfgManager.PathCrTokenOld)){ + Helpers.DeleteFile(CfgManager.PathCrTokenOld); + } } else{ await CrAuth.AuthAnonymous(); } @@ -200,12 +274,11 @@ public class CrunchyrollManager{ ); if (historyList != null){ - HistoryList = historyList; - + Parallel.ForEach(historyList, historySeries => { historySeries.Init(); - + foreach (var historySeriesSeason in historySeries.Seasons){ historySeriesSeason.Init(); } @@ -318,11 +391,13 @@ public class CrunchyrollManager{ QueueManager.Instance.Queue.Refresh(); - await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data); + var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty); + + if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data); } if (options.DownloadToTempFolder){ - await MoveFromTempFolder(merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); + await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); } } } else{ @@ -364,11 +439,12 @@ public class CrunchyrollManager{ QueueManager.Instance.Queue.Refresh(); - await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data); + var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty); + if (preset != null && result.merger != null) await Helpers.RunFFmpegWithPresetAsync(result.merger.options.Output, preset, data); } if (options.DownloadToTempFolder){ - await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); + await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); } } @@ -417,8 +493,8 @@ public class CrunchyrollManager{ QueueManager.Instance.ActiveDownloads--; QueueManager.Instance.Queue.Refresh(); - if (options.History && data.Data != null && data.Data.Count > 0){ - History.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId); + if (options.History && data.Data is{ Count: > 0 } && (options.HistoryIncludeCrArtists && data.Music || !data.Music)){ + History.SetAsDownloaded(data.SeriesId, data.SeasonId, data.Data.First().MediaId); } @@ -473,9 +549,9 @@ public class CrunchyrollManager{ ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR; - var destinationPath = Path.Combine(destinationFolder ?? string.Empty, fileName ?? string.Empty); + var destinationPath = Path.Combine(destinationFolder ?? string.Empty, fileName); - string destinationDirectory = Path.GetDirectoryName(destinationPath); + string? destinationDirectory = Path.GetDirectoryName(destinationPath); if (string.IsNullOrEmpty(destinationDirectory)){ Console.WriteLine("Invalid destination directory path."); return; @@ -519,7 +595,7 @@ public class CrunchyrollManager{ foreach (var downloadedMedia in subs){ var subt = new SubtitleFonts(); subt.Language = downloadedMedia.Language; - subt.Fonts = downloadedMedia.Fonts; + subt.Fonts = downloadedMedia.Fonts ??[]; subsList.Add(subt); } @@ -609,7 +685,9 @@ public class CrunchyrollManager{ } } - syncVideosList.ForEach(syncVideo => Helpers.DeleteFile(syncVideo.Path)); + syncVideosList.ForEach(syncVideo => { + if (syncVideo.Path != null) Helpers.DeleteFile(syncVideo.Path); + }); } if (!options.Mp4 && !muxToMp3){ @@ -725,7 +803,15 @@ public class CrunchyrollManager{ bool dlVideoOnce = false; string fileDir = CfgManager.PathVIDEOS_DIR; - if (data.Data != null){ + if (data.Data is{ Count: > 0 }){ + options.Partsize = options.Partsize > 0 ? options.Partsize : 1; + + var sortedMetaData = data.Data + .OrderBy(metaData => options.DubLang.IndexOf(metaData.Lang?.CrLocale ?? string.Empty) != -1 ? options.DubLang.IndexOf(metaData.Lang?.CrLocale ?? string.Empty) : int.MaxValue) + .ToList(); + + data.Data = sortedMetaData; + foreach (CrunchyEpMetaData epMeta in data.Data){ Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}"); @@ -753,10 +839,10 @@ public class CrunchyrollManager{ string mediaGuid = currentMediaId; if (epMeta.Versions != null){ if (epMeta.Lang != null){ - currentVersion = epMeta.Versions.Find(a => a.AudioLocale == epMeta.Lang?.CrLocale); + currentVersion = epMeta.Versions.Find(a => a.AudioLocale == epMeta.Lang?.CrLocale) ?? currentVersion; } else if (data.SelectedDubs is{ Count: 1 }){ - LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]); - currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang.CrLocale); + LanguageItem? lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]); + currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang?.CrLocale) ?? currentVersion; } else if (epMeta.Versions.Count == 1){ currentVersion = epMeta.Versions[0]; } @@ -772,7 +858,7 @@ public class CrunchyrollManager{ mediaGuid = currentVersion.Guid; if (!isPrimary){ - primaryVersion = epMeta.Versions.Find(a => a.Original); + primaryVersion = epMeta.Versions.Find(a => a.Original) ?? currentVersion; } else{ primaryVersion = currentVersion; } @@ -816,12 +902,12 @@ public class CrunchyrollManager{ Data = new List(), Error = true, FileName = "./unknown", - ErrorText = "Too many active streams that couldn't be stopped" + ErrorText = "Too many active streams that couldn't be stopped\nClose open cruchyroll tabs in your browser" }; } } - MainWindow.Instance.ShowError("Couldn't get Playback Data"); + MainWindow.Instance.ShowError("Couldn't get Playback Data\nTry again later or else check logs and crunchyroll"); return new DownloadResponse{ Data = new List(), Error = true, @@ -1193,9 +1279,10 @@ public class CrunchyrollManager{ } //string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray()); - string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.CrLocale); + string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale); - string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override) + string tempFile = Path.Combine(FileNameManager + .ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override) .ToArray()); string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile); @@ -1207,7 +1294,7 @@ public class CrunchyrollManager{ } else if (options.Novids){ Console.WriteLine("Skipping video download..."); } else{ - var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tsFile, tempTsFile, data, fileDir); + var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tempTsFile, data, fileDir); tsFile = videoDownloadResult.tsFile; @@ -1226,7 +1313,7 @@ public class CrunchyrollManager{ if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){ - var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tsFile, tempTsFile, data, fileDir); + var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tempTsFile, data, fileDir); tsFile = audioDownloadResult.tsFile; @@ -1403,7 +1490,7 @@ public class CrunchyrollManager{ videoDownloadMedia = new DownloadedMedia{ Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Path = $"{tsFile}.video.m4s", - Lang = lang.Value, + Lang = lang, IsPrimary = isPrimary }; files.Add(videoDownloadMedia); @@ -1471,7 +1558,7 @@ public class CrunchyrollManager{ files.Add(new DownloadedMedia{ Type = DownloadMediaType.Audio, Path = $"{tsFile}.audio.m4s", - Lang = lang.Value, + Lang = lang, IsPrimary = isPrimary }); data.downloadedFiles.Add($"{tsFile}.audio.m4s"); @@ -1487,7 +1574,7 @@ public class CrunchyrollManager{ videoDownloadMedia = new DownloadedMedia{ Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Path = $"{tsFile}.video.m4s", - Lang = lang.Value, + Lang = lang, IsPrimary = isPrimary }; files.Add(videoDownloadMedia); @@ -1498,7 +1585,7 @@ public class CrunchyrollManager{ files.Add(new DownloadedMedia{ Type = DownloadMediaType.Audio, Path = $"{tsFile}.audio.m4s", - Lang = lang.Value, + Lang = lang, IsPrimary = isPrimary }); data.downloadedFiles.Add($"{tsFile}.audio.m4s"); @@ -1547,7 +1634,13 @@ public class CrunchyrollManager{ } // Finding language by code - var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang); + var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang) ?? new LanguageItem{ + CrLocale = "und", + Locale = "un", + Code = "und", + Name = string.Empty, + Language = string.Empty + }; if (lang.Code == "und"){ Console.Error.WriteLine($"Unable to find language for code {curStream?.AudioLang}"); } @@ -1571,7 +1664,7 @@ public class CrunchyrollManager{ } if (!options.SkipSubs && data.DownloadSubs.IndexOf("none") == -1){ - await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir, data, (options.DlVideoOnce && dlVideoOnce && options.SyncTiming), videoDownloadMedia); + await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir, data, videoDownloadMedia); } else{ Console.WriteLine("Subtitles downloading skipped!"); } @@ -1646,7 +1739,7 @@ public class CrunchyrollManager{ }; } - private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files, string fileDir, CrunchyEpMeta data, bool needsDelay, + private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files, string fileDir, CrunchyEpMeta data, DownloadedMedia videoDownloadMedia){ if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){ List subsData = pbData.Meta.Subtitles?.Values.ToList() ??[]; @@ -1804,12 +1897,13 @@ public class CrunchyrollManager{ } } - private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data, + private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tempTsFile, CrunchyEpMeta data, string fileDir){ // Prepare for video download int totalParts = chosenVideoSegments.segments.Count; int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize); string mathMsg = $"({mathParts}*{options.Partsize})"; + string tsFile; Console.WriteLine($"Total parts in video stream: {totalParts} {mathMsg}"); if (Path.IsPathRooted(outFile)){ @@ -1843,9 +1937,10 @@ public class CrunchyrollManager{ return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile); } - private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data, + private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tempTsFile, CrunchyEpMeta data, string fileDir){ // Prepare for audio download + string tsFile; int totalParts = chosenAudioSegments.segments.Count; int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize); string mathMsg = $"({mathParts}*{options.Partsize})"; @@ -1911,7 +2006,7 @@ public class CrunchyrollManager{ var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); if (!playbackRequestResponse.IsOk){ - playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint); + playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint); } if (playbackRequestResponse.IsOk){ @@ -1922,7 +2017,7 @@ public class CrunchyrollManager{ playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); if (!playbackRequestResponse.IsOk){ - playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint); + playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint); } if (playbackRequestResponse.IsOk){ @@ -1932,7 +2027,7 @@ public class CrunchyrollManager{ } } - return (IsOk: playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent); + return (playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent); } private async Task<(bool IsOk, string ResponseContent)> SendPlaybackRequestAsync(string endpoint){ @@ -1940,7 +2035,7 @@ public class CrunchyrollManager{ return await HttpClientReq.Instance.SendHttpRequest(request); } - private async Task<(bool IsOk, string ResponseContent)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent) response, string mediaGuidId, string endpoint){ + private async Task<(bool IsOk, string ResponseContent)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent) response, string endpoint){ if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response; var error = StreamError.FromJson(response.ResponseContent); @@ -1990,7 +2085,7 @@ public class CrunchyrollManager{ temppbData.Meta = new PlaybackMeta{ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, - Bifs = new List{ playStream.Bifs }, + Bifs = new List{ playStream.Bifs ?? "" }, MediaId = mediaId, Captions = playStream.Captions, Subtitles = new Subtitles() @@ -2012,7 +2107,7 @@ public class CrunchyrollManager{ #endregion - private async Task ParseChapters(string currentMediaId, List compiledChapters){ + private async Task ParseChapters(string currentMediaId, List compiledChapters){ var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null); var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true); @@ -2024,11 +2119,11 @@ public class CrunchyrollManager{ try{ JObject jObject = JObject.Parse(showRequestResponse.ResponseContent); - if (jObject.TryGetValue("lastUpdate", out JToken lastUpdateToken)){ + if (jObject.TryGetValue("lastUpdate", out JToken? lastUpdateToken)){ chapterData.lastUpdate = lastUpdateToken.ToObject(); } - if (jObject.TryGetValue("mediaId", out JToken mediaIdToken)){ + if (jObject.TryGetValue("mediaId", out JToken? mediaIdToken)){ chapterData.mediaId = mediaIdToken.ToObject(); } @@ -2037,7 +2132,7 @@ public class CrunchyrollManager{ foreach (var property in jObject.Properties()){ if (property.Value.Type == JTokenType.Object && property.Name != "lastUpdate" && property.Name != "mediaId"){ try{ - CrunchyChapter chapter = property.Value.ToObject(); + CrunchyChapter chapter = property.Value.ToObject() ?? new CrunchyChapter(); chapterData.Chapters.Add(chapter); } catch (Exception ex){ Console.Error.WriteLine($"Error parsing chapter: {ex.Message}"); @@ -2046,7 +2141,7 @@ public class CrunchyrollManager{ } } catch (Exception ex){ Console.Error.WriteLine($"Error parsing JSON response: {ex.Message}"); - return false; + return; } if (chapterData.Chapters.Count > 0){ @@ -2098,8 +2193,6 @@ public class CrunchyrollManager{ compiledChapters.Add($"CHAPTER{chapterNumber}NAME={formattedChapterType} End"); } } - - return true; } } else{ Console.WriteLine("Chapter request failed, attempting old API "); @@ -2109,7 +2202,7 @@ public class CrunchyrollManager{ showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true); if (showRequestResponse.IsOk){ - CrunchyOldChapter chapterData = Helpers.Deserialize(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings); + CrunchyOldChapter chapterData = Helpers.Deserialize(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new CrunchyOldChapter(); DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); @@ -2137,13 +2230,10 @@ public class CrunchyrollManager{ chapterNumber = (compiledChapters.Count / 2) + 1; compiledChapters.Add($"CHAPTER{chapterNumber}={endFormatted}"); compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Episode"); - return true; + return; } Console.Error.WriteLine("Chapter request failed"); - return false; } - - return true; } } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index ab2fde3..5884c31 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -12,8 +12,10 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Utils; using CRD.Utils.Ffmpeg_Encoding; +using CRD.Utils.Files; using CRD.Utils.Sonarr; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.History; using CRD.ViewModels; using CRD.ViewModels.Utils; @@ -42,7 +44,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _includeCcSubs; - + [ObservableProperty] private ComboBoxItem _selectedScaledBorderAndShadow; @@ -77,10 +79,10 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _skipSubMux; - + [ObservableProperty] private double? _leadingNumbers; - + [ObservableProperty] private double? _partSize; @@ -107,7 +109,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private ComboBoxItem _selectedHSLang; - + [ObservableProperty] private ComboBoxItem _selectedDescriptionLang; @@ -131,81 +133,78 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private ComboBoxItem? _selectedAudioQuality; - + [ObservableProperty] - private ObservableCollection _selectedSubLang = new(); - + private ObservableCollection _selectedSubLang =[]; + [ObservableProperty] private Color _listBoxColor; - - public ObservableCollection VideoQualityList{ get; } = new(){ - new ComboBoxItem(){ Content = "best" }, - new ComboBoxItem(){ Content = "1080" }, - new ComboBoxItem(){ Content = "720" }, - new ComboBoxItem(){ Content = "480" }, - new ComboBoxItem(){ Content = "360" }, - new ComboBoxItem(){ Content = "240" }, - new ComboBoxItem(){ Content = "worst" }, - }; - public ObservableCollection AudioQualityList{ get; } = new(){ - new ComboBoxItem(){ Content = "best" }, - new ComboBoxItem(){ Content = "128kB/s" }, - new ComboBoxItem(){ Content = "96kB/s" }, - new ComboBoxItem(){ Content = "64kB/s" }, - new ComboBoxItem(){ Content = "worst" }, - }; + public ObservableCollection VideoQualityList{ get; } =[ + new(){ Content = "best" }, + new(){ Content = "1080" }, + new(){ Content = "720" }, + new(){ Content = "480" }, + new(){ Content = "360" }, + new(){ Content = "240" }, + new(){ Content = "worst" } + ]; - public ObservableCollection HardSubLangList{ get; } = new(){ - new ComboBoxItem(){ Content = "none" }, - }; - - public ObservableCollection DescriptionLangList{ get; } = new(){ - new ComboBoxItem(){ Content = "default" }, - new ComboBoxItem(){ Content = "de-DE" }, - new ComboBoxItem(){ Content = "en-US" }, - new ComboBoxItem(){ Content = "es-419" }, - new ComboBoxItem(){ Content = "es-ES" }, - new ComboBoxItem(){ Content = "fr-FR" }, - new ComboBoxItem(){ Content = "it-IT" }, - new ComboBoxItem(){ Content = "pt-BR" }, - new ComboBoxItem(){ Content = "pt-PT" }, - new ComboBoxItem(){ Content = "ru-RU" }, - new ComboBoxItem(){ Content = "hi-IN" }, - new ComboBoxItem(){ Content = "ar-SA" }, - }; + public ObservableCollection AudioQualityList{ get; } =[ + new(){ Content = "best" }, + new(){ Content = "128kB/s" }, + new(){ Content = "96kB/s" }, + new(){ Content = "64kB/s" }, + new(){ Content = "worst" } + ]; - public ObservableCollection DubLangList{ get; } = new(){ - }; + public ObservableCollection HardSubLangList{ get; } =[ + new(){ Content = "none" } + ]; + + public ObservableCollection DescriptionLangList{ get; } =[ + new(){ Content = "default" }, + new(){ Content = "de-DE" }, + new(){ Content = "en-US" }, + new(){ Content = "es-419" }, + new(){ Content = "es-ES" }, + new(){ Content = "fr-FR" }, + new(){ Content = "it-IT" }, + new(){ Content = "pt-BR" }, + new(){ Content = "pt-PT" }, + new(){ Content = "ru-RU" }, + new(){ Content = "hi-IN" }, + new(){ Content = "ar-SA" } + ]; + + public ObservableCollection DubLangList{ get; } =[]; - public ObservableCollection DefaultDubLangList{ get; } = new(){ - }; + public ObservableCollection DefaultDubLangList{ get; } =[]; - public ObservableCollection DefaultSubLangList{ get; } = new(){ - }; + public ObservableCollection DefaultSubLangList{ get; } =[]; - public ObservableCollection SubLangList{ get; } = new(){ - new ListBoxItem(){ Content = "all" }, - new ListBoxItem(){ Content = "none" }, - }; + public ObservableCollection SubLangList{ get; } =[ + new(){ Content = "all" }, + new(){ Content = "none" } + ]; - public ObservableCollection StreamEndpoints{ get; } = new(){ - new ComboBoxItem(){ Content = "web/firefox" }, - new ComboBoxItem(){ Content = "console/switch" }, - new ComboBoxItem(){ Content = "console/ps4" }, - new ComboBoxItem(){ Content = "console/ps5" }, - new ComboBoxItem(){ Content = "console/xbox_one" }, - new ComboBoxItem(){ Content = "web/edge" }, - // new ComboBoxItem(){ Content = "web/safari" }, - new ComboBoxItem(){ Content = "web/chrome" }, - new ComboBoxItem(){ Content = "web/fallback" }, - // new ComboBoxItem(){ Content = "ios/iphone" }, - // new ComboBoxItem(){ Content = "ios/ipad" }, - new ComboBoxItem(){ Content = "android/phone" }, - new ComboBoxItem(){ Content = "tv/samsung" }, - }; + public ObservableCollection StreamEndpoints{ get; } =[ + new(){ Content = "web/firefox" }, + new(){ Content = "console/switch" }, + new(){ Content = "console/ps4" }, + new(){ Content = "console/ps5" }, + new(){ Content = "console/xbox_one" }, + new(){ Content = "web/edge" }, + // new (){ Content = "web/safari" }, + new(){ Content = "web/chrome" }, + new(){ Content = "web/fallback" }, + // new (){ Content = "ios/iphone" }, + // new (){ Content = "ios/ipad" }, + new(){ Content = "android/phone" }, + new(){ Content = "tv/samsung" } + ]; [ObservableProperty] private bool _isEncodeEnabled; @@ -214,8 +213,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ private StringItem _selectedEncodingPreset; public ObservableCollection EncodingPresetsList{ get; } = new(); - - + + [ObservableProperty] private bool _cCSubsMuxingFlag; @@ -224,11 +223,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _signsSubsAsForced; - + + [ObservableProperty] + private bool _searchFetchFeaturedMusic; + private bool settingsLoaded; - + public CrunchyrollSettingsViewModel(){ - foreach (var languageItem in Languages.languages){ HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale }); @@ -244,7 +245,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrDownloadOptions options = CrunchyrollManager.Instance.CrunOptions; - StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == options.EncodingPresetName) ?? null; + StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => !string.IsNullOrEmpty(a.stringValue) && a.stringValue == options.EncodingPresetName) ?? null; SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0]; ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null; @@ -275,7 +276,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ foreach (var listBoxItem in dubLang){ SelectedDubLang.Add(listBoxItem); } - + AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options); @@ -301,22 +302,23 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ SkipSubMux = options.SkipSubsMux; LeadingNumbers = options.Numbers; FileName = options.FileName; + SearchFetchFeaturedMusic = options.SearchFetchFeaturedMusic; ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null; SelectedAudioQuality = qualityAudio ?? AudioQualityList[0]; ComboBoxItem? qualityVideo = VideoQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityVideo) ?? null; SelectedVideoQuality = qualityVideo ?? VideoQualityList[0]; - + MkvMergeOptions.Clear(); - if (options.MkvmergeOptions != null){ + if (options.MkvmergeOptions is{ Count: > 0 }){ foreach (var mkvmergeParam in options.MkvmergeOptions){ MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam }); } } FfmpegOptions.Clear(); - if (options.FfmpegOptions != null){ + if (options.FfmpegOptions is{ Count: > 0 }){ foreach (var ffmpegParam in options.FfmpegOptions){ FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam }); } @@ -341,7 +343,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ if (!settingsLoaded){ return; } - + CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced = SignsSubsAsForced; CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag = CCSubsMuxingFlag; CrunchyrollManager.Instance.CrunOptions.CcSubsFont = CCSubsFont; @@ -364,8 +366,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.FileName = FileName; CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; - CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 0), 0, 10000); - + CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 1), 1, 10000); + CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic = SearchFetchFeaturedMusic; + CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection(); List softSubs = new List(); @@ -378,7 +381,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ string descLang = SelectedDescriptionLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale; - + string hslang = SelectedHSLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang; @@ -398,7 +401,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + ""; CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + ""; - + List mkvmergeParams = new List(); foreach (var mkvmergeParam in MkvMergeOptions){ mkvmergeParams.Add(mkvmergeParam.stringValue); @@ -413,7 +416,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams; - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } @@ -469,7 +472,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ FfmpegOptions.Remove(param); RaisePropertyChanged(nameof(FfmpegOptions)); } - + private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ UpdateSettings(); @@ -515,7 +518,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.HistoryList =[]; } } - } [RelayCommand] @@ -542,9 +544,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ } settingsLoaded = true; - StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == CrunchyrollManager.Instance.CrunOptions.EncodingPresetName) ?? null; + StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => string.IsNullOrEmpty(a.stringValue) && a.stringValue == CrunchyrollManager.Instance.CrunOptions.EncodingPresetName) ?? null; SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0]; } } - } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml index 4fe04b5..e8ad4f3 100644 --- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -191,6 +191,22 @@ + + + + + + + + + + + + CRUpdateSeries(string seriesId, string? seasonId){ + public async Task CrUpdateSeries(string seriesId, string? seasonId){ await crunInstance.CrAuth.RefreshToken(true); CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true); @@ -46,7 +47,8 @@ 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); + + if (seasonData.Data is{ Count: > 0 }) await UpdateWithSeasonData(seasonData.Data.ToList()); } @@ -64,6 +66,141 @@ public class History(){ } + public async Task UpdateWithMusicEpisodeList(List episodeList){ + if (episodeList is{ Count: > 0 }){ + if (crunInstance.CrunOptions is{ History: true, HistoryIncludeCrArtists: true }){ + var concertGroups = episodeList.Where(e => e.EpisodeType == EpisodeType.Concert).GroupBy(e => e.Artist.Id); + var musicVideoGroups = episodeList.Where(e => e.EpisodeType == EpisodeType.MusicVideo).GroupBy(e => e.Artist.Id); + + foreach (var concertGroup in concertGroups){ + await UpdateWithSeasonData(concertGroup.ToList()); + } + + foreach (var musicVideoGroup in musicVideoGroups){ + await UpdateWithSeasonData(musicVideoGroup.ToList()); + } + } + } + } + + public async Task UpdateWithEpisodeList(List episodeList){ + if (episodeList is{ Count: > 0 }){ + var episodeVersions = episodeList.First().Versions; + if (episodeVersions != null){ + var version = episodeVersions.Find(a => a.Original); + if (version?.AudioLocale != episodeList.First().AudioLocale){ + await CrUpdateSeries(episodeList.First().SeriesId, version?.SeasonGuid); + return; + } + } else{ + await CrUpdateSeries(episodeList.First().SeriesId, ""); + return; + } + + await UpdateWithSeasonData(episodeList.ToList()); + } + } + + /// + /// This method updates the History with a list of episodes. The episodes have to be from the same season. + /// + private async Task UpdateWithSeasonData(List episodeList){ + if (episodeList is{ Count: > 0 }){ + var firstEpisode = episodeList.First(); + var seriesId = firstEpisode.GetSeriesId(); + var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); + if (historySeries != null){ + historySeries.HistorySeriesAddDate ??= DateTime.Now; + historySeries.SeriesType = firstEpisode.GetSeriesType(); + historySeries.SeriesStreamingService = StreamingService.Crunchyroll; + + await RefreshSeriesData(seriesId, historySeries); + + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.GetSeasonId()); + + if (historySeason != null){ + historySeason.SeasonTitle = firstEpisode.GetSeasonTitle(); + historySeason.SeasonNum = firstEpisode.GetSeasonNum(); + historySeason.SpecialSeason = firstEpisode.IsSpecialSeason(); + foreach (var historySource in episodeList){ + if (historySource.GetSeasonId() != historySeason.SeasonId){ + continue; + } + + var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == historySource.GetEpisodeId()); + + if (historyEpisode == null){ + var newHistoryEpisode = new HistoryEpisode{ + EpisodeTitle = historySource.GetEpisodeTitle(), + EpisodeDescription = historySource.GetEpisodeDescription(), + EpisodeId = historySource.GetEpisodeId(), + Episode = historySource.GetEpisodeNumber(), + EpisodeSeasonNum = historySource.GetSeasonNum(), + SpecialEpisode = historySource.IsSpecialEpisode(), + HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(), + HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(), + EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), + EpisodeType = historySource.GetEpisodeType() + }; + + historySeason.EpisodesList.Add(newHistoryEpisode); + } else{ + //Update existing episode + historyEpisode.EpisodeTitle = historySource.GetEpisodeTitle(); + historyEpisode.SpecialEpisode = historySource.IsSpecialEpisode(); + historyEpisode.EpisodeDescription = historySource.GetEpisodeDescription(); + historyEpisode.EpisodeId = historySource.GetEpisodeId(); + historyEpisode.Episode = historySource.GetEpisodeNumber(); + historyEpisode.EpisodeSeasonNum = historySource.GetSeasonNum(); + historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate(); + historyEpisode.EpisodeType = historySource.GetEpisodeType(); + + historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(); + historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(); + } + } + + historySeason.EpisodesList.Sort(new NumericStringPropertyComparer()); + } else{ + var newSeason = NewHistorySeason(episodeList, firstEpisode); + + newSeason.EpisodesList.Sort(new NumericStringPropertyComparer()); + + historySeries.Seasons.Add(newSeason); + newSeason.Init(); + } + + historySeries.UpdateNewEpisodes(); + } else if (!string.IsNullOrEmpty(seriesId)){ + historySeries = new HistorySeries{ + SeriesTitle = firstEpisode.GetSeriesTitle(), + SeriesId = firstEpisode.GetSeriesId(), + Seasons =[], + HistorySeriesAddDate = DateTime.Now, + SeriesType = firstEpisode.GetSeriesType(), + SeriesStreamingService = StreamingService.Crunchyroll + }; + crunInstance.HistoryList.Add(historySeries); + + var newSeason = NewHistorySeason(episodeList, firstEpisode); + + newSeason.EpisodesList.Sort(new NumericStringPropertyComparer()); + + await RefreshSeriesData(seriesId, historySeries); + + historySeries.Seasons.Add(newSeason); + historySeries.UpdateNewEpisodes(); + historySeries.Init(); + newSeason.Init(); + } + + SortItems(); + if (historySeries != null){ + SortSeasons(historySeries); + } + } + } + public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); @@ -230,175 +367,57 @@ public class History(){ } - public async Task UpdateWithSeasonData(List? episodeList, bool skippVersionCheck = true){ - if (episodeList != null){ - if (!skippVersionCheck){ - var episodeVersions = episodeList.First().Versions; - if (episodeVersions != null){ - var version = episodeVersions.Find(a => a.Original); - if (version.AudioLocale != episodeList.First().AudioLocale){ - await CRUpdateSeries(episodeList.First().SeriesId, version.SeasonGuid); - return; - } - } else{ - await CRUpdateSeries(episodeList.First().SeriesId, ""); - return; - } - } - - - var firstEpisode = episodeList.First(); - var seriesId = firstEpisode.SeriesId; - var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); - if (historySeries != null){ - historySeries.HistorySeriesAddDate ??= DateTime.Now; - - await RefreshSeriesData(seriesId, historySeries); - - var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.SeasonId); - - if (historySeason != null){ - historySeason.SeasonTitle = firstEpisode.SeasonTitle; - historySeason.SeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + ""; - historySeason.SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier); - foreach (var crunchyEpisode in episodeList){ - var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id); - - if (historyEpisode == null){ - var langList = new List(); - - if (crunchyEpisode.Versions != null){ - langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); - } else{ - langList.Add(crunchyEpisode.AudioLocale); - } - - var newHistoryEpisode = new HistoryEpisode{ - EpisodeTitle = GetEpisodeTitle(crunchyEpisode), - EpisodeDescription = crunchyEpisode.Description, - EpisodeId = crunchyEpisode.Id, - Episode = crunchyEpisode.Episode, - EpisodeSeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "", - SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), - HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList), - HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales), - EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate - }; - - historySeason.EpisodesList.Add(newHistoryEpisode); - } else{ - var langList = new List(); - - if (crunchyEpisode.Versions != null){ - langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); - } else{ - langList.Add(crunchyEpisode.AudioLocale); - } - - //Update existing episode - historyEpisode.EpisodeTitle = GetEpisodeTitle(crunchyEpisode); - historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _); - historyEpisode.EpisodeDescription = crunchyEpisode.Description; - historyEpisode.EpisodeId = crunchyEpisode.Id; - historyEpisode.Episode = crunchyEpisode.Episode; - historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + ""; - historyEpisode.EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate; - - historyEpisode.HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList); - historyEpisode.HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales); - } - } - - historySeason.EpisodesList.Sort(new NumericStringPropertyComparer()); - } else{ - var newSeason = NewHistorySeason(episodeList, firstEpisode); - - newSeason.EpisodesList.Sort(new NumericStringPropertyComparer()); - - historySeries.Seasons.Add(newSeason); - newSeason.Init(); - } - - historySeries.UpdateNewEpisodes(); - } else{ - historySeries = new HistorySeries{ - SeriesTitle = firstEpisode.SeriesTitle, - SeriesId = firstEpisode.SeriesId, - Seasons =[], - HistorySeriesAddDate = DateTime.Now, - }; - crunInstance.HistoryList.Add(historySeries); - - var newSeason = NewHistorySeason(episodeList, firstEpisode); - - newSeason.EpisodesList.Sort(new NumericStringPropertyComparer()); - - await RefreshSeriesData(seriesId, historySeries); - - historySeries.Seasons.Add(newSeason); - historySeries.UpdateNewEpisodes(); - historySeries.Init(); - newSeason.Init(); - } - - SortItems(); - SortSeasons(historySeries); - } - } - - private CrSeriesBase? cachedSeries; - - private string GetEpisodeTitle(CrunchyEpisode crunchyEpisode){ - if (crunchyEpisode.Identifier.Contains("|M|")){ - if (string.IsNullOrEmpty(crunchyEpisode.Title)){ - if (crunchyEpisode.SeasonTitle.StartsWith(crunchyEpisode.SeriesTitle)){ - var splitTitle = crunchyEpisode.SeasonTitle.Split(new[]{ crunchyEpisode.SeriesTitle }, StringSplitOptions.None); - var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0]; - var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", ""); - - return cleanedTitle; - } - - return crunchyEpisode.SeasonTitle; - } - - if (crunchyEpisode.Title.StartsWith(crunchyEpisode.SeriesTitle)){ - var splitTitle = crunchyEpisode.Title.Split(new[]{ crunchyEpisode.SeriesTitle }, StringSplitOptions.None); - var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0]; - var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", ""); - - return cleanedTitle; - } - - return crunchyEpisode.Title; - } - - return crunchyEpisode.Title; - } + private SeriesDataCache? cachedSeries; private async Task RefreshSeriesData(string seriesId, HistorySeries historySeries){ - if (cachedSeries == null || (cachedSeries.Data != null && cachedSeries.Data.First().Id != seriesId)){ - cachedSeries = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true); - } else{ - if (cachedSeries?.Data != null){ - var series = cachedSeries.Data.First(); - historySeries.SeriesDescription = series.Description; - historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries); - historySeries.SeriesTitle = series.Title; - historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales); - historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales); + if (cachedSeries == null || (!string.IsNullOrEmpty(cachedSeries.SeriesId) && cachedSeries.SeriesId != seriesId)){ + if (historySeries.SeriesType == SeriesType.Series){ + var seriesData = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true); + if (seriesData is{ Data: not null }){ + var firstEpisode = seriesData.Data.First(); + cachedSeries = new SeriesDataCache{ + SeriesDescription = firstEpisode.Description, + SeriesId = seriesId, + SeriesTitle = firstEpisode.Title, + ThumbnailImageUrl = GetSeriesThumbnail(seriesData), + HistorySeriesAvailableDubLang = Languages.SortListByLangList(firstEpisode.AudioLocales), + HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(firstEpisode.SubtitleLocales) + }; + + historySeries.SeriesDescription = cachedSeries.SeriesDescription; + historySeries.ThumbnailImageUrl = cachedSeries.ThumbnailImageUrl; + historySeries.SeriesTitle = cachedSeries.SeriesTitle; + historySeries.HistorySeriesAvailableDubLang = cachedSeries.HistorySeriesAvailableDubLang; + historySeries.HistorySeriesAvailableSoftSubs = cachedSeries.HistorySeriesAvailableSoftSubs; + } + } else if (historySeries.SeriesType == SeriesType.Artist){ + var artisteData = await crunInstance.CrMusic.ParseArtistByIdAsync(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, + true); + if (!string.IsNullOrEmpty(artisteData.Id)){ + cachedSeries = new SeriesDataCache{ + SeriesDescription = artisteData.Description ?? "", + SeriesId = artisteData.Id, + SeriesTitle = artisteData.Name ?? "", + ThumbnailImageUrl = artisteData.Images.PosterTall.FirstOrDefault(e => e.Height == 360)?.Source ?? "", + HistorySeriesAvailableDubLang =[], + HistorySeriesAvailableSoftSubs =[] + }; + + historySeries.SeriesDescription = cachedSeries.SeriesDescription; + historySeries.ThumbnailImageUrl = cachedSeries.ThumbnailImageUrl; + historySeries.SeriesTitle = cachedSeries.SeriesTitle; + historySeries.HistorySeriesAvailableDubLang = cachedSeries.HistorySeriesAvailableDubLang; + historySeries.HistorySeriesAvailableSoftSubs = cachedSeries.HistorySeriesAvailableSoftSubs; + } + } + } else{ + if (cachedSeries != null){ + historySeries.SeriesDescription = cachedSeries.SeriesDescription; + historySeries.ThumbnailImageUrl = cachedSeries.ThumbnailImageUrl; + historySeries.SeriesTitle = cachedSeries.SeriesTitle; + historySeries.HistorySeriesAvailableDubLang = cachedSeries.HistorySeriesAvailableDubLang; + historySeries.HistorySeriesAvailableSoftSubs = cachedSeries.HistorySeriesAvailableSoftSubs; } - - return; - } - - if (cachedSeries?.Data != null){ - var series = cachedSeries.Data.First(); - historySeries.SeriesDescription = series.Description; - historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries); - historySeries.SeriesTitle = series.Title; - historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales); - historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales); } } @@ -441,8 +460,8 @@ public class History(){ var sortedSeriesDates = sortingDir ? CrunchyrollManager.Instance.HistoryList .OrderByDescending(s => { - var date = ParseDate(s.SonarrNextAirDate, today); - return date.HasValue ? date.Value : DateTime.MinValue; + var date = ParseDate(s.SonarrNextAirDate ?? string.Empty, today); + return date ?? DateTime.MinValue; }) .ThenByDescending(s => s.SonarrNextAirDate == "Today" ? 1 : 0) .ThenBy(s => string.IsNullOrEmpty(s.SonarrNextAirDate) ? 1 : 0) @@ -452,8 +471,8 @@ public class History(){ .OrderByDescending(s => s.SonarrNextAirDate == "Today") .ThenBy(s => s.SonarrNextAirDate == "Today" ? s.SeriesTitle : null) .ThenBy(s => { - var date = ParseDate(s.SonarrNextAirDate, today); - return date.HasValue ? date.Value : DateTime.MaxValue; + var date = ParseDate(s.SonarrNextAirDate ?? string.Empty, today); + return date ?? DateTime.MaxValue; }) .ThenBy(s => s.SeriesTitle) .ToList(); @@ -499,55 +518,40 @@ public class History(){ private string GetSeriesThumbnail(CrSeriesBase series){ // var series = await crunInstance.CrSeries.SeriesById(seriesId); - if ((series.Data ?? Array.Empty()).First().Images.PosterTall?.Count > 0){ - return series.Data.First().Images.PosterTall.First().First(e => e.Height == 360).Source; + if (series.Data != null && series.Data.First().Images.PosterTall?.Count > 0){ + var imagesPosterTall = series.Data.First().Images.PosterTall; + if (imagesPosterTall != null) return imagesPosterTall.First().First(e => e.Height == 360).Source; } return ""; } - private bool CheckStringForSpecial(string identifier){ - if (string.IsNullOrEmpty(identifier)){ - return false; - } - // Regex pattern to match any sequence that does NOT contain "|S" followed by one or more digits immediately after - string pattern = @"^(?!.*\|S\d+).*"; - - // Use Regex.IsMatch to check if the identifier matches the pattern - return Regex.IsMatch(identifier, pattern); - } - - private HistorySeason NewHistorySeason(List seasonData, CrunchyEpisode firstEpisode){ + private HistorySeason NewHistorySeason(List episodeList, IHistorySource firstEpisode){ var newSeason = new HistorySeason{ - SeasonTitle = firstEpisode.SeasonTitle, - SeasonId = firstEpisode.SeasonId, - SeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "", + SeasonTitle = firstEpisode.GetSeasonTitle(), + SeasonId = firstEpisode.GetSeasonId(), + SeasonNum = firstEpisode.GetSeasonNum(), EpisodesList =[], - SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier) + SpecialSeason = firstEpisode.IsSpecialSeason() }; - foreach (var crunchyEpisode in seasonData){ - var langList = new List(); - - if (crunchyEpisode.Versions != null){ - langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); - } else{ - langList.Add(crunchyEpisode.AudioLocale); + foreach (var historySource in episodeList){ + if (historySource.GetSeasonId() != newSeason.SeasonId){ + continue; } - Languages.SortListByLangList(langList); - var newHistoryEpisode = new HistoryEpisode{ - EpisodeTitle = GetEpisodeTitle(crunchyEpisode), - EpisodeDescription = crunchyEpisode.Description, - EpisodeId = crunchyEpisode.Id, - Episode = crunchyEpisode.Episode, - EpisodeSeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "", - SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), - HistoryEpisodeAvailableDubLang = langList, - HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales, - EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate + EpisodeTitle = historySource.GetEpisodeTitle(), + EpisodeDescription = historySource.GetEpisodeDescription(), + EpisodeId = historySource.GetEpisodeId(), + Episode = historySource.GetEpisodeNumber(), + EpisodeSeasonNum = historySource.GetSeasonNum(), + SpecialEpisode = historySource.IsSpecialEpisode(), + HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(), + HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(), + EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), + EpisodeType = historySource.GetEpisodeType() }; newSeason.EpisodesList.Add(newHistoryEpisode); @@ -563,7 +567,7 @@ public class History(){ foreach (var historySeries in crunInstance.HistoryList){ if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ - var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle); + var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty); if (sonarrSeries != null){ historySeries.SonarrSeriesId = sonarrSeries.Id + ""; historySeries.SonarrTvDbId = sonarrSeries.TvdbId + ""; @@ -581,7 +585,7 @@ public class History(){ } if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ - List? episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); + List episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); historySeries.SonarrNextAirDate = GetNextAirDate(episodes); @@ -603,8 +607,10 @@ public class History(){ historyEpisode.SonarrEpisodeId = episode.Id + ""; historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + ""; historyEpisode.SonarrHasFile = episode.HasFile; + historyEpisode.SonarrIsMonitored = episode.Monitored; historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; + lock (_lock){ episodes.Remove(episode); } @@ -622,8 +628,8 @@ public class History(){ return false; } - var episodeNumberStr = ele.EpisodeNumber.ToString() ?? string.Empty; - var seasonNumberStr = ele.SeasonNumber.ToString() ?? string.Empty; + var episodeNumberStr = ele.EpisodeNumber.ToString(); + var seasonNumberStr = ele.SeasonNumber.ToString(); return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum; }); @@ -631,6 +637,7 @@ public class History(){ historyEpisode.SonarrEpisodeId = episode.Id + ""; historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + ""; historyEpisode.SonarrHasFile = episode.HasFile; + historyEpisode.SonarrIsMonitored = episode.Monitored; historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; lock (_lock){ @@ -649,6 +656,7 @@ public class History(){ historyEpisode.SonarrEpisodeId = episode1.Id + ""; historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + ""; historyEpisode.SonarrHasFile = episode1.HasFile; + historyEpisode.SonarrIsMonitored = episode1.Monitored; historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + ""; lock (_lock){ @@ -666,6 +674,7 @@ public class History(){ historyEpisode.SonarrEpisodeId = episode2.Id + ""; historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + ""; historyEpisode.SonarrHasFile = episode2.HasFile; + historyEpisode.SonarrIsMonitored = episode2.Monitored; historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + ""; lock (_lock){ @@ -677,8 +686,6 @@ public class History(){ } } }); - - } } @@ -706,6 +713,10 @@ public class History(){ } private SonarrSeries? FindClosestMatch(string title){ + if (string.IsNullOrEmpty(title)){ + return null; + } + SonarrSeries? closestMatch = null; double highestSimilarity = 0.0; @@ -748,7 +759,7 @@ public class History(){ Parallel.ForEach(episodeList, episode => { if (episode != null){ - double similarity = CalculateSimilarity(episode.Title, title); + double similarity = CalculateSimilarity(episode.Title ?? string.Empty, title); lock (lockObject) // Ensure thread-safe access to shared variables { if (similarity > highestSimilarity){ @@ -810,13 +821,13 @@ public class History(){ } public class NumericStringPropertyComparer : IComparer{ - public int Compare(HistoryEpisode x, HistoryEpisode y){ - if (double.TryParse(x.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) && - double.TryParse(y.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){ + public int Compare(HistoryEpisode? x, HistoryEpisode? y){ + if (double.TryParse(x?.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) && + double.TryParse(y?.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){ return xDouble.CompareTo(yDouble); } // Fall back to string comparison if not parseable as doubles - return string.Compare(x.Episode, y.Episode, StringComparison.Ordinal); + return string.Compare(x?.Episode, y?.Episode, StringComparison.Ordinal); } } \ No newline at end of file diff --git a/CRD/Downloader/ProgramManager.cs b/CRD/Downloader/ProgramManager.cs index 73ebbfb..266b4a5 100644 --- a/CRD/Downloader/ProgramManager.cs +++ b/CRD/Downloader/ProgramManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -13,7 +12,6 @@ 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 FluentAvalonia.Styling; @@ -51,10 +49,10 @@ public partial class ProgramManager : ObservableObject{ private bool _updateAvailable = true; [ObservableProperty] - private bool _finishedLoading = false; + private bool _finishedLoading; [ObservableProperty] - private bool _navigationLock = false; + private bool _navigationLock; #endregion @@ -66,7 +64,7 @@ public partial class ProgramManager : ObservableObject{ private Queue> taskQueue = new Queue>(); - private bool exitOnTaskFinish = false; + private bool exitOnTaskFinish; public IStorageProvider StorageProvider; @@ -111,7 +109,7 @@ public partial class ProgramManager : ObservableObject{ await Task.WhenAll(tasks); - while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress != null && e.DownloadProgress.Done != true)){ + 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 } diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs index b71fc09..5848260 100644 --- a/CRD/Downloader/QueueManager.cs +++ b/CRD/Downloader/QueueManager.cs @@ -54,7 +54,7 @@ public class QueueManager{ if (e.Action == NotifyCollectionChangedAction.Remove){ if (e.OldItems != null) foreach (var eOldItem in e.OldItems){ - var downloadItem = DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(eOldItem)); + var downloadItem = DownloadItemModels.FirstOrDefault(downloadItem => downloadItem.epMeta.Equals(eOldItem)); if (downloadItem != null){ DownloadItemModels.Remove(downloadItem); } else{ @@ -87,13 +87,17 @@ public class QueueManager{ public async Task CrAddEpisodeToQueue(string epId, string crLocale, List dubLang, bool updateHistory = false, bool onlySubs = false){ + if (string.IsNullOrEmpty(epId)){ + return; + } + await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale); if (episodeL != null){ - if (episodeL.Value.IsPremiumOnly && !CrunchyrollManager.Instance.Profile.HasPremium){ + if (episodeL.IsPremiumOnly && !CrunchyrollManager.Instance.Profile.HasPremium){ MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode – make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3)); return; } @@ -206,6 +210,15 @@ public class QueueManager{ if (musicVideo != null){ var musicVideoMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(musicVideo); + + (HistoryEpisode? historyEpisode, List dublist, List sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", ""); + + if (CrunchyrollManager.Instance.CrunOptions.History){ + historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(musicVideoMeta.SeriesId, musicVideoMeta.SeasonId, musicVideoMeta.Data.First().MediaId); + } + + musicVideoMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo; + Queue.Add(musicVideoMeta); MessageBus.Current.SendMessage(new ToastMessage($"Added music video to the queue", ToastType.Information, 1)); } @@ -218,6 +231,14 @@ public class QueueManager{ if (concert != null){ var concertMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(concert); + + (HistoryEpisode? historyEpisode, List dublist, List sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", ""); + + if (CrunchyrollManager.Instance.CrunOptions.History){ + historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(concertMeta.SeriesId, concertMeta.SeasonId, concertMeta.Data.First().MediaId); + } + + concertMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo; Queue.Add(concertMeta); MessageBus.Current.SendMessage(new ToastMessage($"Added concert to the queue", ToastType.Information, 1)); } @@ -232,7 +253,7 @@ public class QueueManager{ foreach (var crunchyEpMeta in selected.Values.ToList()){ if (crunchyEpMeta.Data?.First() != null){ if (CrunchyrollManager.Instance.CrunOptions.History){ - var historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDownloadDir(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId); + var historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDownloadDir(crunchyEpMeta.SeriesId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId); if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ if (historyEpisode.historyEpisode != null){ if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){ @@ -258,7 +279,7 @@ public class QueueManager{ } } - var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId); + var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.SeriesId, crunchyEpMeta.SeasonId); crunchyEpMeta.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo; crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; diff --git a/CRD/Utils/DRM/Session.cs b/CRD/Utils/DRM/Session.cs index fcb97eb..39c635b 100644 --- a/CRD/Utils/DRM/Session.cs +++ b/CRD/Utils/DRM/Session.cs @@ -15,7 +15,7 @@ using ProtoBuf; namespace CRD.Utils.DRM; -public struct ContentDecryptionModule{ +public class ContentDecryptionModule{ public byte[] privateKey{ get; set; } public byte[] identifierBlob{ get; set; } } diff --git a/CRD/Utils/DRM/Widevine.cs b/CRD/Utils/DRM/Widevine.cs index 4f66e5f..cdec1a8 100644 --- a/CRD/Utils/DRM/Widevine.cs +++ b/CRD/Utils/DRM/Widevine.cs @@ -4,6 +4,7 @@ using System.IO; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using CRD.Utils.Files; namespace CRD.Utils.DRM; diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index 17f82dc..a797235 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -5,6 +5,24 @@ using Newtonsoft.Json; namespace CRD.Utils; +public enum StreamingService{ + Crunchyroll, + Unknown +} + +public enum EpisodeType{ + MusicVideo, + Concert, + Episode, + Unknown +} + +public enum SeriesType{ + Artist, + Series, + Unknown +} + [DataContract] [JsonConverter(typeof(LocaleConverter))] public enum Locale{ diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index a0db102..6afbd55 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -4,21 +4,25 @@ using System.IO; using System.IO.Compression; using System.Reflection; using System.Runtime.InteropServices; +using CRD.Downloader; using CRD.Downloader.Crunchyroll; -using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll; using Newtonsoft.Json; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace CRD.Utils; +namespace CRD.Utils.Files; public class CfgManager{ private static string WorkingDirectory = AppContext.BaseDirectory; - public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.yml"); - public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.yml"); + public static readonly string PathCrTokenOld = Path.Combine(WorkingDirectory, "config", "cr_token.yml"); + public static readonly string PathCrDownloadOptionsOld = Path.Combine(WorkingDirectory, "config", "settings.yml"); + + public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.json"); + public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.json"); + public static readonly string PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json"); public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json"); @@ -83,18 +87,36 @@ public class CfgManager{ } } - public static void WriteJsonResponseToYamlFile(string jsonResponse, string filePath){ - // Convert JSON to an object - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) // Adjust this as needed - .Build(); - var jsonObject = deserializer.Deserialize(jsonResponse); + public static void WriteCrSettings(){ + WriteJsonToFile(PathCrDownloadOptions, CrunchyrollManager.Instance.CrunOptions); + } - // Convert the object to YAML - var serializer = new SerializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention - .Build(); - var yaml = serializer.Serialize(jsonObject); + // public static void WriteTokenToYamlFile(CrToken token, string filePath){ + // // Convert the object to YAML + // var serializer = new SerializerBuilder() + // .WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention + // .Build(); + // var yaml = serializer.Serialize(token); + // + // string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty; + // + // if (!Directory.Exists(dirPath)){ + // Directory.CreateDirectory(dirPath); + // } + // + // if (!File.Exists(filePath)){ + // using (var fileStream = File.Create(filePath)){ + // } + // } + // + // // Write the YAML to a file + // File.WriteAllText(filePath, yaml); + // } + + public static void UpdateSettingsFromFile(T options, string filePath) where T : class{ + if (options == null){ + throw new ArgumentNullException(nameof(options)); + } string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty; @@ -103,74 +125,81 @@ public class CfgManager{ } if (!File.Exists(filePath)){ + // Create the file if it doesn't exist using (var fileStream = File.Create(filePath)){ } - } - - // Write the YAML to a file - File.WriteAllText(filePath, yaml); - } - - public static void WriteTokenToYamlFile(CrToken token, string filePath){ - // Convert the object to YAML - var serializer = new SerializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention - .Build(); - var yaml = serializer.Serialize(token); - - string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty; - - if (!Directory.Exists(dirPath)){ - Directory.CreateDirectory(dirPath); - } - - if (!File.Exists(filePath)){ - using (var fileStream = File.Create(filePath)){ - } - } - - // Write the YAML to a file - File.WriteAllText(filePath, yaml); - } - - public static void WriteSettingsToFile(){ - var serializer = new SerializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) // Use the underscore style - .Build(); - - string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty; - - if (!Directory.Exists(dirPath)){ - Directory.CreateDirectory(dirPath); - } - - if (!File.Exists(PathCrDownloadOptions)){ - using (var fileStream = File.Create(PathCrDownloadOptions)){ - } - } - - var yaml = serializer.Serialize(CrunchyrollManager.Instance.CrunOptions); - - // Write to file - File.WriteAllText(PathCrDownloadOptions, yaml); - } - - - public static void UpdateSettingsFromFile(CrDownloadOptions options){ - string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty; - - if (!Directory.Exists(dirPath)){ - Directory.CreateDirectory(dirPath); - } - - if (!File.Exists(PathCrDownloadOptions)){ - using (var fileStream = File.Create(PathCrDownloadOptions)){ - } return; } - var input = File.ReadAllText(PathCrDownloadOptions); + var input = File.ReadAllText(filePath); + + if (string.IsNullOrWhiteSpace(input)){ + return; + } + + // Deserialize JSON into a dictionary to get top-level properties + var propertiesPresentInJson = GetTopLevelPropertiesInJson(input); + + // Deserialize JSON into the provided options object type + var loadedOptions = JsonConvert.DeserializeObject(input); + + if (loadedOptions == null){ + return; + } + + foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)){ + // Use the JSON property name if present, otherwise use the property name + string jsonPropertyName = property.Name; + var jsonPropertyAttribute = property.GetCustomAttribute(); + if (jsonPropertyAttribute != null){ + jsonPropertyName = jsonPropertyAttribute.PropertyName ?? property.Name; + } + + if (propertiesPresentInJson.Contains(jsonPropertyName)){ + // Update the target property + var value = property.GetValue(loadedOptions); + var targetProperty = options.GetType().GetProperty(property.Name); + + if (targetProperty != null && targetProperty.CanWrite){ + targetProperty.SetValue(options, value); + } + } + } + } + + private static HashSet GetTopLevelPropertiesInJson(string jsonContent){ + var properties = new HashSet(StringComparer.OrdinalIgnoreCase); + + using (var reader = new JsonTextReader(new StringReader(jsonContent))){ + while (reader.Read()){ + if (reader.TokenType == JsonToken.PropertyName){ + properties.Add(reader.Value?.ToString() ?? string.Empty); + } + } + } + + return properties; + } + + + #region YAML OLD + + public static void UpdateSettingsFromFileYAML(CrDownloadOptionsYaml options){ + string dirPath = Path.GetDirectoryName(PathCrDownloadOptionsOld) ?? string.Empty; + + if (!Directory.Exists(dirPath)){ + Directory.CreateDirectory(dirPath); + } + + if (!File.Exists(PathCrDownloadOptionsOld)){ + using (var fileStream = File.Create(PathCrDownloadOptionsOld)){ + } + + return; + } + + var input = File.ReadAllText(PathCrDownloadOptionsOld); if (input.Length <= 0){ return; @@ -180,17 +209,18 @@ public class CfgManager{ .WithNamingConvention(UnderscoredNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); - + var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input); - var loadedOptions = deserializer.Deserialize(new StringReader(input)); + var loadedOptions = deserializer.Deserialize(new StringReader(input)); var instanceOptions = options; - foreach (PropertyInfo property in typeof(CrDownloadOptions).GetProperties()){ + foreach (PropertyInfo property in typeof(CrDownloadOptionsYaml).GetProperties()){ var yamlMemberAttribute = property.GetCustomAttribute(); - string yamlPropertyName = yamlMemberAttribute?.Alias ?? property.Name; + // var jsonMemberAttribute = property.GetCustomAttribute(); + string yamlPropertyName = yamlMemberAttribute?.Alias ?? property.Name; if (propertiesPresentInYaml.Contains(yamlPropertyName)){ - PropertyInfo instanceProperty = instanceOptions.GetType().GetProperty(property.Name); + PropertyInfo? instanceProperty = instanceOptions.GetType().GetProperty(property.Name); if (instanceProperty != null && instanceProperty.CanWrite){ instanceProperty.SetValue(instanceOptions, property.GetValue(loadedOptions)); } @@ -216,6 +246,9 @@ public class CfgManager{ return properties; } + #endregion + + public static void UpdateHistoryFile(){ if (!CrunchyrollManager.Instance.CrunOptions.History){ return; @@ -308,12 +341,32 @@ public class CfgManager{ return Directory.Exists(dirPath) && File.Exists(filePath); } - public static T DeserializeFromFile(string filePath){ - var deserializer = new DeserializerBuilder() - .Build(); + // public static T DeserializeFromFile(string filePath){ + // var deserializer = new DeserializerBuilder() + // .Build(); + // + // using (var reader = new StreamReader(filePath)){ + // return deserializer.Deserialize(reader); + // } + // } - using (var reader = new StreamReader(filePath)){ - return deserializer.Deserialize(reader); + public static T? ReadJsonFromFile(string pathToFile) where T : class{ + try{ + if (!File.Exists(pathToFile)){ + throw new FileNotFoundException($"The file at path {pathToFile} does not exist."); + } + + lock (fileLock){ + using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read)) + using (var streamReader = new StreamReader(fileStream)) + using (var jsonReader = new JsonTextReader(streamReader)){ + var serializer = new JsonSerializer(); + return serializer.Deserialize(jsonReader); + } + } + } catch (Exception ex){ + Console.Error.WriteLine($"An error occurred while reading the JSON file: {ex.Message}"); + return null; } } } \ No newline at end of file diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index 3e80b4e..6525819 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -95,7 +95,7 @@ public class HlsDownloader{ // Check if the file exists and it is not a resume download if (File.Exists(fn) && !_data.IsResume){ - string rwts = _data.Override ?? "Y"; + string rwts = !string.IsNullOrEmpty(_data.Override) ? _data.Override : "Y"; rwts = rwts.ToUpper(); // ?? "N" if (rwts.StartsWith("Y")){ @@ -304,7 +304,7 @@ public class HlsDownloader{ double downloadSpeed = downloadedBytes / (dateElapsed / 1000); int partsLeft = partsTotal - partsDownloaded; - double remainingTime = (partsLeft * (totalDownloadedBytes / partsDownloaded)) / downloadSpeed; + double remainingTime = (partsLeft * ((double)totalDownloadedBytes / partsDownloaded)) / downloadSpeed; return new Info{ Percent = percent, diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 9532199..5992515 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -15,8 +15,10 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using CRD.Downloader; using CRD.Utils.Ffmpeg_Encoding; +using CRD.Utils.Files; using CRD.Utils.JsonConv; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll; using Microsoft.Win32; using Newtonsoft.Json; @@ -747,4 +749,95 @@ public class Helpers{ return false; } } + + public static CrDownloadOptions MigrateSettings(CrDownloadOptionsYaml yaml){ + if (yaml == null){ + throw new ArgumentNullException(nameof(yaml)); + } + + return new CrDownloadOptions{ + // General Settings + AutoDownload = yaml.AutoDownload, + RemoveFinishedDownload = yaml.RemoveFinishedDownload, + Timeout = yaml.Timeout, + FsRetryTime = yaml.FsRetryTime, + Force = yaml.Force, + SimultaneousDownloads = yaml.SimultaneousDownloads, + Theme = yaml.Theme, + AccentColor = yaml.AccentColor, + BackgroundImagePath = yaml.BackgroundImagePath, + BackgroundImageOpacity = yaml.BackgroundImageOpacity, + BackgroundImageBlurRadius = yaml.BackgroundImageBlurRadius, + Override = yaml.Override, + CcTag = yaml.CcTag, + Nocleanup = yaml.Nocleanup, + History = yaml.History, + HistoryIncludeCrArtists = yaml.HistoryIncludeCrArtists, + HistoryLang = yaml.HistoryLang, + HistoryAddSpecials = yaml.HistoryAddSpecials, + HistorySkipUnmonitored = yaml.HistorySkipUnmonitored, + HistoryCountSonarr = yaml.HistoryCountSonarr, + SonarrProperties = yaml.SonarrProperties, + LogMode = yaml.LogMode, + DownloadDirPath = yaml.DownloadDirPath, + DownloadTempDirPath = yaml.DownloadTempDirPath, + DownloadToTempFolder = yaml.DownloadToTempFolder, + HistoryPageProperties = yaml.HistoryPageProperties, + SeasonsPageProperties = yaml.SeasonsPageProperties, + DownloadSpeedLimit = yaml.DownloadSpeedLimit, + ProxyEnabled = yaml.ProxyEnabled, + ProxySocks = yaml.ProxySocks, + ProxyHost = yaml.ProxyHost, + ProxyPort = yaml.ProxyPort, + ProxyUsername = yaml.ProxyUsername, + ProxyPassword = yaml.ProxyPassword, + + // Crunchyroll Settings + Hslang = yaml.Hslang, + Kstream = yaml.Kstream, + Novids = yaml.Novids, + Noaudio = yaml.Noaudio, + StreamServer = yaml.StreamServer, + QualityVideo = yaml.QualityVideo, + QualityAudio = yaml.QualityAudio, + FileName = yaml.FileName, + Numbers = yaml.Numbers, + Partsize = yaml.Partsize, + DlSubs = yaml.DlSubs, + SkipSubs = yaml.SkipSubs, + SkipSubsMux = yaml.SkipSubsMux, + SubsAddScaledBorder = yaml.SubsAddScaledBorder, + IncludeSignsSubs = yaml.IncludeSignsSubs, + SignsSubsAsForced = yaml.SignsSubsAsForced, + IncludeCcSubs = yaml.IncludeCcSubs, + CcSubsFont = yaml.CcSubsFont, + CcSubsMuxingFlag = yaml.CcSubsMuxingFlag, + Mp4 = yaml.Mp4, + VideoTitle = yaml.VideoTitle, + IncludeVideoDescription = yaml.IncludeVideoDescription, + DescriptionLang = yaml.DescriptionLang, + FfmpegOptions = yaml.FfmpegOptions, + MkvmergeOptions = yaml.MkvmergeOptions, + DefaultSub = yaml.DefaultSub, + DefaultSubSigns = yaml.DefaultSubSigns, + DefaultSubForcedDisplay = yaml.DefaultSubForcedDisplay, + DefaultAudio = yaml.DefaultAudio, + DlVideoOnce = yaml.DlVideoOnce, + KeepDubsSeperate = yaml.KeepDubsSeperate, + SkipMuxing = yaml.SkipMuxing, + SyncTiming = yaml.SyncTiming, + IsEncodeEnabled = yaml.IsEncodeEnabled, + EncodingPresetName = yaml.EncodingPresetName, + Chapters = yaml.Chapters, + DubLang = yaml.DubLang, + SelectedCalendarLanguage = yaml.SelectedCalendarLanguage, + CalendarDubFilter = yaml.CalendarDubFilter, + CustomCalendar = yaml.CustomCalendar, + CalendarHideDubs = yaml.CalendarHideDubs, + CalendarFilterByAirDate = yaml.CalendarFilterByAirDate, + CalendarShowUpcomingEpisodes = yaml.CalendarShowUpcomingEpisodes, + StreamEndpoint = yaml.StreamEndpoint, + SearchFetchFeaturedMusic = yaml.SearchFetchFeaturedMusic + }; + } } \ No newline at end of file diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index e8c7c54..1ef16cd 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -249,26 +249,26 @@ public static class ApiUrls{ public static readonly string ApiBeta = "https://beta-api.crunchyroll.com"; public static readonly string ApiN = "https://www.crunchyroll.com"; public static readonly string Anilist = "https://graphql.anilist.co"; - + public static readonly string Auth = ApiN + "/auth/v1/token"; - public static readonly string BetaAuth = ApiBeta + "/auth/v1/token"; - public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile"; - public static readonly string BetaCmsToken = ApiBeta + "/index/v2"; - public static readonly string Search = ApiBeta + "/content/v2/discover/search"; - public static readonly string Browse = ApiBeta + "/content/v2/discover/browse"; - public static readonly string Cms = ApiBeta + "/content/v2/cms"; - public static readonly string Content = ApiBeta + "/content/v2"; + public static readonly string Profile = ApiN + "/accounts/v1/me/profile"; + public static readonly string CmsToken = ApiN + "/index/v2"; + public static readonly string Search = ApiN + "/content/v2/discover/search"; + public static readonly string Browse = ApiN + "/content/v2/discover/browse"; + public static readonly string Cms = ApiN + "/content/v2/cms"; + public static readonly string Content = ApiN + "/content/v2"; + public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse"; public static readonly string BetaCms = ApiBeta + "/cms/v2"; public static readonly string DRM = ApiBeta + "/drm/v1/auth"; - public static readonly string Subscription = ApiBeta + "/subs/v3/subscriptions/"; - public static readonly string CmsN = ApiN + "/content/v2/cms"; + public static readonly string Subscription = ApiN + "/subs/v3/subscriptions/"; public static readonly string authBasic = "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6"; - public static readonly string authBasicMob = "Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o="; + public static readonly string authBasicMob = "Basic ZG1yeWZlc2NkYm90dWJldW56NXo6NU45aThPV2cyVmtNcm1oekNfNUNXekRLOG55SXo0QU0="; public static readonly string authBasicSwitch = "Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4="; public static readonly string ChromeUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"; + public static readonly string MobileUserAgent = "Crunchyroll/3.74.2 Android/14 okhttp/4.12.0"; } \ No newline at end of file diff --git a/CRD/Utils/Muxing/FontsManager.cs b/CRD/Utils/Muxing/FontsManager.cs index 8806ea4..9bc67aa 100644 --- a/CRD/Utils/Muxing/FontsManager.cs +++ b/CRD/Utils/Muxing/FontsManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using CRD.Utils.Files; using CRD.Utils.Structs; namespace CRD.Utils.Muxing; diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 0f9ff05..6169a0a 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using CRD.Downloader.Crunchyroll; +using CRD.Utils.Files; using CRD.Utils.Structs; namespace CRD.Utils.Muxing; @@ -332,18 +333,25 @@ public class Merger{ Time = GetTimeFromFileName(fp, extractFramesCompareEnd.frameRate) }).ToList(); + + // Calculate offsets var startOffset = SyncingHelper.CalculateOffset(baseFramesStart, compareFramesStart); var endOffset = SyncingHelper.CalculateOffset(baseFramesEnd, compareFramesEnd,true); - var lengthDiff = Math.Abs(baseVideoDurationTimeSpan.Value.TotalMicroseconds - compareVideoDurationTimeSpan.Value.TotalMicroseconds) / 1000000; - + var lengthDiff = (baseVideoDurationTimeSpan.Value.TotalMicroseconds - compareVideoDurationTimeSpan.Value.TotalMicroseconds) / 1000000; + endOffset += lengthDiff; Console.WriteLine($"Start offset: {startOffset} seconds"); Console.WriteLine($"End offset: {endOffset} seconds"); CleanupDirectory(cleanupDir); + + baseFramesStart.Clear(); + baseFramesEnd.Clear(); + compareFramesStart.Clear(); + compareFramesEnd.Clear(); var difference = Math.Abs(startOffset - endOffset); @@ -370,7 +378,7 @@ public class Merger{ private static double GetTimeFromFileName(string fileName, double frameRate){ var match = Regex.Match(Path.GetFileName(fileName), @"frame(\d+)"); if (match.Success){ - return int.Parse(match.Groups[1].Value) / frameRate; // Assuming 30 fps + return int.Parse(match.Groups[1].Value) / frameRate; } return 0; diff --git a/CRD/Utils/Muxing/SyncingHelper.cs b/CRD/Utils/Muxing/SyncingHelper.cs index b309caf..f84813f 100644 --- a/CRD/Utils/Muxing/SyncingHelper.cs +++ b/CRD/Utils/Muxing/SyncingHelper.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using CRD.Utils.Files; using CRD.Utils.Structs; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -16,7 +17,7 @@ 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%03d.png\""; + 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 output = ""; @@ -68,7 +69,7 @@ public class SyncingHelper{ return 0; } - private static double CalculateSSIM(float[] pixels1, float[] pixels2, int width, int height){ + private static double CalculateSSIM(float[] pixels1, float[] pixels2){ double mean1 = pixels1.Average(); double mean2 = pixels2.Average(); @@ -110,7 +111,7 @@ public class SyncingHelper{ return pixels; } - public static double ComputeSSIM(string imagePath1, string imagePath2, int targetWidth, int targetHeight){ + public static (double ssim, double pixelDiff) ComputeSSIM(string imagePath1, string imagePath2, int targetWidth, int targetHeight){ using (var image1 = Image.Load(imagePath1)) using (var image2 = Image.Load(imagePath2)){ // Preprocess images (resize and convert to grayscale) @@ -131,13 +132,24 @@ public class SyncingHelper{ // Check if any frame is completely black, if so, skip SSIM calculation if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2)){ // Return a negative value or zero to indicate no SSIM comparison for black frames. - return -1.0; + return (-1.0,99); } // Compute SSIM - return CalculateSSIM(pixels1, pixels2, targetWidth, targetHeight); + return (CalculateSSIM(pixels1, pixels2),CalculatePixelDifference(pixels1,pixels2)); } } + + private static double CalculatePixelDifference(float[] pixels1, float[] pixels2){ + double totalDifference = 0; + int count = pixels1.Length; + + for (int i = 0; i < count; i++){ + totalDifference += Math.Abs(pixels1[i] - pixels2[i]); + } + + return totalDifference / count; // Average difference + } private static bool IsBlackFrame(float[] pixels, float threshold = 1.0f){ // Check if all pixel values are below the threshold, indicating a black frame. @@ -145,10 +157,13 @@ public class SyncingHelper{ } public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){ - double ssim = ComputeSSIM(imagePath1, imagePath2, 256, 256); + var (ssim, pixelDiff) = ComputeSSIM(imagePath1, imagePath2, 256, 256); // Console.WriteLine($"SSIM: {ssim}"); - return ssim > ssimThreshold; + // Console.WriteLine(pixelDiff); + + return ssim > ssimThreshold && pixelDiff < 10; } + public static double CalculateOffset(List baseFrames, List compareFrames,bool reverseCompare = false, double ssimThreshold = 0.9){ @@ -160,7 +175,9 @@ public class SyncingHelper{ foreach (var baseFrame in baseFrames){ var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold)); if (matchingFrame != null){ - Console.WriteLine($"Matched Frame: Base Frame Time: {baseFrame.Time}, Compare Frame Time: {matchingFrame.Time}"); + 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; } else{ // Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}"); diff --git a/CRD/Utils/Parser/MPDTransformer.cs b/CRD/Utils/Parser/MPDTransformer.cs index 1cf14bb..7583992 100644 --- a/CRD/Utils/Parser/MPDTransformer.cs +++ b/CRD/Utils/Parser/MPDTransformer.cs @@ -87,7 +87,7 @@ public static class MPDParser{ throw new NotImplementedException(); } - var foundLanguage = Languages.FindLang(Languages.languages.FirstOrDefault(a => a.Code == item.language).CrLocale ?? "unknown"); + var foundLanguage = Languages.FindLang(Languages.languages.FirstOrDefault(a => a.Code == item.language)?.CrLocale ?? "unknown"); LanguageItem? audioLang = item.language != null ? foundLanguage : (language != null ? language : foundLanguage); var pItem = new AudioPlaylist{ diff --git a/CRD/Utils/Structs/Chapters.cs b/CRD/Utils/Structs/Chapters.cs index 86d1f9e..b5b3377 100644 --- a/CRD/Utils/Structs/Chapters.cs +++ b/CRD/Utils/Structs/Chapters.cs @@ -4,13 +4,13 @@ using Newtonsoft.Json; namespace CRD.Utils.Structs; -public struct CrunchyChapters{ +public class CrunchyChapters{ public List Chapters { get; set; } public DateTime lastUpdate { get; set; } public string? mediaId { get; set; } } -public struct CrunchyChapter{ +public class CrunchyChapter{ public string approverId { get; set; } public string distributionNumber { get; set; } public double? end { get; set; } @@ -22,7 +22,7 @@ public struct CrunchyChapter{ public string type { get; set; } } -public struct CrunchyOldChapter{ +public class CrunchyOldChapter{ public string media_id { get; set; } public double startTime { get; set; } public double endTime { get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index 8fda39e..7d77cbc 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -1,13 +1,261 @@ using System.Collections.Generic; using CRD.Utils.Sonarr; using CRD.ViewModels; +using Newtonsoft.Json; using YamlDotNet.Serialization; -namespace CRD.Utils.Structs; +namespace CRD.Utils.Structs.Crunchyroll; public class CrDownloadOptions{ #region General Settings + [JsonProperty("auto_download")] + public bool AutoDownload{ get; set; } + + [JsonProperty("remove_finished_downloads")] + public bool RemoveFinishedDownload{ get; set; } + + [JsonIgnore] + public int Timeout{ get; set; } + + [JsonIgnore] + public int FsRetryTime{ get; set; } + + [JsonIgnore] + public string Force{ get; set; } = ""; + + [JsonProperty("simultaneous_downloads")] + public int SimultaneousDownloads{ get; set; } + + [JsonProperty("theme")] + public string Theme{ get; set; } = ""; + + [JsonProperty("accent_color")] + public string? AccentColor{ get; set; } + + [JsonProperty("background_image_path")] + public string? BackgroundImagePath{ get; set; } + + [JsonProperty("background_image_opacity")] + public double BackgroundImageOpacity{ get; set; } + + [JsonProperty("background_image_blur_radius")] + public double BackgroundImageBlurRadius{ get; set; } + + [JsonIgnore] + public List Override{ get; set; } =[]; + + [JsonIgnore] + public string CcTag{ get; set; } = ""; + + [JsonIgnore] + public bool Nocleanup{ get; set; } + + [JsonProperty("history")] + public bool History{ get; set; } + + [JsonProperty("history_include_cr_artists")] + public bool HistoryIncludeCrArtists{ get; set; } + + [JsonProperty("history_lang")] + public string? HistoryLang{ get; set; } + + [JsonProperty("history_add_specials")] + public bool HistoryAddSpecials{ get; set; } + + [JsonProperty("history_skip_unmonitored")] + public bool HistorySkipUnmonitored{ get; set; } + + [JsonProperty("history_count_sonarr")] + public bool HistoryCountSonarr{ get; set; } + + [JsonProperty("sonarr_properties")] + public SonarrProperties? SonarrProperties{ get; set; } + + [JsonProperty("log_mode")] + public bool LogMode{ get; set; } + + [JsonProperty("download_dir_path")] + public string? DownloadDirPath{ get; set; } + + [JsonProperty("download_temp_dir_path")] + public string? DownloadTempDirPath{ get; set; } + + [JsonProperty("download_to_temp_folder")] + public bool DownloadToTempFolder{ get; set; } + + [JsonProperty("history_page_properties")] + public HistoryPageProperties? HistoryPageProperties{ get; set; } + + [JsonProperty("seasons_page_properties")] + public SeasonsPageProperties? SeasonsPageProperties{ get; set; } + + [JsonProperty("download_speed_limit")] + public int DownloadSpeedLimit{ get; set; } + + [JsonProperty("proxy_enabled")] + public bool ProxyEnabled{ get; set; } + + [JsonProperty("proxy_socks")] + public bool ProxySocks{ get; set; } + + [JsonProperty("proxy_host")] + public string? ProxyHost{ get; set; } + + [JsonProperty("proxy_port")] + public int ProxyPort{ get; set; } + + [JsonProperty("proxy_username")] + public string? ProxyUsername{ get; set; } + + [JsonProperty("proxy_password")] + public string? ProxyPassword{ get; set; } + + #endregion + + #region Crunchyroll Settings + + [JsonProperty("hard_sub_lang")] + public string Hslang{ get; set; } = ""; + + [JsonIgnore] + public int Kstream{ get; set; } + + [JsonProperty("no_video")] + public bool Novids{ get; set; } + + [JsonProperty("no_audio")] + public bool Noaudio{ get; set; } + + [JsonIgnore] + public int StreamServer{ get; set; } + + [JsonProperty("quality_video")] + public string QualityVideo{ get; set; } = ""; + + [JsonProperty("quality_audio")] + public string QualityAudio{ get; set; } = ""; + + [JsonProperty("file_name")] + public string FileName{ get; set; } = ""; + + [JsonProperty("leading_numbers")] + public int Numbers{ get; set; } + + [JsonProperty("download_part_size")] + public int Partsize{ get; set; } + + [JsonProperty("soft_subs")] + public List DlSubs{ get; set; } =[]; + + [JsonIgnore] + public bool SkipSubs{ get; set; } + + [JsonProperty("mux_skip_subs")] + public bool SkipSubsMux{ get; set; } + + [JsonProperty("subs_add_scaled_border")] + public ScaledBorderAndShadowSelection SubsAddScaledBorder{ get; set; } + + [JsonProperty("include_signs_subs")] + public bool IncludeSignsSubs{ get; set; } + + [JsonProperty("mux_signs_subs_flag")] + public bool SignsSubsAsForced{ get; set; } + + [JsonProperty("include_cc_subs")] + public bool IncludeCcSubs{ get; set; } + + [JsonProperty("cc_subs_font")] + public string? CcSubsFont{ get; set; } + + [JsonProperty("mux_cc_subs_flag")] + public bool CcSubsMuxingFlag{ get; set; } + + [JsonProperty("mux_mp4")] + public bool Mp4{ get; set; } + + [JsonProperty("mux_video_title")] + public string? VideoTitle{ get; set; } + + [JsonProperty("mux_video_description")] + public bool IncludeVideoDescription{ get; set; } + + [JsonProperty("mux_description_lang")] + public string? DescriptionLang{ get; set; } + + [JsonProperty("mux_ffmpeg")] + public List FfmpegOptions{ get; set; } =[]; + + [JsonProperty("mux_mkvmerge")] + public List MkvmergeOptions{ get; set; } =[]; + + [JsonProperty("mux_default_sub")] + public string DefaultSub{ get; set; } = ""; + + [JsonProperty("mux_default_sub_signs")] + public bool DefaultSubSigns{ get; set; } + + [JsonProperty("mux_default_sub_forced_display")] + public bool DefaultSubForcedDisplay{ get; set; } + + [JsonProperty("mux_default_dub")] + public string DefaultAudio{ get; set; } = ""; + + [JsonProperty("dl_video_once")] + public bool DlVideoOnce{ get; set; } + + [JsonProperty("keep_dubs_seperate")] + public bool KeepDubsSeperate{ get; set; } + + [JsonProperty("mux_skip_muxing")] + public bool SkipMuxing{ get; set; } + + [JsonProperty("mux_sync_dubs")] + public bool SyncTiming{ get; set; } + + [JsonProperty("encode_enabled")] + public bool IsEncodeEnabled{ get; set; } + + [JsonProperty("encode_preset")] + public string? EncodingPresetName{ get; set; } + + [JsonProperty("chapters")] + public bool Chapters{ get; set; } + + [JsonProperty("dub_lang")] + public List DubLang{ get; set; } =[]; + + [JsonProperty("calendar_language")] + public string? SelectedCalendarLanguage{ get; set; } + + [JsonProperty("calendar_dub_filter")] + public string? CalendarDubFilter{ get; set; } + + [JsonProperty("calendar_custom")] + public bool CustomCalendar{ get; set; } + + [JsonProperty("calendar_hide_dubs")] + public bool CalendarHideDubs{ get; set; } + + [JsonProperty("calendar_filter_by_air_date")] + public bool CalendarFilterByAirDate{ get; set; } + + [JsonProperty("calendar_show_upcoming_episodes")] + public bool CalendarShowUpcomingEpisodes{ get; set; } + + [JsonProperty("stream_endpoint")] + public string? StreamEndpoint{ get; set; } + + [JsonProperty("search_fetch_featured_music")] + public bool SearchFetchFeaturedMusic{ get; set; } + + #endregion +} + +public class CrDownloadOptionsYaml{ + #region General Settings + [YamlMember(Alias = "auto_download", ApplyNamingConventions = false)] public bool AutoDownload{ get; set; } @@ -21,13 +269,13 @@ public class CrDownloadOptions{ public int FsRetryTime{ get; set; } [YamlIgnore] - public string Force{ get; set; } + public string Force{ get; set; } = ""; [YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)] public int SimultaneousDownloads{ get; set; } [YamlMember(Alias = "theme", ApplyNamingConventions = false)] - public string Theme{ get; set; } + public string Theme{ get; set; } = ""; [YamlMember(Alias = "accent_color", ApplyNamingConventions = false)] public string? AccentColor{ get; set; } @@ -43,23 +291,29 @@ public class CrDownloadOptions{ [YamlIgnore] - public List Override{ get; set; } + public List Override{ get; } =[]; [YamlIgnore] - public string CcTag{ get; set; } + public string CcTag{ get; set; } = ""; [YamlIgnore] - public bool Nocleanup{ get; set; } + public bool Nocleanup{ get; } = false; [YamlMember(Alias = "history", ApplyNamingConventions = false)] public bool History{ get; set; } + [YamlMember(Alias = "history_include_cr_artists", ApplyNamingConventions = false)] + public bool HistoryIncludeCrArtists{ get; set; } + [YamlMember(Alias = "history_lang", ApplyNamingConventions = false)] public string? HistoryLang{ get; set; } [YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)] public bool HistoryAddSpecials{ get; set; } + [YamlMember(Alias = "history_skip_unmonitored", ApplyNamingConventions = false)] + public bool HistorySkipUnmonitored{ get; set; } + [YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)] public bool HistoryCountSonarr{ get; set; } @@ -80,7 +334,7 @@ public class CrDownloadOptions{ [YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)] public HistoryPageProperties? HistoryPageProperties{ get; set; } - + [YamlMember(Alias = "seasons_page_properties", ApplyNamingConventions = false)] public SeasonsPageProperties? SeasonsPageProperties{ get; set; } @@ -89,7 +343,7 @@ public class CrDownloadOptions{ [YamlMember(Alias = "proxy_enabled", ApplyNamingConventions = false)] public bool ProxyEnabled{ get; set; } - + [YamlMember(Alias = "proxy_socks", ApplyNamingConventions = false)] public bool ProxySocks{ get; set; } @@ -98,20 +352,20 @@ public class CrDownloadOptions{ [YamlMember(Alias = "proxy_port", ApplyNamingConventions = false)] public int ProxyPort{ get; set; } - + [YamlMember(Alias = "proxy_username", ApplyNamingConventions = false)] public string? ProxyUsername{ get; set; } [YamlMember(Alias = "proxy_password", ApplyNamingConventions = false)] public string? ProxyPassword{ get; set; } - + #endregion #region Crunchyroll Settings [YamlMember(Alias = "hard_sub_lang", ApplyNamingConventions = false)] - public string Hslang{ get; set; } + public string Hslang{ get; set; } = ""; [YamlIgnore] public int Kstream{ get; set; } @@ -126,23 +380,23 @@ public class CrDownloadOptions{ public int StreamServer{ get; set; } [YamlMember(Alias = "quality_video", ApplyNamingConventions = false)] - public string QualityVideo{ get; set; } + public string QualityVideo{ get; set; } = ""; [YamlMember(Alias = "quality_audio", ApplyNamingConventions = false)] - public string QualityAudio{ get; set; } + public string QualityAudio{ get; set; } = ""; [YamlMember(Alias = "file_name", ApplyNamingConventions = false)] - public string FileName{ get; set; } + public string FileName{ get; set; } = ""; [YamlMember(Alias = "leading_numbers", ApplyNamingConventions = false)] public int Numbers{ get; set; } - [YamlIgnore] + [YamlMember(Alias = "download_part_size", ApplyNamingConventions = false)] public int Partsize{ get; set; } [YamlMember(Alias = "soft_subs", ApplyNamingConventions = false)] - public List DlSubs{ get; set; } + public List DlSubs{ get; set; } =[]; [YamlIgnore] public bool SkipSubs{ get; set; } @@ -181,13 +435,13 @@ public class CrDownloadOptions{ public string? DescriptionLang{ get; set; } [YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)] - public List FfmpegOptions{ get; set; } + public List FfmpegOptions{ get; set; } =[]; [YamlMember(Alias = "mux_mkvmerge", ApplyNamingConventions = false)] - public List MkvmergeOptions{ get; set; } + public List MkvmergeOptions{ get; set; } =[]; [YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)] - public string DefaultSub{ get; set; } + public string DefaultSub{ get; set; } = ""; [YamlMember(Alias = "mux_default_sub_signs", ApplyNamingConventions = false)] public bool DefaultSubSigns{ get; set; } @@ -196,7 +450,7 @@ public class CrDownloadOptions{ public bool DefaultSubForcedDisplay{ get; set; } [YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)] - public string DefaultAudio{ get; set; } + public string DefaultAudio{ get; set; } = ""; [YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)] public bool DlVideoOnce{ get; set; } @@ -220,8 +474,7 @@ public class CrDownloadOptions{ public bool Chapters{ get; set; } [YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)] - public List DubLang{ get; set; } - + public List DubLang{ get; set; } =[]; [YamlMember(Alias = "calendar_language", ApplyNamingConventions = false)] public string? SelectedCalendarLanguage{ get; set; } @@ -237,12 +490,15 @@ public class CrDownloadOptions{ [YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)] public bool CalendarFilterByAirDate{ get; set; } - + [YamlMember(Alias = "calendar_show_upcoming_episodes", ApplyNamingConventions = false)] public bool CalendarShowUpcomingEpisodes{ get; set; } - + [YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)] public string? StreamEndpoint{ get; set; } + [YamlMember(Alias = "search_fetch_featured_music", ApplyNamingConventions = false)] + public bool SearchFetchFeaturedMusic{ get; set; } + #endregion } \ No newline at end of file diff --git a/CRD/Utils/Structs/Crunchyroll/CrMovie.cs b/CRD/Utils/Structs/Crunchyroll/CrMovie.cs index 9651efd..b6f8d0a 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrMovie.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrMovie.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace CRD.Utils.Structs; -public struct CrunchyMovieList{ +public class CrunchyMovieList{ public int Total{ get; set; } public List? Data{ get; set; } public Meta Meta{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/CrBrowseEpisode.cs b/CRD/Utils/Structs/Crunchyroll/Episode/CrBrowseEpisode.cs index f0ddaca..101f02c 100644 --- a/CRD/Utils/Structs/Crunchyroll/Episode/CrBrowseEpisode.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/CrBrowseEpisode.cs @@ -185,7 +185,7 @@ public class CrBrowseEpisodeMetaData{ } -public struct CrBrowseEpisodeVersion{ +public class CrBrowseEpisodeVersion{ [JsonProperty("audio_locale")] public Locale? AudioLocale{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs index afb8b12..7f6f4a1 100644 --- a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs @@ -1,16 +1,19 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using CRD.Utils.Structs.History; using Newtonsoft.Json; namespace CRD.Utils.Structs; -public struct CrunchyEpisodeList{ +public class CrunchyEpisodeList{ public int Total{ get; set; } public List? Data{ get; set; } public Meta Meta{ get; set; } } -public struct CrunchyEpisode{ +public class CrunchyEpisode : IHistorySource{ [JsonProperty("next_episode_id")] public string NextEpisodeId{ get; set; } @@ -81,7 +84,7 @@ public struct CrunchyEpisode{ [JsonProperty("audio_locale")] public string AudioLocale{ get; set; } - public string Id{ get; set; } + public required string Id{ get; set; } [JsonProperty("media_type")] public string? MediaType{ get; set; } @@ -173,9 +176,123 @@ public struct CrunchyEpisode{ [JsonProperty("__links__")] public Links? Links{ get; set; } + + [JsonIgnore] + public EpisodeType EpisodeType{ get; set; } = EpisodeType.Episode; + + #region Interface + + public string GetSeriesId(){ + return SeriesId; + } + + public string GetSeriesTitle(){ + return SeriesTitle; + } + + public string GetSeasonTitle(){ + return SeasonTitle; + } + + public string GetSeasonNum(){ + return Helpers.ExtractNumberAfterS(Identifier) ?? SeasonNumber + ""; + } + + public string GetSeasonId(){ + return SeasonId; + } + + public string GetEpisodeId(){ + return Id; + } + + public string GetEpisodeNumber(){ + return Episode ?? ""; + } + + public string GetEpisodeTitle(){ + if (Identifier.Contains("|M|")){ + if (string.IsNullOrEmpty(Title)){ + if (SeasonTitle.StartsWith(SeriesTitle)){ + var splitTitle = SeasonTitle.Split(new[]{ SeriesTitle }, StringSplitOptions.None); + var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0]; + var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", ""); + + return cleanedTitle; + } + + return SeasonTitle; + } + + if (Title.StartsWith(SeriesTitle)){ + var splitTitle = Title.Split(new[]{ SeriesTitle }, StringSplitOptions.None); + var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0]; + var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", ""); + + return cleanedTitle; + } + + return Title; + } + + return Title; + } + + public string GetEpisodeDescription(){ + return Description; + } + + public bool IsSpecialSeason(){ + if (string.IsNullOrEmpty(Identifier)){ + return false; + } + + // does NOT contain "|S" followed by one or more digits immediately after + string pattern = @"^(?!.*\|S\d+).*"; + + return Regex.IsMatch(Identifier, pattern); + } + + public bool IsSpecialEpisode(){ + return !int.TryParse(Episode, out _); + } + + public List GetAnimeIds(){ + return[]; + } + + public List GetEpisodeAvailableDubLang(){ + var langList = new List(); + + if (Versions != null){ + langList.AddRange(Versions.Select(version => version.AudioLocale)); + } else{ + langList.Add(AudioLocale); + } + + return Languages.SortListByLangList(langList); + } + + public List GetEpisodeAvailableSoftSubs(){ + return Languages.SortListByLangList(SubtitleLocales); + } + + public DateTime GetAvailableDate(){ + return PremiumAvailableDate; + } + + public SeriesType GetSeriesType(){ + return SeriesType.Series; + } + + public EpisodeType GetEpisodeType(){ + return EpisodeType; + } + + #endregion } -public struct Images{ +public class Images{ [JsonProperty("poster_tall")] public List>? PosterTall{ get; set; } @@ -188,14 +305,14 @@ public struct Images{ public List>? Thumbnail{ get; set; } } -public struct Image{ +public class Image{ public int Height{ get; set; } public string Source{ get; set; } public ImageType Type{ get; set; } public int Width{ get; set; } } -public struct EpisodeVersion{ +public class EpisodeVersion{ [JsonProperty("audio_locale")] public string AudioLocale{ get; set; } @@ -215,11 +332,11 @@ public struct EpisodeVersion{ public string Variant{ get; set; } } -public struct Link{ +public class Link{ public string Href{ get; set; } } -public struct Links(){ +public class Links(){ public Dictionary LinkMappings{ get; set; } = new(){ { "episode/channel", default }, { "episode/next_episode", default }, @@ -230,7 +347,7 @@ public struct Links(){ } public class CrunchyEpMeta{ - public List? Data{ get; set; } + public List Data{ get; set; } =[]; public string? SeriesTitle{ get; set; } public string? SeasonTitle{ get; set; } @@ -239,11 +356,11 @@ public class CrunchyEpMeta{ public string? Description{ get; set; } public string? SeasonId{ get; set; } public string? Season{ get; set; } - public string? ShowId{ get; set; } + public string? SeriesId{ get; set; } public string? AbsolutEpisodeNumberE{ get; set; } public string? Image{ get; set; } public bool Paused{ get; set; } - public DownloadProgress? DownloadProgress{ get; set; } + public DownloadProgress DownloadProgress{ get; set; } = new(); public List? SelectedDubs{ get; set; } @@ -259,7 +376,7 @@ public class CrunchyEpMeta{ public string Resolution{ get; set; } public List downloadedFiles{ get; set; } =[]; - + public bool OnlySubs{ get; set; } } @@ -274,7 +391,7 @@ public class DownloadProgress{ public double DownloadSpeed{ get; set; } } -public struct CrunchyEpMetaData{ +public class CrunchyEpMetaData{ public string MediaId{ get; set; } public LanguageItem? Lang{ get; set; } public string? Playback{ get; set; } @@ -283,7 +400,7 @@ public struct CrunchyEpMetaData{ public bool IsDubbed{ get; set; } } -public struct CrunchyRollEpisodeData{ +public class CrunchyRollEpisodeData{ public string Key{ get; set; } public EpisodeAndLanguage EpisodeAndLanguages{ get; set; } } \ No newline at end of file diff --git a/CRD/Utils/Structs/Crunchyroll/Music/CrArtist.cs b/CRD/Utils/Structs/Crunchyroll/Music/CrArtist.cs new file mode 100644 index 0000000..55c2258 --- /dev/null +++ b/CRD/Utils/Structs/Crunchyroll/Music/CrArtist.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs.Crunchyroll.Music; + + +public class CrunchyArtistList{ + public int Total{ get; set; } + public List Data{ get; set; } =[]; + public Meta? Meta{ get; set; } +} + +public class CrArtist{ + [JsonProperty("description")] + public string? Description{ get; set; } + + [JsonProperty("name")] + public string? Name{ get; set; } + + [JsonProperty("slug")] + public string? Slug{ get; set; } + + [JsonProperty("type")] + public string? Type{ get; set; } + + [JsonProperty("id")] + public string? Id{ get; set; } + + [JsonProperty("publishDate")] + public DateTime? PublishDate{ get; set; } + + public MusicImages Images{ get; set; } = new(); + +} \ No newline at end of file diff --git a/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs index a7b49c5..312ab10 100644 --- a/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs +++ b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs @@ -1,132 +1,207 @@ using System; using System.Collections.Generic; +using CRD.Utils.Structs.History; using Newtonsoft.Json; namespace CRD.Utils.Structs.Crunchyroll.Music; -public struct CrunchyMusicVideoList{ +public class CrunchyMusicVideoList{ public int Total{ get; set; } - public List? Data{ get; set; } - public Meta Meta{ get; set; } + public List Data{ get; set; } =[]; + public Meta? Meta{ get; set; } } -public class CrunchyMusicVideo{ - +public class CrunchyMusicVideo : IHistorySource{ [JsonProperty("copyright")] public string? Copyright{ get; set; } - + [JsonProperty("hash")] public string? Hash{ get; set; } - + [JsonProperty("availability")] public MusicVideoAvailability? Availability{ get; set; } - + [JsonProperty("isMature")] public bool IsMature{ get; set; } - + [JsonProperty("maturityRatings")] public object? MaturityRatings{ get; set; } - + [JsonProperty("title")] public string? Title{ get; set; } - + [JsonProperty("artists")] public object? Artists{ get; set; } - + [JsonProperty("displayArtistNameRequired")] public bool DisplayArtistNameRequired{ get; set; } - + [JsonProperty("streams_link")] public string? StreamsLink{ get; set; } - + [JsonProperty("matureBlocked")] public bool MatureBlocked{ get; set; } - + [JsonProperty("originalRelease")] public DateTime OriginalRelease{ get; set; } - + [JsonProperty("sequenceNumber")] public int SequenceNumber{ get; set; } - + [JsonProperty("type")] public string? Type{ get; set; } - + [JsonProperty("animeIds")] public List? AnimeIds{ get; set; } - + [JsonProperty("description")] public string? Description{ get; set; } - + [JsonProperty("durationMs")] public int DurationMs{ get; set; } - + [JsonProperty("licensor")] public string? Licensor{ get; set; } - + [JsonProperty("slug")] public string? Slug{ get; set; } - + [JsonProperty("artist")] - public MusicVideoArtist? Artist{ get; set; } - + public required MusicVideoArtist Artist{ get; set; } + [JsonProperty("isPremiumOnly")] public bool IsPremiumOnly{ get; set; } - + [JsonProperty("isPublic")] public bool IsPublic{ get; set; } - + [JsonProperty("publishDate")] public DateTime PublishDate{ get; set; } - + [JsonProperty("displayArtistName")] public string? DisplayArtistName{ get; set; } - + [JsonProperty("genres")] - public object? genres{ get; set; } - + public object? Genres{ get; set; } + [JsonProperty("readyToPublish")] public bool ReadyToPublish{ get; set; } - + [JsonProperty("id")] - public string? Id{ get; set; } - + public required string Id{ get; set; } + [JsonProperty("createdAt")] public DateTime CreatedAt{ get; set; } public MusicImages? Images{ get; set; } - + [JsonProperty("updatedAt")] public DateTime UpdatedAt{ get; set; } + + [JsonIgnore] + public EpisodeType EpisodeType{ get; set; } = EpisodeType.MusicVideo; + + #region Interface + + public string GetSeriesId(){ + return Artist.Id; + } + + public string GetSeriesTitle(){ + return Artist.Name; + } + + public string GetSeasonTitle(){ + return EpisodeType == EpisodeType.MusicVideo ? "Music Videos" : "Concerts"; + } + + public string GetSeasonNum(){ + return EpisodeType == EpisodeType.MusicVideo ? "1" : "2"; + } + + public string GetSeasonId(){ + return EpisodeType == EpisodeType.MusicVideo ? "MusicVideos" : "Concerts"; + } + + public string GetEpisodeId(){ + return Id; + } + + public string GetEpisodeNumber(){ + return SequenceNumber + ""; + } + + public string GetEpisodeTitle(){ + return Title ?? ""; + } + + public string GetEpisodeDescription(){ + return Description ?? ""; + } + + public bool IsSpecialSeason(){ + return false; + } + + public bool IsSpecialEpisode(){ + return false; + } + + public List GetAnimeIds(){ + return AnimeIds ?? []; + } + public List GetEpisodeAvailableDubLang(){ + return[]; + } + + public List GetEpisodeAvailableSoftSubs(){ + return[]; + } + + public DateTime GetAvailableDate(){ + return PublishDate; + } + + public SeriesType GetSeriesType(){ + return SeriesType.Artist; + } + + public EpisodeType GetEpisodeType(){ + return EpisodeType; + } + + #endregion } -public struct MusicImages{ +public class MusicImages{ [JsonProperty("poster_tall")] - public List? PosterTall{ get; set; } + public List PosterTall{ get; set; } =[]; [JsonProperty("poster_wide")] - public List? PosterWide{ get; set; } + public List PosterWide{ get; set; } =[]; [JsonProperty("promo_image")] - public List? PromoImage{ get; set; } + public List PromoImage{ get; set; } =[]; - public List? Thumbnail{ get; set; } + public List Thumbnail{ get; set; } =[]; } -public struct MusicVideoArtist{ +public class MusicVideoArtist{ [JsonProperty("id")] - public string? Id{ get; set; } + public required string Id{ get; set; } + [JsonProperty("name")] - public string? Name{ get; set; } + public required string Name{ get; set; } + [JsonProperty("slug")] public string? Slug{ get; set; } - } -public struct MusicVideoAvailability{ +public class MusicVideoAvailability{ [JsonProperty("endDate")] public DateTime EndDate{ get; set; } + [JsonProperty("startDate")] public DateTime StartDate{ get; set; } - } \ No newline at end of file diff --git a/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesBase.cs b/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesBase.cs index f397a82..3953aea 100644 --- a/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesBase.cs +++ b/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesBase.cs @@ -9,7 +9,7 @@ public class CrSeriesBase{ public Meta Meta{ get; set; } } -public struct SeriesBaseItem{ +public class SeriesBaseItem{ [JsonProperty("extended_maturity_rating")] public Dictionary ExtendedMaturityRating{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesSearch.cs b/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesSearch.cs index 0f707b0..700c5cf 100644 --- a/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesSearch.cs +++ b/CRD/Utils/Structs/Crunchyroll/Series/CrSeriesSearch.cs @@ -9,7 +9,7 @@ public class CrSeriesSearch{ public Meta Meta{ get; set; } } -public struct SeriesSearchItem{ +public class SeriesSearchItem{ public string Description{ get; set; } [JsonProperty("seo_description")] @@ -91,7 +91,7 @@ public struct SeriesSearchItem{ public string SeoTitle{ get; set; } } -public struct Version{ +public class Version{ [JsonProperty("audio_locale")] public string? AudioLocale{ get; set; } diff --git a/CRD/Utils/Structs/Structs.cs b/CRD/Utils/Structs/HelperClasses.cs similarity index 90% rename from CRD/Utils/Structs/Structs.cs rename to CRD/Utils/Structs/HelperClasses.cs index cb13a13..121089b 100644 --- a/CRD/Utils/Structs/Structs.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json; namespace CRD.Utils.Structs; -public struct AuthData{ +public class AuthData{ public string Username{ get; set; } public string Password{ get; set; } } @@ -18,12 +18,12 @@ public class DrmAuthData{ public string? Token{ get; set; } } -public struct Meta{ +public class Meta{ [JsonProperty("versions_considered")] public bool? VersionsConsidered{ get; set; } } -public struct LanguageItem{ +public class LanguageItem{ [JsonProperty("cr_locale")] public string CrLocale{ get; set; } @@ -33,12 +33,12 @@ public struct LanguageItem{ public string Language{ get; set; } } -public struct EpisodeAndLanguage{ +public class EpisodeAndLanguage{ public List Items{ get; set; } public List Langs{ get; set; } } -public struct CrunchyMultiDownload(List dubLang, bool? all = null, bool? but = null, List? e = null, string? s = null){ +public class CrunchyMultiDownload(List dubLang, bool? all = null, bool? but = null, List? e = null, string? s = null){ public List DubLang{ get; set; } = dubLang; //lang code public bool? AllEpisodes{ get; set; } = all; // download all episodes public bool? But{ get; set; } = but; //download all except selected episodes @@ -46,12 +46,12 @@ public struct CrunchyMultiDownload(List dubLang, bool? all = null, bool? public string? S{ get; set; } = s; //season id } -public struct CrunchySeriesList{ +public class CrunchySeriesList{ public List List{ get; set; } public Dictionary Data{ get; set; } } -public struct Episode{ +public class Episode{ public string E{ get; set; } public List Lang{ get; set; } public string Name{ get; set; } @@ -63,9 +63,11 @@ public struct Episode{ public string Img{ get; set; } public string Description{ get; set; } public string Time{ get; set; } + + public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown; } -public struct DownloadResponse{ +public class DownloadResponse{ public List Data{ get; set; } public string? FileName{ get; set; } diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs index 97b1fdd..9a9d7b5 100644 --- a/CRD/Utils/Structs/History/HistoryEpisode.cs +++ b/CRD/Utils/Structs/History/HistoryEpisode.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Threading.Tasks; using CRD.Downloader; using CRD.Downloader.Crunchyroll; +using CRD.Utils.Files; using Newtonsoft.Json; namespace CRD.Utils.Structs.History; @@ -32,12 +33,18 @@ public class HistoryEpisode : INotifyPropertyChanged{ [JsonProperty("episode_special_episode")] public bool SpecialEpisode{ get; set; } + + [JsonProperty("episode_type")] + public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown; [JsonProperty("sonarr_episode_id")] public string? SonarrEpisodeId{ get; set; } [JsonProperty("sonarr_has_file")] public bool SonarrHasFile{ get; set; } + + [JsonProperty("sonarr_is_monitored")] + public bool SonarrIsMonitored{ get; set; } [JsonProperty("sonarr_episode_number")] public string? SonarrEpisodeNumber{ get; set; } @@ -77,8 +84,22 @@ public class HistoryEpisode : INotifyPropertyChanged{ } public async Task DownloadEpisode(bool onlySubs = false){ - await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId, - string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, - CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs); + switch (EpisodeType){ + case EpisodeType.MusicVideo: + await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty); + break; + case EpisodeType.Concert: + await QueueManager.Instance.CrAddConcertToQueue(EpisodeId ?? string.Empty); + break; + case EpisodeType.Episode: + case EpisodeType.Unknown: + default: + await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId ?? string.Empty, + string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, + CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs); + break; + } + + } } \ No newline at end of file diff --git a/CRD/Utils/Structs/History/HistorySeason.cs b/CRD/Utils/Structs/History/HistorySeason.cs index 9a7ab0e..6a7461f 100644 --- a/CRD/Utils/Structs/History/HistorySeason.cs +++ b/CRD/Utils/Structs/History/HistorySeason.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using CRD.Utils.Files; using Newtonsoft.Json; namespace CRD.Utils.Structs.History; diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs index 817061c..d073ea6 100644 --- a/CRD/Utils/Structs/History/HistorySeries.cs +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -8,11 +8,18 @@ using System.Threading.Tasks; using Avalonia.Media.Imaging; using CRD.Downloader.Crunchyroll; using CRD.Utils.CustomList; +using CRD.Utils.Files; using Newtonsoft.Json; namespace CRD.Utils.Structs.History; public class HistorySeries : INotifyPropertyChanged{ + [JsonProperty("series_streaming_service")] + public StreamingService SeriesStreamingService{ get; set; } = StreamingService.Unknown; + + [JsonProperty("series_type")] + public SeriesType SeriesType{ get; set; } = SeriesType.Unknown; + [JsonProperty("series_title")] public string? SeriesTitle{ get; set; } @@ -96,7 +103,7 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonIgnore] private bool Loading = false; - + [JsonIgnore] public StringItem? _selectedVideoQualityItem; @@ -111,7 +118,6 @@ public class HistorySeries : INotifyPropertyChanged{ if (!Loading){ CfgManager.UpdateHistoryFile(); } - } } @@ -230,16 +236,48 @@ 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; + var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled && + !string.IsNullOrEmpty(SonarrSeriesId); - if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr && !string.IsNullOrEmpty(SonarrSeriesId)){ + var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored; + + if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){ 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 (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ + continue; + } + + if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){ + count++; + } + } + } + + continue; + } + var episodesList = season.EpisodesList; for (int j = episodesList.Count - 1; j >= 0; j--){ var episode = episodesList[j]; + if (sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ + continue; + } + + if (episode.SpecialEpisode){ + if (historyAddSpecials && !episode.SonarrHasFile){ + count++; + } + + continue; + } + if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){ count++; } @@ -253,6 +291,10 @@ public class HistorySeries : INotifyPropertyChanged{ if (historyAddSpecials){ var episodes = season.EpisodesList; for (int j = episodes.Count - 1; j >= 0; j--){ + if (sonarrEnabled && sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ + continue; + } + if (!episodes[j].WasDownloaded){ count++; } @@ -266,6 +308,10 @@ public class HistorySeries : INotifyPropertyChanged{ for (int j = episodesList.Count - 1; j >= 0; j--){ var episode = episodesList[j]; + if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ + continue; + } + if (episode.SpecialEpisode){ if (historyAddSpecials && !episode.WasDownloaded){ count++; @@ -303,47 +349,103 @@ public class HistorySeries : INotifyPropertyChanged{ public async Task AddNewMissingToDownloads(){ bool foundWatched = false; var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; + var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled && + !string.IsNullOrEmpty(SonarrSeriesId); - for (int i = Seasons.Count - 1; i >= 0; i--){ - var season = Seasons[i]; + var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored; - if (season.SpecialSeason == true){ - if (historyAddSpecials){ - var episodes = season.EpisodesList; - for (int j = episodes.Count - 1; j >= 0; j--){ - if (!episodes[j].WasDownloaded){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); + if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){ + 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 (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ + continue; + } + + if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){ + await Seasons[i].EpisodesList[j].DownloadEpisode(); + } } } - } - - continue; - } - - var episodesList = season.EpisodesList; - for (int j = episodesList.Count - 1; j >= 0; j--){ - var episode = episodesList[j]; - - if (episode.SpecialEpisode){ - if (historyAddSpecials && !episode.WasDownloaded){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } continue; } - if (!episode.WasDownloaded && !foundWatched){ - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } else{ - foundWatched = true; - if (!historyAddSpecials){ - break; + var episodesList = season.EpisodesList; + for (int j = episodesList.Count - 1; j >= 0; j--){ + var episode = episodesList[j]; + + if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ + continue; + } + + if (episode.SpecialEpisode){ + if (historyAddSpecials && !episode.SonarrHasFile){ + await Seasons[i].EpisodesList[j].DownloadEpisode(); + } + + continue; + } + + if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){ + await Seasons[i].EpisodesList[j].DownloadEpisode(); } } } + } else{ + for (int i = Seasons.Count - 1; i >= 0; i--){ + var season = Seasons[i]; - if (foundWatched && !historyAddSpecials){ - break; + if (season.SpecialSeason == true){ + if (historyAddSpecials){ + var episodes = season.EpisodesList; + for (int j = episodes.Count - 1; j >= 0; j--){ + if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){ + continue; + } + + if (!episodes[j].WasDownloaded){ + await Seasons[i].EpisodesList[j].DownloadEpisode(); + } + } + } + + continue; + } + + var episodesList = season.EpisodesList; + for (int j = episodesList.Count - 1; j >= 0; j--){ + var episode = episodesList[j]; + + if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){ + continue; + } + + if (episode.SpecialEpisode){ + if (historyAddSpecials && !episode.WasDownloaded){ + await Seasons[i].EpisodesList[j].DownloadEpisode(); + } + + continue; + } + + if (!episode.WasDownloaded && !foundWatched){ + await Seasons[i].EpisodesList[j].DownloadEpisode(); + } else{ + foundWatched = true; + if (!historyAddSpecials){ + break; + } + } + } + + if (foundWatched && !historyAddSpecials){ + break; + } } } } @@ -352,13 +454,32 @@ public class HistorySeries : INotifyPropertyChanged{ Console.WriteLine($"Fetching Data for: {SeriesTitle}"); FetchingData = true; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); - try{ - await CrunchyrollManager.Instance.History.CRUpdateSeries(SeriesId, seasonId); - } catch (Exception e){ - Console.Error.WriteLine("Failed to update History series"); - Console.Error.WriteLine(e); + + 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){ + Console.Error.WriteLine("Failed to update History artist"); + Console.Error.WriteLine(e); + } + + break; + case SeriesType.Series: + case SeriesType.Unknown: + default: + try{ + await CrunchyrollManager.Instance.History.CrUpdateSeries(SeriesId, seasonId); + } catch (Exception e){ + Console.Error.WriteLine("Failed to update History series"); + Console.Error.WriteLine(e); + } + + break; } + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesTitle))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesDescription))); UpdateNewEpisodes(); @@ -384,6 +505,15 @@ public class HistorySeries : INotifyPropertyChanged{ } public void OpenCrPage(){ - Helpers.OpenUrl($"https://www.crunchyroll.com/series/{SeriesId}"); + switch (SeriesType){ + case SeriesType.Artist: + Helpers.OpenUrl($"https://www.crunchyroll.com/artist/{SeriesId}"); + break; + case SeriesType.Series: + case SeriesType.Unknown: + default: + Helpers.OpenUrl($"https://www.crunchyroll.com/series/{SeriesId}"); + break; + } } } \ No newline at end of file diff --git a/CRD/Utils/Structs/History/IHistorySource.cs b/CRD/Utils/Structs/History/IHistorySource.cs new file mode 100644 index 0000000..7e9fc00 --- /dev/null +++ b/CRD/Utils/Structs/History/IHistorySource.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace CRD.Utils.Structs.History; + +public interface IHistorySource{ + string GetSeriesId(); + string GetSeriesTitle(); + string GetSeasonTitle(); + string GetSeasonNum(); + string GetSeasonId(); + + string GetEpisodeId(); + string GetEpisodeNumber(); + string GetEpisodeTitle(); + string GetEpisodeDescription(); + + bool IsSpecialSeason(); + bool IsSpecialEpisode(); + + List GetAnimeIds(); + + List GetEpisodeAvailableDubLang(); + List GetEpisodeAvailableSoftSubs(); + + DateTime GetAvailableDate(); + + SeriesType GetSeriesType(); + EpisodeType GetEpisodeType(); +} \ No newline at end of file diff --git a/CRD/Utils/Structs/History/SeriesDataCache.cs b/CRD/Utils/Structs/History/SeriesDataCache.cs new file mode 100644 index 0000000..53be77d --- /dev/null +++ b/CRD/Utils/Structs/History/SeriesDataCache.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace CRD.Utils.Structs.History; + +public class SeriesDataCache{ + + public string SeriesId{ get; set; } = ""; + + public string SeriesTitle{ get; set; } = ""; + + public string SeriesDescription{ get; set; } = ""; + public string ThumbnailImageUrl{ get; set; } = ""; + + public List HistorySeriesAvailableDubLang{ get; set; } =[]; + + public List HistorySeriesAvailableSoftSubs{ get; set; } =[]; +} \ No newline at end of file diff --git a/CRD/Utils/Structs/Languages.cs b/CRD/Utils/Structs/Languages.cs index f93d680..347efbc 100644 --- a/CRD/Utils/Structs/Languages.cs +++ b/CRD/Utils/Structs/Languages.cs @@ -54,7 +54,7 @@ public class Languages{ else if (yExists) return 1; // y comes before any missing value else - return string.Compare(x, y); // Sort alphabetically or by another logic for missing values + return string.CompareOrdinal(x, y); // Sort alphabetically or by another logic for missing values }); return langList; @@ -116,8 +116,8 @@ public class Languages{ } public static LanguageItem FindLang(string crLocale){ - LanguageItem lang = languages.FirstOrDefault(l => l.CrLocale == crLocale); - if (lang.CrLocale != null){ + LanguageItem? lang = languages.FirstOrDefault(l => l.CrLocale == crLocale); + if (lang?.CrLocale != null){ return lang; } else{ return new LanguageItem{ @@ -159,7 +159,7 @@ public class Languages{ var property = typeof(T).GetProperty(sortKey, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (property == null) throw new ArgumentException($"Property '{sortKey}' not found on type '{typeof(T).Name}'."); - var value = property.GetValue(item) as string; + var value = property.GetValue(item) as string ?? string.Empty; int index = idx.ContainsKey(value) ? idx[value] : 50; return index; }).ToList(); diff --git a/CRD/Utils/UI/UiListHasElementsConverter.cs b/CRD/Utils/UI/UiListHasElementsConverter.cs new file mode 100644 index 0000000..10189fb --- /dev/null +++ b/CRD/Utils/UI/UiListHasElementsConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace CRD.Utils.UI; + +public class UiListHasElementsConverter : IValueConverter{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture){ + if (value is IEnumerable enumerable){ + // Check if the collection has any elements + foreach (var _ in enumerable){ + return true; // At least one element exists + } + + return false; // No elements + } + + // Return false if the input is not a collection or is null + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){ + throw new NotSupportedException("ListToBooleanConverter does not support ConvertBack."); + } +} \ No newline at end of file diff --git a/CRD/Utils/Updater/Updater.cs b/CRD/Utils/Updater/Updater.cs index 6e4c861..6209dec 100644 --- a/CRD/Utils/Updater/Updater.cs +++ b/CRD/Utils/Updater/Updater.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading.Tasks; +using CRD.Utils.Files; namespace CRD.Utils.Updater; diff --git a/CRD/ViewModels/AccountPageViewModel.cs b/CRD/ViewModels/AccountPageViewModel.cs index f00c849..2d6f9a8 100644 --- a/CRD/ViewModels/AccountPageViewModel.cs +++ b/CRD/ViewModels/AccountPageViewModel.cs @@ -29,19 +29,19 @@ public partial class AccountPageViewModel : ViewModelBase{ private static DispatcherTimer? _timer; private DateTime _targetTime; - private bool IsCancelled = false; - private bool UnknownEndDate = false; - private bool EndedButMaybeActive = false; + private bool IsCancelled; + private bool UnknownEndDate; + private bool EndedButMaybeActive; public AccountPageViewModel(){ UpdatetProfile(); } - private void Timer_Tick(object sender, EventArgs e){ + private void Timer_Tick(object? sender, EventArgs e){ var remaining = _targetTime - DateTime.Now; if (remaining <= TimeSpan.Zero){ RemainingTime = "No active Subscription"; - _timer.Stop(); + _timer?.Stop(); if (UnknownEndDate){ RemainingTime = "Unknown Subscription end date"; } diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index 255f580..ff67028 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -32,28 +32,28 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ private string _buttonTextSelectSeason = "Select Season"; [ObservableProperty] - private bool _addAllEpisodes = false; + private bool _addAllEpisodes; [ObservableProperty] - private bool _buttonEnabled = false; + private bool _buttonEnabled; [ObservableProperty] - private bool _allButtonEnabled = false; + private bool _allButtonEnabled; [ObservableProperty] - private bool _showLoading = false; + private bool _showLoading; [ObservableProperty] - private bool _searchEnabled = false; + private bool _searchEnabled; [ObservableProperty] private bool _searchVisible = true; [ObservableProperty] - private bool _slectSeasonVisible = false; + private bool _slectSeasonVisible; [ObservableProperty] - private bool _searchPopupVisible = false; + private bool _searchPopupVisible; public ObservableCollection Items{ get; set; } = new(); public ObservableCollection SearchItems{ get; set; } = new(); @@ -69,13 +69,13 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ private Dictionary> episodesBySeason = new(); - private List selectedEpisodes = new(); + private List selectedEpisodes = new(); private CrunchySeriesList? currentSeriesList; private CrunchyMusicVideoList? currentMusicVideoList; - private bool CurrentSeasonFullySelected = false; + private bool CurrentSeasonFullySelected; public AddDownloadPageViewModel(){ SelectedItems.CollectionChanged += OnSelectedItemsChanged; @@ -98,9 +98,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (episode.ImageBitmap == null){ if (episode.Images.PosterTall != null){ var posterTall = episode.Images.PosterTall.First(); - var imageUrl = posterTall.Find(ele => ele.Height == 180).Source - ?? (posterTall.Count >= 2 ? posterTall[1].Source : posterTall.FirstOrDefault().Source); - episode.LoadImage(imageUrl); + var imageUrl = posterTall.Find(ele => ele.Height == 180)?.Source + ?? (posterTall.Count >= 2 ? posterTall[1].Source : posterTall.FirstOrDefault()?.Source); + episode.LoadImage(imageUrl ?? string.Empty); } } @@ -171,14 +171,16 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ #region OnButtonPress [RelayCommand] - public async void OnButtonPress(){ + public async Task OnButtonPress(){ if (HasSelectedItemsOrEpisodes()){ Console.WriteLine("Added to Queue"); if (currentMusicVideoList != null){ AddSelectedMusicVideosToQueue(); - } else{ - AddSelectedEpisodesToQueue(); + } + + if (currentSeriesList != null){ + await AddSelectedEpisodesToQueue(); } ResetState(); @@ -194,10 +196,12 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } private void AddSelectedMusicVideosToQueue(){ - if (SelectedItems.Count > 0){ + AddItemsToSelectedEpisodes(); + + if (selectedEpisodes.Count > 0){ var musicClass = CrunchyrollManager.Instance.CrMusic; - foreach (var selectedItem in SelectedItems){ - var music = currentMusicVideoList.Value.Data?.FirstOrDefault(ele => ele.Id == selectedItem.Id); + foreach (var selectedItem in selectedEpisodes){ + var music = currentMusicVideoList?.Data?.FirstOrDefault(ele => ele.Id == selectedItem.Id); if (music != null){ var meta = musicClass.EpisodeMeta(music); @@ -207,24 +211,24 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } - private async void AddSelectedEpisodesToQueue(){ + private async Task AddSelectedEpisodesToQueue(){ AddItemsToSelectedEpisodes(); if (currentSeriesList != null){ await QueueManager.Instance.CrAddSeriesToQueue( - currentSeriesList.Value, + currentSeriesList, new CrunchyMultiDownload( CrunchyrollManager.Instance.CrunOptions.DubLang, AddAllEpisodes, false, - selectedEpisodes)); + selectedEpisodes.Select(selectedEpisode => selectedEpisode.AbsolutNum).ToList())); } } private void AddItemsToSelectedEpisodes(){ foreach (var selectedItem in SelectedItems){ - if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){ - selectedEpisodes.Add(selectedItem.AbsolutNum); + if (!selectedEpisodes.Contains(selectedItem)){ + selectedEpisodes.Add(selectedItem); } } } @@ -256,7 +260,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ var matchResult = ExtractLocaleAndIdFromUrl(); - if (matchResult is (string locale, string id)){ + if (matchResult is ({ } locale, { } id)){ switch (GetUrlType()){ case CrunchyUrlType.Artist: await HandleArtistUrlAsync(locale, id); @@ -299,8 +303,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ private async Task HandleArtistUrlAsync(string locale, string id){ SetLoadingState(true); - var list = await CrunchyrollManager.Instance.CrMusic.ParseArtistMusicVideosByIdAsync( - id, DetermineLocale(locale), true); + var list = await CrunchyrollManager.Instance.CrMusic.ParseArtistVideosByIdAsync( + id, DetermineLocale(locale), true, true); SetLoadingState(false); @@ -335,6 +339,16 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ id, DetermineLocale(locale), new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true); + if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){ + var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(id, DetermineLocale(locale), true); + + if (musicList != null){ + currentMusicVideoList = musicList; + PopulateItemsFromMusicVideoList(); + } + + } + SetLoadingState(false); if (list != null){ @@ -348,16 +362,37 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ private void PopulateItemsFromMusicVideoList(){ if (currentMusicVideoList?.Data != null){ - foreach (var episode in currentMusicVideoList.Value.Data){ - var imageUrl = episode.Images?.Thumbnail?.FirstOrDefault().Source ?? ""; + foreach (var episode in currentMusicVideoList.Data){ + string seasonKey; + switch (episode.EpisodeType){ + case EpisodeType.MusicVideo: + seasonKey = "Music Videos "; + break; + case EpisodeType.Concert: + seasonKey = "Concerts "; + break; + case EpisodeType.Episode: + case EpisodeType.Unknown: + default: + seasonKey = "Unknown "; + break; + } + + var imageUrl = episode.Images?.Thumbnail.FirstOrDefault()?.Source ?? ""; var time = $"{(episode.DurationMs / 1000) / 60}:{(episode.DurationMs / 1000) % 60:D2}"; - var newItem = new ItemModel(episode.Id ?? "", imageUrl, episode.Description ?? "", time, episode.Title ?? "", "", - episode.SequenceNumber.ToString(), episode.SequenceNumber.ToString(), new List()); + var newItem = new ItemModel(episode.Id, imageUrl, episode.Description ?? "", time, episode.Title ?? "", seasonKey, + episode.SequenceNumber.ToString(), episode.Id, new List(), episode.EpisodeType); - newItem.LoadImage(imageUrl); - Items.Add(newItem); + if (!episodesBySeason.ContainsKey(seasonKey)){ + episodesBySeason[seasonKey] = new List{ newItem }; + SeasonList.Add(new ComboBoxItem{ Content = seasonKey }); + } else{ + episodesBySeason[seasonKey].Add(newItem); + } } + + CurrentSelectedSeason = SeasonList.First(); } } @@ -367,7 +402,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ var itemModel = new ItemModel( episode.Id, episode.Img, episode.Description, episode.Time, episode.Name, seasonKey, episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, - episode.E, episode.Lang); + episode.E, episode.Lang, episode.EpisodeType); if (!episodesBySeason.ContainsKey(seasonKey)){ episodesBySeason[seasonKey] = new List{ itemModel }; @@ -407,7 +442,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ public void OnSelectSeasonPressed(){ if (CurrentSeasonFullySelected){ foreach (var item in Items){ - selectedEpisodes.Remove(item.AbsolutNum); + selectedEpisodes.Remove(item); SelectedItems.Remove(item); } @@ -426,10 +461,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } partial void OnCurrentSelectedSeasonChanging(ComboBoxItem? oldValue, ComboBoxItem newValue){ - if (SelectedItems == null) return; foreach (var selectedItem in SelectedItems){ - if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){ - selectedEpisodes.Add(selectedItem.AbsolutNum); + if (!selectedEpisodes.Contains(selectedItem)){ + selectedEpisodes.Add(selectedItem); } } @@ -443,7 +477,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){ - if (Items == null) return; CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item)); @@ -486,7 +519,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (list != null){ currentSeriesList = list; - SearchPopulateEpisodesBySeason(); + await SearchPopulateEpisodesBySeason(value.Id); UpdateUiForEpisodeSelection(); } else{ ButtonEnabled = true; @@ -514,7 +547,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true); } - private void SearchPopulateEpisodesBySeason(){ + private async Task SearchPopulateEpisodesBySeason(string seriesId){ if (currentSeriesList?.List == null){ return; } @@ -528,8 +561,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); - - foreach (var episode in currentSeriesList.Value.List){ + foreach (var episode in currentSeriesList.List){ var seasonKey = "S" + episode.Season; var episodeModel = new ItemModel( episode.Id, @@ -540,7 +572,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ seasonKey, episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum, episode.E, - episode.Lang); + episode.Lang, episode.EpisodeType); if (!episodesBySeason.ContainsKey(seasonKey)){ episodesBySeason[seasonKey] = new List{ episodeModel }; @@ -549,6 +581,19 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ episodesBySeason[seasonKey].Add(episodeModel); } } + + if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){ + var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) + ? CrunchyrollManager.Instance.DefaultLocale + : CrunchyrollManager.Instance.CrunOptions.HistoryLang; + var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(seriesId, DetermineLocale(locale), true); + + if (musicList != null){ + currentMusicVideoList = musicList; + PopulateItemsFromMusicVideoList(); + } + + } CurrentSelectedSeason = SeasonList.First(); } @@ -575,12 +620,12 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (episode.ImageBitmap == null){ episode.LoadImage(episode.ImageUrl); Items.Add(episode); - if (selectedEpisodes.Contains(episode.AbsolutNum)){ + if (selectedEpisodes.Contains(episode)){ SelectedItems.Add(episode); } } else{ Items.Add(episode); - if (selectedEpisodes.Contains(episode.AbsolutNum)){ + if (selectedEpisodes.Contains(episode)){ SelectedItems.Add(episode); } } @@ -604,21 +649,16 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ // Clear collections and other managed resources Items.Clear(); - Items = null; SearchItems.Clear(); - SearchItems = null; SelectedItems.Clear(); - SelectedItems = null; SeasonList.Clear(); - SeasonList = null; episodesBySeason.Clear(); - episodesBySeason = null; selectedEpisodes.Clear(); - selectedEpisodes = null; } } -public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List availableAudios) : INotifyPropertyChanged{ +public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List availableAudios, EpisodeType epType) + : INotifyPropertyChanged{ public string Id{ get; set; } = id; public string ImageUrl{ get; set; } = imageUrl; public Bitmap? ImageBitmap{ get; set; } @@ -633,6 +673,9 @@ public class ItemModel(string id, string imageUrl, string description, string ti public string TitleFull{ get; set; } = season + episode + " - " + title; public List AvailableAudios{ get; set; } = availableAudios; + public EpisodeType EpisodeType{ get; set; } = epType; + + public bool HasDubs{ get; set; } = availableAudios.Count != 0; public event PropertyChangedEventHandler? PropertyChanged; diff --git a/CRD/ViewModels/CalendarPageViewModel.cs b/CRD/ViewModels/CalendarPageViewModel.cs index 86128a9..bc151c0 100644 --- a/CRD/ViewModels/CalendarPageViewModel.cs +++ b/CRD/ViewModels/CalendarPageViewModel.cs @@ -2,21 +2,15 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Structs; -using CRD.Utils.Structs.History; using DynamicData; -using DynamicData.Kernel; -using Newtonsoft.Json; namespace CRD.ViewModels; @@ -70,9 +64,10 @@ public partial class CalendarPageViewModel : ViewModelBase{ private CalendarWeek? currentWeek; - private bool loading = true; + private bool loading; public CalendarPageViewModel(){ + loading = true; CalendarDays = new ObservableCollection(); foreach (var languageItem in Languages.languages){ @@ -138,7 +133,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ currentWeek = week; CalendarDays.Clear(); - CalendarDays.AddRange(week.CalendarDays); + if (week.CalendarDays != null) CalendarDays.AddRange(week.CalendarDays); RaisePropertyChanged(nameof(CalendarDays)); ShowLoading = false; if (CustomCalendar){ @@ -146,9 +141,9 @@ public partial class CalendarPageViewModel : ViewModelBase{ foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){ if (calendarDayCalendarEpisode.ImageBitmap == null){ if (calendarDayCalendarEpisode.AnilistEpisode){ - calendarDayCalendarEpisode.LoadImage(100,150); + _ = calendarDayCalendarEpisode.LoadImage(100,150); } else{ - calendarDayCalendarEpisode.LoadImage(); + _ = calendarDayCalendarEpisode.LoadImage(); } } @@ -165,9 +160,9 @@ public partial class CalendarPageViewModel : ViewModelBase{ if (calendarDayCalendarEpisode.ImageBitmap == null){ if (calendarDayCalendarEpisode.AnilistEpisode){ - calendarDayCalendarEpisode.LoadImage(100,150); + _ = calendarDayCalendarEpisode.LoadImage(100,150); } else{ - calendarDayCalendarEpisode.LoadImage(); + _ = calendarDayCalendarEpisode.LoadImage(); } } } @@ -263,7 +258,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ if (value?.Content != null){ CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage = value.Content.ToString(); Refresh(); - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } } @@ -276,7 +271,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true); - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } partial void OnHideDubsChanged(bool value){ @@ -285,7 +280,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ } CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs = value; - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } partial void OnFilterByAirDateChanged(bool value){ @@ -294,7 +289,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ } CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value; - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } partial void OnShowUpcomingEpisodesChanged(bool value){ @@ -303,7 +298,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ } CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes = value; - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } partial void OnCurrentCalendarDubFilterChanged(ComboBoxItem? value){ @@ -313,7 +308,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ if (!string.IsNullOrEmpty(value?.Content + "")){ CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter = value?.Content + ""; - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } } diff --git a/CRD/ViewModels/DownloadsPageViewModel.cs b/CRD/ViewModels/DownloadsPageViewModel.cs index 95ce20f..2523f90 100644 --- a/CRD/ViewModels/DownloadsPageViewModel.cs +++ b/CRD/ViewModels/DownloadsPageViewModel.cs @@ -10,7 +10,9 @@ using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll; namespace CRD.ViewModels; @@ -36,12 +38,12 @@ public partial class DownloadsPageViewModel : ViewModelBase{ QueueManager.Instance.UpdateDownloadListItems(); } - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } partial void OnRemoveFinishedChanged(bool value){ CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload = value; - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } } @@ -67,9 +69,10 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ public DownloadItemModel(CrunchyEpMeta epMetaF){ epMeta = epMetaF; - ImageUrl = epMeta.Image; + ImageUrl = epMeta.Image ?? string.Empty; Title = epMeta.SeriesTitle + (!string.IsNullOrEmpty(epMeta.Season) ? " - S" + epMeta.Season + "E" + (epMeta.EpisodeNumber != string.Empty ? epMeta.EpisodeNumber : epMeta.AbsolutEpisodeNumberE) : "") + " - " + epMeta.EpisodeTitle; + isDownloading = epMeta.DownloadProgress.IsDownloading || Done; Done = epMeta.DownloadProgress.Done; @@ -81,11 +84,19 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") : epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting"; - if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString() + (!string.IsNullOrEmpty(epMeta.Resolution) ? "- " + epMeta.Resolution : ""); + InfoText = JoinWithSeparator( + GetDubString(), + GetSubtitleString(), + epMeta.Resolution + ); Error = epMeta.DownloadProgress.Error; } + string JoinWithSeparator(params string[] parts){ + return string.Join(" - ", parts.Where(part => !string.IsNullOrEmpty(part))); + } + private string GetDubString(){ if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){ return ""; @@ -138,10 +149,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") : epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting"; - if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString() + (!string.IsNullOrEmpty(epMeta.Resolution) ? "- " + epMeta.Resolution : ""); + InfoText = JoinWithSeparator( + GetDubString(), + GetSubtitleString(), + epMeta.Resolution + ); Error = epMeta.DownloadProgress.Error; + if (PropertyChanged != null){ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Percent))); @@ -186,15 +202,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ epMeta.DownloadProgress.IsDownloading = true; Paused = !epMeta.Paused && !isDownloading || epMeta.Paused; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused))); - + CrDownloadOptions newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions); if (epMeta.OnlySubs){ newOptions.Novids = true; newOptions.Noaudio = true; } - - await CrunchyrollManager.Instance.DownloadEpisode(epMeta,newOptions ); + + await CrunchyrollManager.Instance.DownloadEpisode(epMeta, newOptions); } } @@ -209,7 +225,8 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ if (File.Exists(downloadItemDownloadedFile)){ File.Delete(downloadItemDownloadedFile); } - } catch (Exception e){ + } catch (Exception){ + // ignored } } } diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs index ccd751f..3e3c080 100644 --- a/CRD/ViewModels/HistoryPageViewModel.cs +++ b/CRD/ViewModels/HistoryPageViewModel.cs @@ -13,6 +13,7 @@ using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Sonarr; using CRD.Utils.Structs; using CRD.Utils.Structs.History; @@ -29,7 +30,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ private ProgramManager _programManager; [ObservableProperty] - private HistorySeries _selectedSeries; + private HistorySeries? _selectedSeries; [ObservableProperty] private static bool _editMode; @@ -72,10 +73,16 @@ public partial class HistoryPageViewModel : ViewModelBase{ [ObservableProperty] - private bool _isPosterViewSelected = false; + private bool _isPosterViewSelected; [ObservableProperty] - private bool _isTableViewSelected = false; + private bool _isTableViewSelected; + + [ObservableProperty] + private bool _showSeries = true; + + [ObservableProperty] + private bool _showArtists; [ObservableProperty] private static bool _viewSelectionOpen; @@ -98,7 +105,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ private FilterType currentFilterType; [ObservableProperty] - private static bool _sortDir = false; + private static bool _sortDir; [ObservableProperty] private static bool _sonarrAvailable; @@ -127,6 +134,8 @@ public partial class HistoryPageViewModel : ViewModelBase{ currentFilterType = properties?.SelectedFilter ?? FilterType.All; ScaleValue = properties?.ScaleValue ?? 0.73; SortDir = properties?.Ascending ?? false; + ShowSeries = properties?.ShowSeries ?? true; + ShowArtists = properties?.ShowArtists ?? false; foreach (HistoryViewType viewType in Enum.GetValues(typeof(HistoryViewType))){ var combobox = new ComboBoxItem{ Content = viewType }; @@ -137,7 +146,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ } foreach (SortingType sortingType in Enum.GetValues(typeof(SortingType))){ - var combobox = new SortingListElement(){ SortingTitle = sortingType.GetEnumMemberValue(), SelectedSorting = sortingType }; + var combobox = new SortingListElement{ SortingTitle = sortingType.GetEnumMemberValue(), SelectedSorting = sortingType }; SortingList.Add(combobox); if (sortingType == currentSortingType){ SelectedSorting = combobox; @@ -149,7 +158,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ continue; } - var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType }; + var item = new FilterListElement{ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType }; FilterList.Add(item); if (filterType == currentFilterType){ SelectedFilter = item; @@ -162,7 +171,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ foreach (var historySeries in Items){ if (historySeries.ThumbnailImage == null){ - historySeries.LoadImage(); + _ = historySeries.LoadImage(); } historySeries.UpdateNewEpisodes(); @@ -179,15 +188,15 @@ public partial class HistoryPageViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType; CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.Ascending = SortDir; } else{ - CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties() + CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties { ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType, Ascending = SortDir }; } - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } - partial void OnSelectedViewChanged(ComboBoxItem value){ - if (Enum.TryParse(value.Content + "", out HistoryViewType viewType)){ + partial void OnSelectedViewChanged(ComboBoxItem? value){ + if (Enum.TryParse(value?.Content + "", out HistoryViewType viewType)){ currentViewType = viewType; IsPosterViewSelected = currentViewType == HistoryViewType.Posters; IsTableViewSelected = currentViewType == HistoryViewType.Table; @@ -214,21 +223,33 @@ public partial class HistoryPageViewModel : ViewModelBase{ return; } - if (newValue.SelectedSorting != null){ - 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"); + currentSortingType = newValue.SelectedSorting; + if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType; + CrunchyrollManager.Instance.History.SortItems(); + if (SelectedFilter != null){ + OnSelectedFilterChanged(SelectedFilter); } SortingSelectionOpen = false; UpdateSettings(); } + partial void OnShowArtistsChanged(bool value){ + if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.ShowArtists = ShowArtists; + + CfgManager.WriteCrSettings(); + + ApplyFilter(); + } + + partial void OnShowSeriesChanged(bool value){ + if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.ShowSeries = ShowSeries; + + CfgManager.WriteCrSettings(); + + ApplyFilter(); + } + partial void OnSelectedFilterChanged(FilterListElement? value){ if (value == null){ @@ -237,37 +258,52 @@ public partial class HistoryPageViewModel : ViewModelBase{ currentFilterType = value.SelectedType; if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType; + CfgManager.WriteCrSettings(); + ApplyFilter(); + } + + private void ApplyFilter(){ + List filteredItems; + switch (currentFilterType){ case FilterType.All: - FilteredItems.Clear(); - FilteredItems.AddRange(Items); + filteredItems = Items.ToList(); break; + case FilterType.MissingEpisodes: - List filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList(); - FilteredItems.Clear(); - FilteredItems.AddRange(filteredItems); + filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList(); 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 + filteredItems = Items.Where(historySeries => + !string.IsNullOrEmpty(historySeries.SonarrSeriesId) && + historySeries.Seasons.Any(season => + season.EpisodesList.Any(historyEpisode => + !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile))) .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); + filteredItems = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList(); + break; + + default: + filteredItems = new List(); break; } + + if (!ShowArtists){ + filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Artist); + } + + if (!ShowSeries){ + filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Series); + } + + FilteredItems.Clear(); + FilteredItems.AddRange(filteredItems); } @@ -298,12 +334,12 @@ public partial class HistoryPageViewModel : ViewModelBase{ NavToSeries(); if (!string.IsNullOrEmpty(value.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){ - CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries); + if (SelectedSeries != null) _ = CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries); CfgManager.UpdateHistoryFile(); } + SelectedSeries = null; - _selectedSeries = null; } [RelayCommand] @@ -348,7 +384,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ } [RelayCommand] - public async void AddMissingToQueue(){ + public async Task AddMissingToQueue(){ var tasks = FilteredItems .Select(item => item.AddNewMissingToDownloads()); @@ -408,7 +444,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ ProgressText = $"{count + 1}/{totalSeries}"; // Await the CRUpdateSeries task for each seriesId - await crInstance.History.CRUpdateSeries(seriesIds[count], ""); + await crInstance.History.CrUpdateSeries(seriesIds[count], ""); RaisePropertyChanged(nameof(ProgressText)); } @@ -500,27 +536,30 @@ public partial class HistoryPageViewModel : ViewModelBase{ } } -public class HistoryPageProperties(){ +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; } + + public bool ShowSeries{ get; set; } = true; + public bool ShowArtists{ get; set; } = true; } -public class SeasonsPageProperties(){ +public class SeasonsPageProperties{ public SortingType? SelectedSorting{ get; set; } public bool Ascending{ get; set; } } -public class SortingListElement(){ +public class SortingListElement{ public SortingType SelectedSorting{ get; set; } public string? SortingTitle{ get; set; } } -public class FilterListElement(){ +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 96ced87..3b3cbad 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -29,6 +29,9 @@ public partial class SeriesPageViewModel : ViewModelBase{ [ObservableProperty] public static bool _sonarrAvailable; + + [ObservableProperty] + public static bool _showMonitoredBookmark; [ObservableProperty] public static bool _sonarrConnected; @@ -53,7 +56,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ _selectedSeries = CrunchyrollManager.Instance.SelectedSeries; if (_selectedSeries.ThumbnailImage == null){ - _selectedSeries.LoadImage(); + _ = _selectedSeries.LoadImage(); } if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){ @@ -61,13 +64,18 @@ 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); @@ -87,17 +95,19 @@ public partial class SeriesPageViewModel : ViewModelBase{ var seasonPath = season.SeasonDownloadPath; var directoryInfo = new DirectoryInfo(seasonPath); - string parentFolderPath = directoryInfo.Parent?.FullName; + if (!string.IsNullOrEmpty(directoryInfo.Parent?.FullName)){ + string parentFolderPath = directoryInfo.Parent?.FullName ?? string.Empty; - if (Directory.Exists(parentFolderPath)){ - SeriesFolderPath = parentFolderPath; - SeriesFolderPathExists = true; + if (Directory.Exists(parentFolderPath)){ + SeriesFolderPath = parentFolderPath; + SeriesFolderPathExists = true; + } } } catch (Exception e){ Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}"); } } else{ - var customPath = string.Empty; + string customPath; if (string.IsNullOrEmpty(SelectedSeries.SeriesTitle)) return; @@ -110,10 +120,10 @@ public partial class SeriesPageViewModel : ViewModelBase{ // Check Crunchyroll download directory var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath; if (!string.IsNullOrEmpty(downloadDirPath)){ - customPath = System.IO.Path.Combine(downloadDirPath, seriesTitle); + customPath = Path.Combine(downloadDirPath, seriesTitle); } else{ // Fallback to configured VIDEOS_DIR path - customPath = System.IO.Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle); + customPath = Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle); } // Check if custom path exists @@ -186,7 +196,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ SonarrConnected = SonarrAvailable = false; } - UpdateData(""); + _ = UpdateData(""); } } diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index 41a6819..8106ac3 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -2,7 +2,6 @@ using System; using System.Collections.ObjectModel; using Avalonia.Controls; using Avalonia.Layout; -using Avalonia.Media; using Avalonia.Media.Imaging; using CRD.Downloader.Crunchyroll.ViewModels; using CRD.Downloader.Crunchyroll.Views; @@ -15,14 +14,14 @@ using Image = Avalonia.Controls.Image; namespace CRD.ViewModels; -public partial class SettingsPageViewModel : ViewModelBase{ +public class SettingsPageViewModel : ViewModelBase{ public ObservableCollection Tabs{ get; } = new(); private TabViewItem CreateTab(string header, string iconPath, UserControl content, object viewModel){ content.DataContext = viewModel; - Bitmap bitmap = null; + Bitmap? bitmap = null; try{ // Load the image using AssetLoader.Open bitmap = new Bitmap(Avalonia.Platform.AssetLoader.Open(new Uri(iconPath))); diff --git a/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs b/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs index 97db9c9..9c7c806 100644 --- a/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs +++ b/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs @@ -1,30 +1,24 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; -using System.Net; using System.Net.Http; -using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; using System.Text; -using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Utils.Structs.History; using CRD.Views; using Newtonsoft.Json; using ReactiveUI; -using JsonSerializer = Newtonsoft.Json.JsonSerializer; namespace CRD.ViewModels; @@ -147,7 +141,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{ #endregion [ObservableProperty] - private AnilistSeries _selectedSeries; + private AnilistSeries? _selectedSeries; [ObservableProperty] private int _selectedIndex; @@ -164,7 +158,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{ private SortingType currentSortingType; [ObservableProperty] - private static bool _sortDir = false; + private static bool _sortDir; public ObservableCollection SortingList{ get; } =[]; @@ -236,12 +230,12 @@ public partial class UpcomingPageViewModel : ViewModelBase{ } [RelayCommand] - public async void AddToHistory(AnilistSeries series){ + public async Task AddToHistory(AnilistSeries series){ if (!string.IsNullOrEmpty(series.CrunchyrollID)){ if (CrunchyrollManager.Instance.CrunOptions.History){ series.IsInHistory = true; RaisePropertyChanged(nameof(series.IsInHistory)); - var sucess = await CrunchyrollManager.Instance.History.CRUpdateSeries(series.CrunchyrollID, ""); + var sucess = await CrunchyrollManager.Instance.History.CrUpdateSeries(series.CrunchyrollID, ""); series.IsInHistory = sucess; RaisePropertyChanged(nameof(series.IsInHistory)); @@ -435,7 +429,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties = new SeasonsPageProperties(){ SelectedSorting = currentSortingType, Ascending = SortDir }; } - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } partial void OnSelectedSortingChanged(SortingListElement? oldValue, SortingListElement? newValue){ @@ -452,11 +446,9 @@ public partial class UpcomingPageViewModel : ViewModelBase{ return; } - if (newValue.SelectedSorting != null){ - currentSortingType = newValue.SelectedSorting; - if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null) CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType; - SortItems(); - } + currentSortingType = newValue.SelectedSorting; + if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null) CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType; + SortItems(); SortingSelectionOpen = false; UpdateSettings(); diff --git a/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs b/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs index e8c0852..2d126b9 100644 --- a/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs +++ b/CRD/ViewModels/Utils/ContentDialogEncodingPresetViewModel.cs @@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Utils; using CRD.Utils.Ffmpeg_Encoding; +using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Views; using DynamicData; diff --git a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs index 67f1048..5381d90 100644 --- a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs +++ b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs @@ -17,8 +17,9 @@ using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; +using CRD.Utils.Files; using CRD.Utils.Sonarr; -using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.History; using FluentAvalonia.Styling; @@ -35,8 +36,14 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _history; + [ObservableProperty] + private bool _historyIncludeCrArtists; + [ObservableProperty] private bool _historyAddSpecials; + + [ObservableProperty] + private bool _historySkipUnmonitored; [ObservableProperty] private bool _historyCountSonarr; @@ -236,7 +243,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ ProxyUsername = options.ProxyUsername ?? ""; ProxyPassword = options.ProxyPassword ?? ""; ProxyPort = options.ProxyPort; + HistoryIncludeCrArtists = options.HistoryIncludeCrArtists; HistoryAddSpecials = options.HistoryAddSpecials; + HistorySkipUnmonitored = options.HistorySkipUnmonitored; HistoryCountSonarr = options.HistoryCountSonarr; DownloadSpeed = options.DownloadSpeedLimit; DownloadToTempFolder = options.DownloadToTempFolder; @@ -265,6 +274,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder; CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials; + CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists = HistoryIncludeCrArtists; + CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored = HistorySkipUnmonitored; CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr; CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000); CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10); @@ -309,7 +320,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.LogMode = LogMode; - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } [RelayCommand] @@ -364,7 +375,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ pathSetter(selectedFolder.Path.LocalPath); var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter(); pathSetter(finalPath); - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } } @@ -411,7 +422,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ pathSetter(selectedFile.Path.LocalPath); var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter(); pathSetter(finalPath); - CfgManager.WriteSettingsToFile(); + CfgManager.WriteCrSettings(); } } @@ -490,9 +501,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ decompressedJson, CrunchyrollManager.Instance.SettingsJsonSerializerSettings ) ?? new ObservableCollection(); - + CrunchyrollManager.Instance.HistoryList = historyList; - + Parallel.ForEach(historyList, historySeries => { historySeries.Init(); @@ -500,14 +511,13 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ historySeriesSeason.Init(); } }); - } else{ CrunchyrollManager.Instance.HistoryList = new ObservableCollection(); } } else{ CrunchyrollManager.Instance.HistoryList = new ObservableCollection(); } - + _ = Task.Run(() => SonarrClient.Instance.RefreshSonarrLite()); } else{ CrunchyrollManager.Instance.HistoryList = new ObservableCollection(); diff --git a/CRD/Views/AddDownloadPageView.axaml b/CRD/Views/AddDownloadPageView.axaml index affec89..99ec3f8 100644 --- a/CRD/Views/AddDownloadPageView.axaml +++ b/CRD/Views/AddDownloadPageView.axaml @@ -212,6 +212,7 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml index 1d6b375..97f6cd3 100644 --- a/CRD/Views/SeriesPageView.axaml +++ b/CRD/Views/SeriesPageView.axaml @@ -13,6 +13,7 @@ + @@ -57,8 +58,8 @@ - - + + @@ -183,7 +184,7 @@ - + @@ -313,14 +314,15 @@ - + - + @@ -343,10 +345,30 @@ - + + + + + + + + + + + + + + + + @@ -382,7 +404,7 @@ - + - - diff --git a/CRD/Views/SeriesPageView.axaml.cs b/CRD/Views/SeriesPageView.axaml.cs index 879e450..c9c7adf 100644 --- a/CRD/Views/SeriesPageView.axaml.cs +++ b/CRD/Views/SeriesPageView.axaml.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Interactivity; namespace CRD.Views; diff --git a/CRD/Views/UpcomingSeasonsPageView.axaml.cs b/CRD/Views/UpcomingSeasonsPageView.axaml.cs index 306381a..6c26219 100644 --- a/CRD/Views/UpcomingSeasonsPageView.axaml.cs +++ b/CRD/Views/UpcomingSeasonsPageView.axaml.cs @@ -1,8 +1,4 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using CRD.Utils.Sonarr; +using Avalonia.Controls; using CRD.Utils.Structs; using CRD.ViewModels; diff --git a/CRD/Views/Utils/GeneralSettingsView.axaml b/CRD/Views/Utils/GeneralSettingsView.axaml index 6e751d9..bcb140b 100644 --- a/CRD/Views/Utils/GeneralSettingsView.axaml +++ b/CRD/Views/Utils/GeneralSettingsView.axaml @@ -33,17 +33,29 @@ + + + + + + - + + + + + + +