diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs index 07dde67..704c6ce 100644 --- a/CRD/Downloader/CalendarManager.cs +++ b/CRD/Downloader/CalendarManager.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using CRD.Downloader.Crunchyroll; +using CRD.Downloader.Crunchyroll.Utils; using CRD.Utils; using CRD.Utils.Structs; using CRD.Utils.Structs.History; @@ -67,6 +68,10 @@ public class CalendarManager{ return forDate; } + if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){ + await LoadAnilistUpcoming(); + } + var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us") ? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false) : HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false); @@ -75,19 +80,26 @@ public class CalendarManager{ request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); request.Headers.AcceptEncoding.ParseAdd("gzip, deflate, br"); - var response = await HttpClientReq.Instance.SendHttpRequest(request); + (bool IsOk, string ResponseContent, string error) response; + if (!HttpClientReq.Instance.useFlareSolverr){ + response = await HttpClientReq.Instance.SendHttpRequest(request); + } else{ + response = await HttpClientReq.Instance.SendFlareSolverrHttpRequest(request); + } + if (!response.IsOk){ - if (response.ResponseContent.Contains("Just a moment...") || - response.ResponseContent.Contains("Access denied") || - response.ResponseContent.Contains("Attention Required! | Cloudflare") || - response.ResponseContent.Trim().Equals("error code: 1020") || + if (response.ResponseContent.Contains("Just a moment...") || + response.ResponseContent.Contains("Access denied") || + response.ResponseContent.Contains("Attention Required! | Cloudflare") || + response.ResponseContent.Trim().Equals("error code: 1020") || response.ResponseContent.IndexOf("DDOS-GUARD", StringComparison.OrdinalIgnoreCase) > -1){ MessageBus.Current.SendMessage(new ToastMessage("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5)); Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar."); } else{ Console.Error.WriteLine($"Calendar request failed"); } + return new CalendarWeek(); } @@ -164,6 +176,24 @@ public class CalendarManager{ Console.Error.WriteLine("No days found in the HTML document."); } + if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){ + foreach (var calendarDay in week.CalendarDays){ + if (calendarDay.DateTime.Date >= DateTime.Now.Date){ + if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){ + var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")]; + + foreach (var calendarEpisode in list + .Where(e => calendarDay.DateTime.Date.Day == e.DateTime.Date.Day) + .Where(e => calendarDay.CalendarEpisodes.All(ele => + ele.CrSeriesID != e.CrSeriesID && + !CrSimulcastCalendarFilter.IsMatch(ele.SeasonName, e.SeasonName, similarityThreshold: 0.5)))){ + calendarDay.CalendarEpisodes.Add(calendarEpisode); + } + } + } + } + } + calendar[weeksMondayDate] = week; @@ -172,14 +202,14 @@ public class CalendarManager{ public async Task BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){ - if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){ - await LoadAnilistUpcoming(); - } - if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){ return forDate; } + if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){ + await LoadAnilistUpcoming(); + } + CalendarWeek week = new CalendarWeek(); week.CalendarDays = new List(); @@ -201,7 +231,7 @@ public class CalendarManager{ var firstDayOfWeek = week.CalendarDays.First().DateTime; week.FirstDayOfWeek = firstDayOfWeek; - var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes("", 200, firstDayOfWeek, true); + var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true); if (newEpisodesBase is{ Data.Count: > 0 }){ var newEpisodes = newEpisodesBase.Data; @@ -222,36 +252,22 @@ public class CalendarManager{ DateTime targetDate; - if (CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate){ - targetDate = episodeAirDate; - if (targetDate >= oneYearFromNow){ - DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc - ? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime() - : crBrowseEpisode.EpisodeMetadata.FreeAvailableDate; + targetDate = premiumAvailableStart; - if (freeAvailableStart <= oneYearFromNow){ - targetDate = freeAvailableStart; - } else{ - targetDate = premiumAvailableStart; - } - } - } else{ - targetDate = premiumAvailableStart; + if (targetDate >= oneYearFromNow){ + DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc + ? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime() + : crBrowseEpisode.EpisodeMetadata.FreeAvailableDate; - if (targetDate >= oneYearFromNow){ - DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc - ? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime() - : crBrowseEpisode.EpisodeMetadata.FreeAvailableDate; - - if (freeAvailableStart <= oneYearFromNow){ - targetDate = freeAvailableStart; - } else{ - targetDate = episodeAirDate; - } + if (freeAvailableStart <= oneYearFromNow){ + targetDate = freeAvailableStart; + } else{ + targetDate = episodeAirDate; } } + var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter; if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && @@ -280,7 +296,7 @@ public class CalendarManager{ : Regex.IsMatch(crBrowseEpisode.EpisodeMetadata.SeasonTitle, @"^Season\s+\d+$", RegexOptions.IgnoreCase) ? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}" : crBrowseEpisode.EpisodeMetadata.SeasonTitle; - + calEpisode.DateTime = targetDate; calEpisode.HasPassed = DateTime.Now > targetDate; calEpisode.EpisodeName = crBrowseEpisode.Title; @@ -340,7 +356,8 @@ public class CalendarManager{ var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")]; foreach (var calendarEpisode in list.Where(calendarEpisodeAnilist => calendarDay.DateTime.Date.Day == calendarEpisodeAnilist.DateTime.Date.Day) - .Where(calendarEpisodeAnilist => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisodeAnilist.CrSeriesID && ele.SeasonName != calendarEpisodeAnilist.SeasonName))){ + .Where(calendarEpisodeAnilist => + calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisodeAnilist.CrSeriesID && ele.SeasonName != calendarEpisodeAnilist.SeasonName))){ calendarDay.CalendarEpisodes.Add(calendarEpisode); } } @@ -427,7 +444,7 @@ public class CalendarManager{ aniListResponse ??= currentResponse; if (aniListResponse != currentResponse){ - aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ??[]); + aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ?? []); } hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false; @@ -436,12 +453,12 @@ public class CalendarManager{ } while (hasNextPage && currentPage < 20); - var list = aniListResponse.Data?.Page?.AiringSchedules ??[]; + var list = aniListResponse.Data?.Page?.AiringSchedules ?? []; list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external => string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList(); - List calendarEpisodes =[]; + List calendarEpisodes = []; foreach (var anilistEle in list){ var calEp = new CalendarEpisode(); @@ -541,7 +558,7 @@ public class CalendarManager{ oldestRelease.Second, calEp.DateTime.Kind ); - + if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){ adjustedDate = oldestRelease.AddDays(7); } diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index de2ee39..1035e71 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -43,11 +43,11 @@ public class CrEpisode(){ } if (epsidoe is{ Total: 1, Data: not null } && - (epsidoe.Data.First().Versions ??[]) + (epsidoe.Data.First().Versions ?? []) .GroupBy(v => v.AudioLocale) .Any(g => g.Count() > 1)){ Console.Error.WriteLine("Episode has Duplicate Audio Locales"); - var list = (epsidoe.Data.First().Versions ??[]).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList(); + var list = (epsidoe.Data.First().Versions ?? []).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList(); //guid for episode id foreach (var episodeVersionse in list){ foreach (var version in episodeVersionse){ @@ -173,7 +173,7 @@ public class CrEpisode(){ } var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key; - var images = (item.Images?.Thumbnail ??[new List{ new(){ Source = "/notFound.jpg" } }]); + var images = (item.Images?.Thumbnail ?? [new List{ new(){ Source = "/notFound.jpg" } }]); Regex dubPattern = new Regex(@"\(\w+ Dub\)"); @@ -237,60 +237,45 @@ public class CrEpisode(){ public async Task GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){ await crunInstance.CrAuthEndpoint1.RefreshToken(true); - CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase(); - complete.Data =[]; - var i = 0; - do{ - NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); + if (string.IsNullOrEmpty(crLocale)){ + crLocale = "en-US"; + } - if (!string.IsNullOrEmpty(crLocale)){ - query["locale"] = crLocale; - if (forcedLang){ - query["force_locale"] = crLocale; - } + NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); + + if (!string.IsNullOrEmpty(crLocale)){ + query["locale"] = crLocale; + if (forcedLang){ + query["force_locale"] = crLocale; } + } - query["start"] = i + ""; - query["n"] = "50"; - query["sort_by"] = "newly_added"; - query["type"] = "episode"; + query["n"] = requestAmount + ""; + query["sort_by"] = "newly_added"; + query["type"] = "episode"; - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); - var response = await HttpClientReq.Instance.SendHttpRequest(request); + var response = await HttpClientReq.Instance.SendHttpRequest(request); - if (!response.IsOk){ - Console.Error.WriteLine("Series Request Failed"); - return null; - } + if (!response.IsOk){ + Console.Error.WriteLine("Series Request Failed"); + return null; + } - CrBrowseEpisodeBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + CrBrowseEpisodeBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); - if (series != null){ - complete.Total = series.Total; - if (series.Data != null){ - complete.Data.AddRange(series.Data); - if (firstWeekDay != null){ - if (firstWeekDay.Value.Date <= series.Data.Last().LastPublic && i + 50 == requestAmount){ - requestAmount += 50; - } - } - } - } else{ - break; - } + series?.Data?.Sort((a, b) => + b.EpisodeMetadata.PremiumAvailableDate.CompareTo(a.EpisodeMetadata.PremiumAvailableDate)); - i += 50; - } while (i < requestAmount && requestAmount < 500); - - - return complete; + return series; } public async Task MarkAsWatched(string episodeId){ - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true, crunInstance.CrAuthEndpoint1.Token?.access_token, null); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true, + crunInstance.CrAuthEndpoint1.Token?.access_token, null); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index 76669f1..4fed16f 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -338,7 +338,7 @@ public class CrSeries{ } if (episodeList.Total < 1){ - Console.Error.WriteLine("Season is empty!"); + Console.Error.WriteLine($"Season is empty! Uri: {episodeRequest.RequestUri}"); } return episodeList; diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index ef018e0..a2e5a9b 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -838,7 +838,8 @@ public class CrunchyrollManager{ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){ if (!File.Exists(CfgManager.PathFFMPEG)){ Console.Error.WriteLine("Missing ffmpeg"); - MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}"); + MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}", "FFmpeg", + "https://github.com/GyanD/codexffmpeg/releases/latest"); return new DownloadResponse{ Data = new List(), Error = true, @@ -849,7 +850,8 @@ public class CrunchyrollManager{ if (!File.Exists(CfgManager.PathMKVMERGE)){ Console.Error.WriteLine("Missing Mkvmerge"); - MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}"); + MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}", "Mkvmerge", + "https://mkvtoolnix.download/downloads.html#windows"); return new DownloadResponse{ Data = new List(), Error = true, @@ -883,7 +885,8 @@ public class CrunchyrollManager{ if (!_widevine.canDecrypt){ Console.Error.WriteLine("CDM files missing"); - MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", true); + MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", "GitHub Wiki", + "https://github.com/Crunchy-DL/Crunchy-Downloader/wiki"); return new DownloadResponse{ Data = new List(), Error = true, @@ -1361,7 +1364,7 @@ public class CrunchyrollManager{ // List streamServers = new List(streamPlaylists.Data.Keys); if (streamPlaylistsReqResponseList.Count > 0){ HashSet streamServers = []; - Dictionary playListData = new Dictionary(); + ServerData playListData = new ServerData(); foreach (var curStreams in streamPlaylistsReqResponseList){ var match = Regex.Match(curStreams.Key ?? string.Empty, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))"); @@ -1382,7 +1385,7 @@ public class CrunchyrollManager{ } } - options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer; + // options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer; if (streamServers.Count == 0){ return new DownloadResponse{ @@ -1393,17 +1396,20 @@ public class CrunchyrollManager{ }; } - if (options.StreamServer == 0){ - options.StreamServer = 1; - } + playListData.video ??= []; + playListData.audio ??= []; + + // if (options.StreamServer == 0){ + // options.StreamServer = 1; + // } // string selectedServer = streamServers[options.StreamServer - 1]; // ServerData selectedList = streamPlaylists.Data[selectedServer]; - string selectedServer = streamServers.ToList()[options.StreamServer - 1]; - ServerData selectedList = playListData[selectedServer]; + // string selectedServer = streamServers.ToList()[options.StreamServer - 1]; + // ServerData selectedList = playListData[selectedServer]; - var videos = selectedList.video.Select(item => new VideoItem{ + var videos = playListData.video.Select(item => new VideoItem{ segments = item.segments, pssh = item.pssh, quality = item.quality, @@ -1411,7 +1417,7 @@ public class CrunchyrollManager{ resolutionText = $"{item.quality.width}x{item.quality.height} ({Math.Round(item.bandwidth / 1024.0)}KiB/s)" }).ToList(); - var audios = selectedList.audio.Select(item => new AudioItem{ + var audios = playListData.audio.Select(item => new AudioItem{ @default = item.@default, segments = item.segments, pssh = item.pssh, @@ -1532,7 +1538,7 @@ public class CrunchyrollManager{ sb.AppendLine($"Selected quality:"); sb.AppendLine($"\tVideo: {chosenVideoSegments.resolutionText}"); sb.AppendLine($"\tAudio: {chosenAudioSegments.resolutionText} / {chosenAudioSegments.audioSamplingRate}"); - sb.AppendLine($"\tServer: {selectedServer}"); + sb.AppendLine($"\tServer: {string.Join(", ", playListData.servers)}"); string qualityConsoleLog = sb.ToString(); Console.WriteLine(qualityConsoleLog); @@ -1591,8 +1597,10 @@ public class CrunchyrollManager{ await CrAuthEndpoint1.RefreshToken(true); await CrAuthEndpoint2.RefreshToken(true); - Dictionary authDataDict = new Dictionary - { { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + Dictionary authDataDict = new Dictionary{ + { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) }, + { "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } + }; chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); @@ -1626,8 +1634,10 @@ public class CrunchyrollManager{ await CrAuthEndpoint2.RefreshToken(true); if (chosenVideoSegments.encryptionKeys.Count == 0){ - Dictionary authDataDict = new Dictionary - { { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + Dictionary authDataDict = new Dictionary{ + { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) }, + { "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } + }; chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); @@ -1685,8 +1695,10 @@ public class CrunchyrollManager{ await CrAuthEndpoint1.RefreshToken(true); await CrAuthEndpoint2.RefreshToken(true); - Dictionary authDataDict = new Dictionary - { { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + Dictionary authDataDict = new Dictionary{ + { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) }, + { "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } + }; var encryptionKeys = chosenVideoSegments.encryptionKeys; diff --git a/CRD/Downloader/Crunchyroll/Utils/CrSimulcastCalendarFilter.cs b/CRD/Downloader/Crunchyroll/Utils/CrSimulcastCalendarFilter.cs new file mode 100644 index 0000000..d41a521 --- /dev/null +++ b/CRD/Downloader/Crunchyroll/Utils/CrSimulcastCalendarFilter.cs @@ -0,0 +1,147 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace CRD.Downloader.Crunchyroll.Utils; + +public class CrSimulcastCalendarFilter{ + private static readonly Regex SeasonLangSuffix = + new Regex(@"\bSeason\s+\d+\s*\((?.*)\)\s*$", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly string[] NonLanguageTags ={ + "uncut", "simulcast", "sub", "subbed" + }; + + private static readonly string[] LanguageHints ={ + "deutsch", "german", + "español", "espanol", "spanish", "américa latina", "america latina", "latin america", + "português", "portugues", "portuguese", "brasil", "brazil", + "français", "francais", "french", + "italiano", "italian", + "english", + "рус", "russian", + "한국", "korean", + "中文", "普通话", "mandarin", + "ไทย", "thai", + "türk", "turk", "turkish", + "polski", "polish", + "nederlands", "dutch" + }; + + public static bool IsDubOrAltLanguageSeason(string? seasonName){ + if (string.IsNullOrWhiteSpace(seasonName)) + return false; + + // Explicit "Dub" anywhere + if (seasonName.Contains("dub", StringComparison.OrdinalIgnoreCase)) + return true; + + // "Season N ( ... )" suffix + var m = SeasonLangSuffix.Match(seasonName); + if (!m.Success) + return false; + + var tag = m.Groups["tag"].Value.Trim(); + if (tag.Length == 0) + return false; + + foreach (var nl in NonLanguageTags) + if (tag.Contains(nl, StringComparison.OrdinalIgnoreCase)) + return false; + + // Non-ASCII in the tag (e.g., 中文, Español, Português) + if (tag.Any(c => c > 127)) + return true; + + // Otherwise look for known language hints + foreach (var hint in LanguageHints) + if (tag.Contains(hint, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + #region Name Match to upcoming + + private static readonly Regex TrailingParenGroups = + new Regex(@"\s*(\([^)]*\))\s*$", RegexOptions.Compiled); + + public static bool IsMatch(string? a, string? b, double similarityThreshold = 0.85){ + if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) + return false; + + var na = Normalize(a); + var nb = Normalize(b); + + if (string.Equals(na, nb, StringComparison.OrdinalIgnoreCase)) + return true; + + if (na.Length >= 8 && nb.Length >= 8 && + (na.Contains(nb, StringComparison.OrdinalIgnoreCase) || + nb.Contains(na, StringComparison.OrdinalIgnoreCase))) + return true; + + return Similarity(na, nb) >= similarityThreshold; + } + + private static string Normalize(string s){ + s = s.Trim(); + + while (TrailingParenGroups.IsMatch(s)) + s = TrailingParenGroups.Replace(s, "").TrimEnd(); + + s = s.Normalize(NormalizationForm.FormD); + var sb = new StringBuilder(s.Length); + foreach (var ch in s){ + var uc = CharUnicodeInfo.GetUnicodeCategory(ch); + if (uc != UnicodeCategory.NonSpacingMark) + sb.Append(ch); + } + + s = sb.ToString().Normalize(NormalizationForm.FormC); + + var cleaned = new StringBuilder(s.Length); + foreach (var ch in s) + cleaned.Append(char.IsLetterOrDigit(ch) ? ch : ' '); + + return Regex.Replace(cleaned.ToString(), @"\s+", " ").Trim().ToLowerInvariant(); + } + + private static double Similarity(string a, string b){ + if (a.Length == 0 && b.Length == 0) return 1.0; + int dist = LevenshteinDistance(a, b); + int maxLen = Math.Max(a.Length, b.Length); + return 1.0 - (double)dist / maxLen; + } + + private static int LevenshteinDistance(string a, string b){ + if (a.Length == 0) return b.Length; + if (b.Length == 0) return a.Length; + + var prev = new int[b.Length + 1]; + var curr = new int[b.Length + 1]; + + for (int j = 0; j <= b.Length; j++) + prev[j] = j; + + for (int i = 1; i <= a.Length; i++){ + curr[0] = i; + for (int j = 1; j <= b.Length; j++){ + int cost = a[i - 1] == b[j - 1] ? 0 : 1; + curr[j] = Math.Min( + Math.Min(curr[j - 1] + 1, prev[j] + 1), + prev[j - 1] + cost + ); + } + + (prev, curr) = (curr, prev); + } + + return prev[b.Length]; + } + + #endregion +} \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index 378a4e9..accf84a 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -153,7 +153,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private ComboBoxItem _selectedStreamEndpoint; - + [ObservableProperty] private bool _firstEndpointVideo; @@ -166,9 +166,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private string _endpointAuthorization = ""; - [ObservableProperty] - private string _endpointClientId = ""; - [ObservableProperty] private string _endpointUserAgent = ""; @@ -367,13 +364,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0]; EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty; - EndpointClientId = options.StreamEndpointSecondSettings?.Client_ID ?? string.Empty; EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty; EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty; EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty; EndpointVideo = options.StreamEndpointSecondSettings?.Video ?? true; EndpointAudio = options.StreamEndpointSecondSettings?.Audio ?? true; - + FirstEndpointVideo = options.StreamEndpoint?.Video ?? true; FirstEndpointAudio = options.StreamEndpoint?.Audio ?? true; @@ -383,6 +379,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions()); + if (FFmpegHWAccel.Count == 0){ + FFmpegHWAccel.Add(new StringItemWithDisplayName{ + DisplayName = "No hardware acceleration (error)", + value = "error" + }); + } + StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null; SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0]; @@ -554,7 +557,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ var endpointSettings = new CrAuthSettings(); endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + ""; endpointSettings.Authorization = EndpointAuthorization; - endpointSettings.Client_ID = EndpointClientId; endpointSettings.UserAgent = EndpointUserAgent; endpointSettings.Device_name = EndpointDeviceName; endpointSettings.Device_type = EndpointDeviceType; @@ -730,7 +732,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0]; EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization; - EndpointClientId = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Client_ID; EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent; EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name; EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type; @@ -786,11 +787,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ return MapHWAccelOptions(accels); } } catch (Exception e){ - Console.WriteLine("Failed to get Available HW Accel Options" + e); + Console.Error.WriteLine("Failed to get Available HW Accel Options" + e); } + var result = new List(); + result.Add(new StringItemWithDisplayName{ + DisplayName = "No hardware acceleration / error", + value = "error" + }); - return []; + return result; } private List MapHWAccelOptions(List accels){ diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml index d8f410f..55e90b7 100644 --- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -304,12 +304,6 @@ Text="{Binding EndpointAuthorization}" /> - - - - - target, + ServerData target, Dictionary source, bool mergeAudio, bool mergeVideo){ + if (target == null) throw new ArgumentNullException(nameof(target)); + if (source == null) throw new ArgumentNullException(nameof(source)); + + var serverSet = new HashSet(target.servers); + + void AddServer(string s){ + if (!string.IsNullOrWhiteSpace(s) && serverSet.Add(s)) + target.servers.Add(s); + } + foreach (var kvp in source){ var key = kvp.Key; var src = kvp.Value; + + if (!src.servers.Contains(key)) + src.servers.Add(key); + + AddServer(key); + foreach (var s in src.servers) + AddServer(s); + + if (mergeAudio && src.audio != null){ + target.audio ??= []; + target.audio.AddRange(src.audio); + } - if (target.TryGetValue(key, out var existing)){ - if (mergeAudio){ - existing.audio ??= []; - if (src.audio != null) - existing.audio.AddRange(src.audio); - } - - if (mergeVideo){ - existing.video ??= []; - if (src.video != null) - existing.video.AddRange(src.video); - } - } else{ - target[key] = new ServerData{ - audio = (mergeAudio && src.audio != null) - ? new List(src.audio) - : new List(), - video = (mergeVideo && src.video != null) - ? new List(src.video) - : new List() - }; + if (mergeVideo && src.video != null){ + target.video ??= []; + target.video.AddRange(src.video); } } } diff --git a/CRD/Utils/Http/FlareSolverrClient.cs b/CRD/Utils/Http/FlareSolverrClient.cs new file mode 100644 index 0000000..dd75d28 --- /dev/null +++ b/CRD/Utils/Http/FlareSolverrClient.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using CRD.Downloader.Crunchyroll; +using Newtonsoft.Json; + +namespace CRD.Utils; + +public class FlareSolverrClient{ + private readonly HttpClient _httpClient; + + private FlareSolverrProperties properties; + + private string flaresolverrUrl = "http://localhost:8191"; + + public FlareSolverrClient(){ + if (CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties != null) properties = CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties; + + if (properties != null){ + flaresolverrUrl = $"http{(properties.UseSsl ? "s" : "")}://{(!string.IsNullOrEmpty(properties.Host) ? properties.Host : "localhost")}:{properties.Port}"; + } + + _httpClient = new HttpClient{ BaseAddress = new Uri(flaresolverrUrl) }; + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"); + } + + + public async Task<(bool IsOk, string ResponseContent, List cookies)> SendViaFlareSolverrAsync(HttpRequestMessage request,List cookiesToSend){ + + var flaresolverrCookies = new List(); + + foreach (var cookie in cookiesToSend) + { + flaresolverrCookies.Add(new + { + name = cookie.Name, + value = cookie.Value, + domain = cookie.Domain, + path = cookie.Path, + secure = cookie.Secure, + httpOnly = cookie.HttpOnly + }); + } + + var requestData = new{ + cmd = request.Method.Method.ToLower() switch{ + "get" => "request.get", + "post" => "request.post", + "patch" => "request.patch", + _ => "request.get" // Default to GET if the method is unknown + }, + url = request.RequestUri.ToString(), + maxTimeout = 60000, + postData = request.Method == HttpMethod.Post || request.Method == HttpMethod.Patch + ? await request.Content.ReadAsStringAsync() + : null, + cookies = flaresolverrCookies + }; + + // Serialize the request data to JSON + var json = JsonConvert.SerializeObject(requestData); + var flareSolverrContent = new StringContent(json, Encoding.UTF8, "application/json"); + + // Send the request to FlareSolverr + var flareSolverrRequest = new HttpRequestMessage(HttpMethod.Post, $"{flaresolverrUrl}/v1"){ + Content = flareSolverrContent + }; + + HttpResponseMessage flareSolverrResponse; + try{ + flareSolverrResponse = await _httpClient.SendAsync(flareSolverrRequest); + } catch (Exception ex){ + Console.Error.WriteLine($"Error sending request to FlareSolverr: {ex.Message}"); + return (IsOk: false, ResponseContent: $"Error sending request to FlareSolverr: {ex.Message}", []); + } + + string flareSolverrResponseContent = await flareSolverrResponse.Content.ReadAsStringAsync(); + + // Parse the FlareSolverr response + var flareSolverrResult = JsonConvert.DeserializeObject(flareSolverrResponseContent); + + if (flareSolverrResult != null && flareSolverrResult.Status == "ok"){ + return (IsOk: true, ResponseContent: flareSolverrResult.Solution.Response, flareSolverrResult.Solution.cookies); + } else{ + Console.Error.WriteLine($"Flare Solverr Failed \n Response: {flareSolverrResponseContent}"); + return (IsOk: false, ResponseContent: flareSolverrResponseContent, []); + } + } + + private Dictionary GetHeadersDictionary(HttpRequestMessage request){ + var headers = new Dictionary(); + foreach (var header in request.Headers){ + headers[header.Key] = string.Join(", ", header.Value); + } + + if (request.Content != null){ + foreach (var header in request.Content.Headers){ + headers[header.Key] = string.Join(", ", header.Value); + } + } + + return headers; + } + + private Dictionary GetCookiesDictionary(HttpRequestMessage request, Dictionary cookieStore){ + var cookiesDictionary = new Dictionary(); + if (cookieStore.TryGetValue(request.RequestUri.Host, out CookieCollection cookies)){ + foreach (Cookie cookie in cookies){ + cookiesDictionary[cookie.Name] = cookie.Value; + } + } + + return cookiesDictionary; + } +} + +public class FlareSolverrResponse{ + public string Status{ get; set; } + public FlareSolverrSolution Solution{ get; set; } +} + +public class FlareSolverrSolution{ + public string Url{ get; set; } + public string Status{ get; set; } + public List cookies{ get; set; } + public string Response{ get; set; } +} + +public class FlareSolverrProperties(){ + public bool UseFlareSolverr{ get; set; } + public string? Host{ get; set; } + public int Port{ get; set; } + public bool UseSsl{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index e7b3479..c68e0e2 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -36,6 +36,9 @@ public class HttpClientReq{ private HttpClient client; + public readonly bool useFlareSolverr; + private FlareSolverrClient flareSolverrClient; + public HttpClientReq(){ IWebProxy systemProxy = WebRequest.DefaultWebProxy; @@ -79,6 +82,11 @@ public class HttpClientReq{ client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br"); // client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.5"); client.DefaultRequestHeaders.Connection.ParseAdd("keep-alive"); + + if (CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties != null && CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties.UseFlareSolverr){ + useFlareSolverr = true; + flareSolverrClient = new FlareSolverrClient(); + } } private HttpMessageHandler CreateHttpClientHandler(){ @@ -150,6 +158,24 @@ public class HttpClientReq{ } } + public async Task<(bool IsOk, string ResponseContent, string error)> SendFlareSolverrHttpRequest(HttpRequestMessage request, bool suppressError = false){ + string content = string.Empty; + try{ + var flareSolverrResponses = await flareSolverrClient.SendViaFlareSolverrAsync(request, []); + + + content = flareSolverrResponses.ResponseContent; + + return (IsOk: flareSolverrResponses.IsOk, ResponseContent: content, error: ""); + } catch (Exception e){ + if (!suppressError){ + Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}"); + } + + return (IsOk: false, ResponseContent: content, error: ""); + } + } + private void CaptureResponseCookies(HttpResponseMessage response, Uri requestUri, Dictionary? cookieStore){ if (cookieStore == null){ return; diff --git a/CRD/Utils/Parser/MPDTransformer.cs b/CRD/Utils/Parser/MPDTransformer.cs index 0fb0dbe..2f72bfb 100644 --- a/CRD/Utils/Parser/MPDTransformer.cs +++ b/CRD/Utils/Parser/MPDTransformer.cs @@ -64,8 +64,9 @@ public class MPDParsed{ } public class ServerData{ - public List audio{ get; set; } =[]; - public List video{ get; set; } =[]; + public List servers{ get; set; } = []; + public List? audio{ get; set; } =[]; + public List? video{ get; set; } =[]; } public static class MPDParser{ diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index 7e5da7f..5709017 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -134,6 +134,10 @@ public class CrDownloadOptions{ [JsonProperty("proxy_password")] public string? ProxyPassword{ get; set; } + + [JsonProperty("flare_solverr_properties")] + public FlareSolverrProperties? FlareSolverrProperties{ get; set; } + #endregion @@ -301,9 +305,6 @@ public class CrDownloadOptions{ [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; } diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs index 5757cd5..8e3a55d 100644 --- a/CRD/Utils/Structs/HelperClasses.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -14,7 +14,6 @@ public class AuthData{ public class CrAuthSettings{ public string Endpoint{ get; set; } - public string Client_ID{ get; set; } public string Authorization{ get; set; } public string UserAgent{ get; set; } public string Device_type{ get; set; } @@ -66,8 +65,8 @@ public class CrunchyMultiDownload(List dubLang, bool? all = null, bool? } public class CrunchySeriesList{ - public List List{ get; set; } - public Dictionary Data{ get; set; } + public List List{ get; set; } = []; + public Dictionary Data{ get; set; } = []; } public class Episode{ diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index b2557ce..3c49126 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -16,6 +16,8 @@ using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll.Music; +using CRD.Views; +using ReactiveUI; // ReSharper disable InconsistentNaming @@ -210,6 +212,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } else if (AddAllEpisodes){ var musicClass = CrunchyrollManager.Instance.CrMusic; + if (currentMusicVideoList == null) return; foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){ QueueManager.Instance.CrAddMusicMetaToQueue(meta); } @@ -421,7 +424,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } - CurrentSelectedSeason = SeasonList.First(); + if (SeasonList.Count > 0){ + CurrentSelectedSeason = SeasonList.First(); + } } private string DetermineLocale(string locale){ @@ -525,15 +530,35 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ var list = await FetchSeriesListAsync(value.Id); - if (list != null){ + if (list is{ List.Count: > 0 }){ currentSeriesList = list; await SearchPopulateEpisodesBySeason(value.Id); UpdateUiForEpisodeSelection(); } else{ - ButtonEnabled = true; + ResetSearch(); + MessageBus.Current.SendMessage(new ToastMessage($"Failed to get Episodes for Series", ToastType.Error, 2)); } } + private void ResetSearch(){ + currentMusicVideoList = null; + UrlInput = ""; + selectedEpisodes.Clear(); + SelectedItems.Clear(); + Items.Clear(); + currentSeriesList = null; + SeasonList.Clear(); + episodesBySeason.Clear(); + AllButtonEnabled = false; + AddAllEpisodes = false; + ButtonEnabled = false; + SearchVisible = true; + SlectSeasonVisible = false; + ShowLoading = false; + SearchEnabled = false; // disable and enable for button text + SearchEnabled = true; + } + private void UpdateUiForSearchSelection(){ SearchPopupVisible = false; RaisePropertyChanged(nameof(SearchVisible)); @@ -602,7 +627,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } - CurrentSelectedSeason = SeasonList.First(); + if (SeasonList.Count > 0){ + CurrentSelectedSeason = SeasonList.First(); + } } private void UpdateUiForEpisodeSelection(){ diff --git a/CRD/ViewModels/CalendarPageViewModel.cs b/CRD/ViewModels/CalendarPageViewModel.cs index d491278..8430a7a 100644 --- a/CRD/ViewModels/CalendarPageViewModel.cs +++ b/CRD/ViewModels/CalendarPageViewModel.cs @@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; +using CRD.Downloader.Crunchyroll.Utils; using CRD.Utils.Files; using CRD.Utils.Structs; using DynamicData; @@ -18,7 +19,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ [ObservableProperty] private bool _prevButtonEnabled = true; - + [ObservableProperty] private bool _nextButtonEnabled = true; @@ -28,9 +29,6 @@ public partial class CalendarPageViewModel : ViewModelBase{ [ObservableProperty] private bool _customCalendar; - [ObservableProperty] - private bool _filterByAirDate; - [ObservableProperty] private bool _showUpcomingEpisodes; @@ -75,7 +73,6 @@ public partial class CalendarPageViewModel : ViewModelBase{ CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar; HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs; - FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate; ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes; ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null; @@ -87,7 +84,6 @@ public partial class CalendarPageViewModel : ViewModelBase{ } - private string GetThisWeeksMondayDate(){ DateTime today = DateTime.Today; @@ -104,13 +100,12 @@ public partial class CalendarPageViewModel : ViewModelBase{ return formattedDate; } - public async void LoadCalendar(string mondayDate,DateTime customCalDate, bool forceUpdate){ + public async void LoadCalendar(string mondayDate, DateTime customCalDate, bool forceUpdate){ ShowLoading = true; - + CalendarWeek week; if (CustomCalendar){ - if (customCalDate.Date == DateTime.Now.Date){ PrevButtonEnabled = false; NextButtonEnabled = true; @@ -118,7 +113,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ PrevButtonEnabled = true; NextButtonEnabled = false; } - + week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate); } else{ PrevButtonEnabled = true; @@ -140,29 +135,24 @@ 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(); } - } } } } else{ foreach (var calendarDay in CalendarDays){ - var episodesCopy = new List(calendarDay.CalendarEpisodes); - foreach (var calendarDayCalendarEpisode in episodesCopy){ - if (calendarDayCalendarEpisode.SeasonName != null && HideDubs && calendarDayCalendarEpisode.SeasonName.EndsWith("Dub)")){ - calendarDay.CalendarEpisodes.Remove(calendarDayCalendarEpisode); - continue; - } + if (HideDubs) + calendarDay.CalendarEpisodes.RemoveAll(e => CrSimulcastCalendarFilter.IsDubOrAltLanguageSeason(e.SeasonName)); - if (calendarDayCalendarEpisode.ImageBitmap == null){ - if (calendarDayCalendarEpisode.AnilistEpisode){ - _ = calendarDayCalendarEpisode.LoadImage(100,150); - } else{ - _ = calendarDayCalendarEpisode.LoadImage(); - } + foreach (var e in calendarDay.CalendarEpisodes){ + if (e.ImageBitmap == null){ + if (e.AnilistEpisode) + _ = e.LoadImage(100, 150); + else + _ = e.LoadImage(); } } } @@ -199,7 +189,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ refreshDate = currentWeek.FirstDayOfWeek.AddDays(6); } - LoadCalendar(mondayDate,refreshDate, true); + LoadCalendar(mondayDate, refreshDate, true); } [RelayCommand] @@ -215,13 +205,13 @@ public partial class CalendarPageViewModel : ViewModelBase{ } else{ mondayDate = GetThisWeeksMondayDate(); } - + var refreshDate = DateTime.Now; if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){ refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1); } - LoadCalendar(mondayDate,refreshDate, false); + LoadCalendar(mondayDate, refreshDate, false); } [RelayCommand] @@ -237,15 +227,13 @@ public partial class CalendarPageViewModel : ViewModelBase{ } else{ mondayDate = GetThisWeeksMondayDate(); } - + var refreshDate = DateTime.Now; if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){ refreshDate = currentWeek.FirstDayOfWeek.AddDays(13); } - LoadCalendar(mondayDate,refreshDate, false); - - + LoadCalendar(mondayDate, refreshDate, false); } @@ -268,7 +256,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value; - LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true); + LoadCalendar(GetThisWeeksMondayDate(), DateTime.Now, true); CfgManager.WriteCrSettings(); } @@ -282,15 +270,6 @@ public partial class CalendarPageViewModel : ViewModelBase{ CfgManager.WriteCrSettings(); } - partial void OnFilterByAirDateChanged(bool value){ - if (loading){ - return; - } - - CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value; - CfgManager.WriteCrSettings(); - } - partial void OnShowUpcomingEpisodesChanged(bool value){ if (loading){ return; @@ -310,7 +289,4 @@ public partial class CalendarPageViewModel : ViewModelBase{ CfgManager.WriteCrSettings(); } } - - - } \ No newline at end of file diff --git a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs index f41cdda..ce0b30a 100644 --- a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs +++ b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs @@ -207,6 +207,18 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [ObservableProperty] private string _proxyPassword; + + [ObservableProperty] + private string _flareSolverrHost = "localhost"; + + [ObservableProperty] + private string _flareSolverrPort = "8191"; + + [ObservableProperty] + private bool _flareSolverrUseSsl = false; + + [ObservableProperty] + private bool _useFlareSolverr = false; [ObservableProperty] private string _tempDownloadDirPath; @@ -264,6 +276,15 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ SonarrPort = props.Port + ""; SonarrApiKey = props.ApiKey + ""; } + + var propsFlareSolverr = options.FlareSolverrProperties; + + if (propsFlareSolverr != null){ + FlareSolverrUseSsl = propsFlareSolverr.UseSsl; + UseFlareSolverr = propsFlareSolverr.UseFlareSolverr; + FlareSolverrHost = propsFlareSolverr.Host + ""; + FlareSolverrPort = propsFlareSolverr.Port + ""; + } ProxyEnabled = options.ProxyEnabled; ProxySocks = options.ProxySocks; @@ -362,9 +383,22 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ } props.ApiKey = SonarrApiKey; - - + settings.SonarrProperties = props; + + var propsFlareSolverr = new FlareSolverrProperties(); + + propsFlareSolverr.UseSsl = FlareSolverrUseSsl; + propsFlareSolverr.UseFlareSolverr = UseFlareSolverr; + propsFlareSolverr.Host = FlareSolverrHost; + + if (int.TryParse(FlareSolverrPort, out var portNumberFlare)){ + propsFlareSolverr.Port = portNumberFlare; + } else{ + propsFlareSolverr.Port = 8989; + } + + settings.FlareSolverrProperties = propsFlareSolverr; settings.LogMode = LogMode; diff --git a/CRD/Views/CalendarPageView.axaml b/CRD/Views/CalendarPageView.axaml index 5c928c2..5e0ab31 100644 --- a/CRD/Views/CalendarPageView.axaml +++ b/CRD/Views/CalendarPageView.axaml @@ -96,9 +96,6 @@ SelectedItem="{Binding CurrentCalendarDubFilter}" ItemsSource="{Binding CalendarDubFilter}"> - - @@ -109,11 +106,16 @@ - + - - + + + + + + @@ -232,8 +234,7 @@