- Added **FlareSolverr support** for the default simulcast calendar

- Added **upcoming episodes** to the default simulcast calendar
- Adjusted **default simulcast calendar** to filter dubs correctly
- Removed **FFmpeg and MKVMerge** from the GitHub package
- Adjusted **custom calendar episode fetching**
- Fixed **download error** when audio and video were served from different servers
- Fixed **crash** when a searched series had no episodes
This commit is contained in:
Elwador 2026-01-10 02:23:41 +01:00
parent c5660a87e7
commit c7687c80e8
19 changed files with 620 additions and 215 deletions

View file

@ -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("<title>Just a moment...</title>") ||
response.ResponseContent.Contains("<title>Access denied</title>") ||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
response.ResponseContent.Trim().Equals("error code: 1020") ||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
response.ResponseContent.Contains("<title>Access denied</title>") ||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
response.ResponseContent.Trim().Equals("error code: 1020") ||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
MessageBus.Current.SendMessage(new ToastMessage("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<CalendarWeek> 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<CalendarDay>();
@ -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<CalendarEpisode> calendarEpisodes =[];
List<CalendarEpisode> 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);
}

View file

@ -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<Image>{ new(){ Source = "/notFound.jpg" } }]);
var images = (item.Images?.Thumbnail ?? [new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
@ -237,60 +237,45 @@ public class CrEpisode(){
public async Task<CrBrowseEpisodeBase?> 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<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(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);

View file

@ -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;

View file

@ -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<DownloadedMedia>(),
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<DownloadedMedia>(),
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<DownloadedMedia>(),
Error = true,
@ -1361,7 +1364,7 @@ public class CrunchyrollManager{
// List<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
if (streamPlaylistsReqResponseList.Count > 0){
HashSet<string> streamServers = [];
Dictionary<string, ServerData> playListData = new Dictionary<string, ServerData>();
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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, string> authDataDict = new Dictionary<string, string>{
{ "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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, string> authDataDict = new Dictionary<string, string>{
{ "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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, string> authDataDict = new Dictionary<string, string>{
{ "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;

View file

@ -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*\((?<tag>.*)\)\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
}

View file

@ -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<StringItemWithDisplayName>();
result.Add(new StringItemWithDisplayName{
DisplayName = "No hardware acceleration / error",
value = "error"
});
return [];
return result;
}
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){

View file

@ -304,12 +304,6 @@
Text="{Binding EndpointAuthorization}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Client Id" />
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointClientId}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="User Agent" />
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"

View file

@ -847,36 +847,41 @@ public class Helpers{
}
}
public static void MergePlaylistData(
Dictionary<string, ServerData> target,
ServerData target,
Dictionary<string, ServerData> 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<string>(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<AudioPlaylist>(src.audio)
: new List<AudioPlaylist>(),
video = (mergeVideo && src.video != null)
? new List<VideoPlaylist>(src.video)
: new List<VideoPlaylist>()
};
if (mergeVideo && src.video != null){
target.video ??= [];
target.video.AddRange(src.video);
}
}
}

View file

@ -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<Cookie> cookies)> SendViaFlareSolverrAsync(HttpRequestMessage request,List<Cookie> cookiesToSend){
var flaresolverrCookies = new List<object>();
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<FlareSolverrResponse>(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<string, string> GetHeadersDictionary(HttpRequestMessage request){
var headers = new Dictionary<string, string>();
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<string, string> GetCookiesDictionary(HttpRequestMessage request, Dictionary<string, CookieCollection> cookieStore){
var cookiesDictionary = new Dictionary<string, string>();
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<Cookie> 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; }
}

View file

@ -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<string, CookieCollection>? cookieStore){
if (cookieStore == null){
return;

View file

@ -64,8 +64,9 @@ public class MPDParsed{
}
public class ServerData{
public List<AudioPlaylist> audio{ get; set; } =[];
public List<VideoPlaylist> video{ get; set; } =[];
public List<string> servers{ get; set; } = [];
public List<AudioPlaylist>? audio{ get; set; } =[];
public List<VideoPlaylist>? video{ get; set; } =[];
}
public static class MPDParser{

View file

@ -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; }

View file

@ -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<string> dubLang, bool? all = null, bool?
}
public class CrunchySeriesList{
public List<Episode> List{ get; set; }
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; }
public List<Episode> List{ get; set; } = [];
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; } = [];
}
public class Episode{

View file

@ -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(){

View file

@ -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<CalendarEpisode>(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();
}
}
}

View file

@ -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;

View file

@ -96,9 +96,6 @@
SelectedItem="{Binding CurrentCalendarDubFilter}"
ItemsSource="{Binding CalendarDubFilter}">
</ComboBox>
<CheckBox IsChecked="{Binding FilterByAirDate}"
Content="Filter by episode air date" Margin="5 5 0 0">
</CheckBox>
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
Content="Show Upcoming episodes" Margin="5 5 0 0">
</CheckBox>
@ -109,11 +106,16 @@
</controls:SettingsExpander>
<controls:SettingsExpander Header="Calendar ">
<controls:SettingsExpander Header="Calendar " IsVisible="{Binding !CustomCalendar}">
<controls:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding HideDubs}"
Content="Hide Dubs" Margin="5 0 0 0">
</CheckBox>
<StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding HideDubs}"
Content="Hide Dubs" Margin="5 0 0 0">
</CheckBox>
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
Content="Show Upcoming episodes" Margin="5 5 0 0">
</CheckBox>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
@ -232,8 +234,7 @@
<Button HorizontalAlignment="Center" Content="Download"
IsEnabled="{Binding HasPassed}"
IsVisible="{Binding HasPassed}"
Command="{Binding AddEpisodeToQue}"
/>
Command="{Binding AddEpisodeToQue}" />
</StackPanel>
</Border>
</DataTemplate>

View file

@ -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);

View file

@ -75,7 +75,7 @@
<CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Allow early start of next download" Description="When enabled, the next download starts as soon as the previous file has finished downloading, even if it is still being finalized (muxed/moved).">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadAllowEarlyStart}"> </CheckBox>
@ -186,7 +186,7 @@
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding DownloadAllowEarlyStart}" Content="Parallel Processing Jobs" Description="The maximum number of completed downloads that can be processed simultaneously (encoding, muxing, moving)">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="10"
@ -385,6 +385,40 @@
</controls:SettingsExpander>
<controls:SettingsExpander Header="Flare Solverr Settings"
IconSource="Wifi2"
Description="FlareSolverr settings (used only for the Calendar)"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Use Flare Solverr">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding UseFlareSolverr}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Host">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FlareSolverrHost}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Port">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FlareSolverrPort}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use SSL">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding FlareSolverrUseSsl}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="App Appearance"
IconSource="DarkTheme"
Description="Customize the look and feel of the application"
@ -617,7 +651,7 @@
DockPanel.Dock="Left" />
<ColorPicker Color="{Binding CustomAccentColor}"
DockPanel.Dock="Right" />
DockPanel.Dock="Right" />
</DockPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>