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
diff --git a/CRD/Views/MainWindow.axaml.cs b/CRD/Views/MainWindow.axaml.cs
index af01794..4c12a07 100644
--- a/CRD/Views/MainWindow.axaml.cs
+++ b/CRD/Views/MainWindow.axaml.cs
@@ -120,7 +120,10 @@ public partial class MainWindow : AppWindow{
.Subscribe(message => ShowToast(message.Message ?? string.Empty, message.Type, message.Seconds));
}
- public async void ShowError(string message, bool githubWikiButton = false){
+ //ffmpeg - https://github.com/GyanD/codexffmpeg/releases/latest
+ //mkvmerge - https://mkvtoolnix.download/downloads.html#windows
+ //git wiki - https://github.com/Crunchy-DL/Crunchy-Downloader/wiki
+ public async void ShowError(string message, string urlButtonText = "", string url = ""){
if (activeErrors.Contains(message))
return;
@@ -132,14 +135,14 @@ public partial class MainWindow : AppWindow{
CloseButtonText = "Close"
};
- if (githubWikiButton){
- dialog.PrimaryButtonText = "Github Wiki";
+ if (!string.IsNullOrEmpty(urlButtonText)){
+ dialog.PrimaryButtonText = urlButtonText;
}
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary){
- Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
+ Helpers.OpenUrl(url);
}
activeErrors.Remove(message);
diff --git a/CRD/Views/Utils/GeneralSettingsView.axaml b/CRD/Views/Utils/GeneralSettingsView.axaml
index f50f3e5..bee21e3 100644
--- a/CRD/Views/Utils/GeneralSettingsView.axaml
+++ b/CRD/Views/Utils/GeneralSettingsView.axaml
@@ -75,7 +75,7 @@
-
+
@@ -186,7 +186,7 @@
HorizontalAlignment="Stretch" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DockPanel.Dock="Right" />