Compare commits

..

2 commits

Author SHA1 Message Date
Elwador
c7687c80e8 - 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
2026-01-10 02:23:41 +01:00
Elwador
c5660a87e7 - Added **fallback for hardsub** to use **no-hardsub video** if enabled
- Added **video/audio toggle for endpoints** to control which media type is used from each endpoint
- **Updated packages** to the latest versions
- **Updated Android phone token**
2025-12-01 11:47:30 +01:00
24 changed files with 872 additions and 323 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

@ -42,7 +42,7 @@ public class CrunchyrollManager{
public ObservableCollection<HistorySeries> HistoryList = new();
public HistorySeries SelectedSeries = new HistorySeries{
Seasons =[]
Seasons = []
};
#endregion
@ -107,8 +107,8 @@ public class CrunchyrollManager{
options.Partsize = 10;
options.DlSubs = new List<string>{ "en-US" };
options.SkipMuxing = false;
options.MkvmergeOptions =[];
options.FfmpegOptions =[];
options.MkvmergeOptions = [];
options.FfmpegOptions = [];
options.DefaultAudio = "ja-JP";
options.DefaultSub = "en-US";
options.QualityAudio = "best";
@ -128,7 +128,7 @@ public class CrunchyrollManager{
options.CalendarDubFilter = "none";
options.CustomCalendar = true;
options.DlVideoOnce = true;
options.StreamEndpoint = "web/firefox";
options.StreamEndpoint = new CrAuthSettings(){ Endpoint = "tv/android_tv", Audio = true, Video = true };
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
options.HistoryLang = DefaultLocale;
options.FixCccSubtitles = true;
@ -201,13 +201,17 @@ public class CrunchyrollManager{
DefaultAndroidAuthSettings = new CrAuthSettings(){
Endpoint = "android/phone",
Authorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=",
UserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0",
Client_ID = "pd6uw3dfyhzghs0wxae3",
Authorization = "Basic cGQ2dXczZGZ5aHpnaHMwd3hhZTM6NXJ5SjJFQXR3TFc0UklIOEozaWk1anVqbnZrRWRfTkY=",
UserAgent = "Crunchyroll/3.95.2 Android/16 okhttp/4.12.0",
Device_name = "CPH2449",
Device_type = "OnePlus CPH2449"
Device_type = "OnePlus CPH2449",
Audio = true,
Video = true,
};
CrunOptions.StreamEndpoint = "tv/android_tv";
CrunOptions.StreamEndpoint ??= new CrAuthSettings(){ Endpoint = "tv/android_tv", Audio = true, Video = true };
CrunOptions.StreamEndpoint.Endpoint = "tv/android_tv";
CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
Endpoint = "tv/android_tv",
Authorization = "Basic ZGsxYndzemRyc3lkeTR1N2xvenE6bDl0SU1BdTlzTGc4ZjA4ajlfQkQ4eWZmQmZTSms0R0o=",
@ -236,7 +240,7 @@ public class CrunchyrollManager{
// ApiUrls.authBasicMob = "Basic " + token;
// }
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") :[];
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") : [];
foreach (var file in jsonFiles){
try{
@ -276,13 +280,13 @@ public class CrunchyrollManager{
}
});
} else{
HistoryList =[];
HistoryList = [];
}
} else{
HistoryList =[];
HistoryList = [];
}
} else{
HistoryList =[];
HistoryList = [];
}
@ -303,7 +307,14 @@ public class CrunchyrollManager{
Doing = "Starting"
};
QueueManager.Instance.Queue.Refresh();
var res = await DownloadMediaList(data, options);
var res = new DownloadResponse();
try{
res = await DownloadMediaList(data, options);
} catch (Exception e){
Console.WriteLine(e);
res.Error = true;
}
if (res.Error){
QueueManager.Instance.DecrementDownloads();
@ -356,7 +367,7 @@ public class CrunchyrollManager{
var fileNameAndPath = options.DownloadToTempFolder
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true } && (!options.Noaudio || !options.Novids)){
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List<Merger>();
foreach (var keyValue in groupByDub){
@ -421,7 +432,7 @@ public class CrunchyrollManager{
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
}
if (options.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.options.Subtitles);
}
@ -481,7 +492,22 @@ public class CrunchyrollManager{
}
if (options.DownloadToTempFolder){
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, result.merger?.options.Subtitles ?? []);
var tempFolder = res.TempFolderPath ?? CfgManager.PathTEMP_DIR;
List<SubtitleInput> subtitles =
result.merger?.options.Subtitles
?? res.Data
.Where(d => d.Type == DownloadMediaType.Subtitle)
.Select(d => new SubtitleInput{
File = d.Path ?? string.Empty,
Language = d.Language,
ClosedCaption = d.Cc ?? false,
Signs = d.Signs ?? false,
RelatedVideoDownloadMedia = d.RelatedVideoDownloadMedia
})
.ToList();
await MoveFromTempFolder(result.merger, data, options, tempFolder, subtitles);
}
}
@ -663,7 +689,7 @@ public class CrunchyrollManager{
foreach (var downloadedMedia in subs){
var subt = new SubtitleFonts();
subt.Language = downloadedMedia.Language;
subt.Fonts = downloadedMedia.Fonts ??[];
subt.Fonts = downloadedMedia.Fonts ?? [];
subsList.Add(subt);
}
@ -701,7 +727,7 @@ public class CrunchyrollManager{
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
KeepAllVideos = options.KeepAllVideos,
Fonts = options.MuxFonts ? FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList) :[],
Fonts = options.MuxFonts ? FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList) : [],
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle,
Options = new MuxOptions(){
@ -718,8 +744,8 @@ public class CrunchyrollManager{
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced,
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[],
Cover = options.MuxCover ? data.Where(a => a.Type == DownloadMediaType.Cover).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[],
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() : [],
Cover = options.MuxCover ? data.Where(a => a.Type == DownloadMediaType.Cover).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() : [],
});
if (!File.Exists(CfgManager.PathFFMPEG)){
@ -731,7 +757,7 @@ public class CrunchyrollManager{
}
bool isMuxed, syncError = false;
List<string> notSyncedDubs =[];
List<string> notSyncedDubs = [];
if (options is{ SyncTiming: true, DlVideoOnce: true } && merger.options.OnlyVid.Count > 0 && merger.options.OnlyAudio.Count > 0){
@ -812,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,
@ -823,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,
@ -857,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,
@ -904,11 +933,10 @@ public class CrunchyrollManager{
options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
if (options.DownloadDescriptionAudio){
var alreadyAdr = new HashSet<string>(
data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err")
);
bool HasDescriptionRole(IEnumerable<string>? roles) =>
roles?.Any(r => string.Equals(r, "description", StringComparison.OrdinalIgnoreCase)) == true;
@ -918,7 +946,7 @@ public class CrunchyrollManager{
.Where(m => m.Versions?.Any(v => (v.AudioLocale == (m.Lang?.CrLocale ?? "err"))
&& HasDescriptionRole(v.roles)) == true)
.ToList();
var additions = toDuplicate.Select(m => new CrunchyEpMetaData{
MediaId = m.MediaId,
Lang = m.Lang,
@ -1024,13 +1052,18 @@ public class CrunchyrollManager{
#endregion
var fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData = default;
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
if (CrAuthEndpoint2.Profile.Username != "???"){
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
if (CrAuthEndpoint1.Profile.Username != "???" && options.StreamEndpoint != null && (options.StreamEndpoint.Video || options.StreamEndpoint.Audio)){
fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription, options.StreamEndpoint);
}
if (!fetchPlaybackData.IsOk){
if (CrAuthEndpoint2.Profile.Username != "???" && options.StreamEndpointSecondSettings != null && (options.StreamEndpointSecondSettings.Video || options.StreamEndpointSecondSettings.Audio)){
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription, options.StreamEndpointSecondSettings);
}
if (!fetchPlaybackData.IsOk && !fetchPlaybackData2.IsOk){
var errorJson = fetchPlaybackData.error;
if (!string.IsNullOrEmpty(errorJson)){
var error = StreamError.FromJson(errorJson);
@ -1077,7 +1110,7 @@ public class CrunchyrollManager{
}
if (fetchPlaybackData2.IsOk){
if (fetchPlaybackData.pbData.Data != null && fetchPlaybackData2.pbData?.Data != null)
if (fetchPlaybackData.pbData?.Data != null && fetchPlaybackData2.pbData?.Data != null){
foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){
var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data;
if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){
@ -1101,13 +1134,16 @@ public class CrunchyrollManager{
}
}
}
} else{
fetchPlaybackData = fetchPlaybackData2;
}
}
var pbData = fetchPlaybackData.pbData;
List<string> hsLangs = new List<string>();
var pbStreams = pbData.Data;
var pbStreams = pbData?.Data;
var streams = new List<StreamDetailsPop>();
variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true));
@ -1116,12 +1152,12 @@ public class CrunchyrollManager{
variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true));
variables.Add(new Variable("seasonTitle", data.SeasonTitle ?? string.Empty, true));
variables.Add(new Variable("season", !string.IsNullOrEmpty(data.Season) ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false));
variables.Add(new Variable("dubs", string.Join(", ", data.SelectedDubs ??[]), true));
variables.Add(new Variable("dubs", string.Join(", ", data.SelectedDubs ?? []), true));
if (pbStreams?.Keys != null){
var pb = pbStreams.Select(v => {
if (v.Key != "none" && v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
if (v.Key != "none" && v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
hsLangs.Add(v.Value.HardsubLang.CrLocale);
}
@ -1216,14 +1252,22 @@ public class CrunchyrollManager{
};
}
} else{
dlFailed = true;
if (options.HsRawFallback){
streams = streams.Where((s) => !s.IsHardsubbed).ToList();
if (streams.Count < 1){
Console.Error.WriteLine("Raw streams not available!");
dlFailed = true;
}
} else{
dlFailed = true;
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "No Hardsubs available"
};
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "No Hardsubs available"
};
}
}
}
}
@ -1263,7 +1307,7 @@ public class CrunchyrollManager{
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
Dictionary<string, string> streamPlaylistsReqResponseList =[];
Dictionary<string, StreamInfo> streamPlaylistsReqResponseList = [];
foreach (var streamUrl in curStream.Url){
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl.Url ?? string.Empty, HttpMethod.Get, true, streamUrl.CrAuth?.Token?.access_token);
@ -1280,7 +1324,11 @@ public class CrunchyrollManager{
}
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
streamPlaylistsReqResponseList[streamUrl.Url ?? ""] = streamPlaylistsReqResponse.ResponseContent;
streamPlaylistsReqResponseList[streamUrl.Url ?? ""] = new StreamInfo(){
Playlist = streamPlaylistsReqResponse.ResponseContent,
Audio = streamUrl.Audio,
Video = streamUrl.Video
};
}
}
@ -1315,8 +1363,8 @@ 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>();
HashSet<string> streamServers = [];
ServerData playListData = new ServerData();
foreach (var curStreams in streamPlaylistsReqResponseList){
var match = Regex.Match(curStreams.Key ?? string.Empty, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))");
@ -1328,15 +1376,16 @@ public class CrunchyrollManager{
}
try{
MPDParsed streamPlaylists = MPDParser.Parse(curStreams.Value, Languages.FindLang(crLocal), matchedUrl);
var entry = curStreams.Value;
MPDParsed streamPlaylists = MPDParser.Parse(entry.Playlist, Languages.FindLang(crLocal), matchedUrl);
streamServers.UnionWith(streamPlaylists.Data.Keys);
Helpers.MergePlaylistData(playListData, streamPlaylists.Data);
Helpers.MergePlaylistData(playListData, streamPlaylists.Data, entry.Audio, entry.Video);
} catch (Exception e){
Console.Error.WriteLine(e);
}
}
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{
@ -1347,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,
@ -1365,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,
@ -1486,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);
@ -1543,9 +1595,12 @@ public class CrunchyrollManager{
Console.WriteLine("Skipping video download...");
} else{
await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "authorization", "Bearer " + CrAuthEndpoint1.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);
@ -1576,10 +1631,13 @@ public class CrunchyrollManager{
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
if (chosenVideoSegments.encryptionKeys.Count == 0){
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "authorization", "Bearer " + CrAuthEndpoint1.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);
@ -1635,9 +1693,12 @@ public class CrunchyrollManager{
}
await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "authorization", "Bearer " + CrAuthEndpoint1.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;
@ -1895,7 +1956,7 @@ public class CrunchyrollManager{
var isAbsolute = Path.IsPathRooted(outFile);
// Get all directory parts of the path except the last segment (assuming it's a file)
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ??[];
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? [];
// Initialize the cumulative path based on whether the original path is absolute or not
var cumulativePath = isAbsolute ? "" : fileDir;
@ -1990,7 +2051,7 @@ public class CrunchyrollManager{
Console.WriteLine($"{fileName}.xml has been created with the description.");
}
if (options.MuxCover){
if (options is{ MuxCover: true, Noaudio: false, Novids: false }){
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
var bitmap = await Helpers.LoadImage(data.ImageBig);
if (bitmap != null){
@ -2040,8 +2101,8 @@ public class CrunchyrollManager{
videoDownloadMedia.Lang = pbData.Meta.AudioLocale;
}
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[];
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ??[];
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ?? [];
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ?? [];
var subsDataMapped = subsData.Select(s => {
var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
return new{
@ -2348,7 +2409,8 @@ public class CrunchyrollManager{
#region Fetch Playback Data
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music, bool auioRoleDesc){
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music, bool auioRoleDesc,
CrAuthSettings optionsStreamEndpointSettings){
var temppbData = new PlaybackData{
Total = 0,
Data = new Dictionary<string, StreamDetails>()
@ -2364,7 +2426,7 @@ public class CrunchyrollManager{
}
if (playbackRequestResponse.IsOk){
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
} else{
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play{(auioRoleDesc ? "?audioRole=description" : "")}";
@ -2375,7 +2437,7 @@ public class CrunchyrollManager{
}
if (playbackRequestResponse.IsOk){
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
} else{
Console.Error.WriteLine("Fallback Request Stream URLs FAILED!");
}
@ -2405,7 +2467,7 @@ public class CrunchyrollManager{
return response;
}
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint){
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint, CrAuthSettings optionsStreamEndpointSettings){
var temppbData = new PlaybackData{
Total = 0,
Data = new Dictionary<string, StreamDetails>()
@ -2424,7 +2486,7 @@ public class CrunchyrollManager{
foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url =[new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint }],
Url = [new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint, Audio = optionsStreamEndpointSettings.Audio, Video = optionsStreamEndpointSettings.Video }],
IsHardsubbed = true,
HardsubLocale = stream.Hlang,
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
@ -2433,7 +2495,7 @@ public class CrunchyrollManager{
}
derivedPlayCrunchyStreams[""] = new StreamDetails{
Url =[new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint }],
Url = [new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint, Audio = optionsStreamEndpointSettings.Audio, Video = optionsStreamEndpointSettings.Video }],
IsHardsubbed = false,
HardsubLocale = Locale.DefaulT,
HardsubLang = Languages.DEFAULT_lang

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

@ -139,6 +139,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedHSLang;
[ObservableProperty]
private bool _hsRawFallback;
[ObservableProperty]
private ComboBoxItem _selectedDescriptionLang;
@ -151,15 +154,18 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedStreamEndpoint;
[ObservableProperty]
private bool _firstEndpointVideo;
[ObservableProperty]
private bool _firstEndpointAudio;
[ObservableProperty]
private ComboBoxItem _SelectedStreamEndpointSecondary;
[ObservableProperty]
private string _endpointAuthorization = "";
[ObservableProperty]
private string _endpointClientId = "";
[ObservableProperty]
private string _endpointUserAgent = "";
@ -169,6 +175,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private string _endpointDeviceType = "";
[ObservableProperty]
private bool _endpointVideo;
[ObservableProperty]
private bool _endpointAudio;
[ObservableProperty]
private bool _isLoggingIn;
@ -188,7 +200,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
private ComboBoxItem? _selectedAudioQuality;
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedSubLang =[];
private ObservableCollection<ListBoxItem> _selectedSubLang = [];
[ObservableProperty]
private Color _listBoxColor;
@ -231,12 +243,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "ar-SA" }
];
public ObservableCollection<ListBoxItem> DubLangList{ get; } =[];
public ObservableCollection<ListBoxItem> DubLangList{ get; } = [];
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } =[];
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = [];
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } =[];
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = [];
public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
@ -277,7 +289,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "tv/android_tv" },
];
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } = [];
[ObservableProperty]
private StringItemWithDisplayName _selectedFFmpegHWAccel;
@ -345,17 +357,21 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint?.Endpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
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;
if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
EndpointNotSignedWarning = true;
@ -363,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];
@ -390,6 +413,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
HsRawFallback = options.HsRawFallback;
FixCccSubtitles = options.FixCccSubtitles;
ConvertVtt2Ass = options.ConvertVtt2Ass;
SubsDownloadDuplicate = options.SubsDownloadDuplicate;
@ -519,20 +543,25 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.HsRawFallback = HsRawFallback;
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
var endpointSettingsFirst = new CrAuthSettings();
endpointSettingsFirst.Endpoint = SelectedStreamEndpoint.Content + "";
endpointSettingsFirst.Video = FirstEndpointVideo;
endpointSettingsFirst.Audio = FirstEndpointAudio;
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = endpointSettingsFirst;
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;
endpointSettings.Video = EndpointVideo;
endpointSettings.Audio = EndpointAudio;
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
@ -657,13 +686,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
}
}
} else{
CrunchyrollManager.Instance.HistoryList =[];
CrunchyrollManager.Instance.HistoryList = [];
}
}
_ = SonarrClient.Instance.RefreshSonarrLite();
} else{
CrunchyrollManager.Instance.HistoryList =[];
CrunchyrollManager.Instance.HistoryList = [];
}
}
}
@ -703,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;
@ -759,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

@ -75,6 +75,12 @@
</ComboBox>
</controls:SettingsExpander.Footer>
<controls:SettingsExpanderItem Content="No hardsubs fallback" Description="If no hardsubs are available, automatically download the no-hardsub (raw) video.">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HsRawFallback}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
@ -249,12 +255,27 @@
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Stream Endpoint " IsEnabled="False">
<controls:SettingsExpanderItem Content="Stream Endpoint ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpoints}"
SelectedItem="{Binding SelectedStreamEndpoint}">
</ComboBox>
<StackPanel>
<ComboBox IsEnabled="False" HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpoints}"
SelectedItem="{Binding SelectedStreamEndpoint}">
</ComboBox>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding FirstEndpointVideo}" />
</StackPanel>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding FirstEndpointAudio}" />
</StackPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
@ -266,33 +287,38 @@
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
</ComboBox>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding EndpointVideo}" />
</StackPanel>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding EndpointAudio}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Authorization" />
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointAuthorization}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Client Id" />
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding EndpointClientId}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="User Agent" />
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointUserAgent}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Device Type" />
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointDeviceType}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Device Name" />
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointDeviceName}" />
</StackPanel>

View file

@ -1,6 +1,7 @@
using System;
using Avalonia;
using System.Linq;
using ReactiveUI.Avalonia;
namespace CRD;
@ -26,7 +27,8 @@ sealed class Program{
var builder = AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
.LogToTrace()
.UseReactiveUI() ;
if (isHeadless){
Console.WriteLine("Running in headless mode...");

View file

@ -73,10 +73,12 @@ public class Helpers{
}
public static int ToKbps(int bps) => (int)Math.Round(bps / 1000.0);
public static int SnapToAudioBucket(int kbps){
int[] buckets ={ 64, 96, 128, 192,256 };
int[] buckets = { 64, 96, 128, 192, 256 };
return buckets.OrderBy(b => Math.Abs(b - kbps)).First();
}
public static int WidthBucket(int width, int height){
int expected = (int)Math.Round(height * 16 / 9.0);
int tol = Math.Max(8, (int)(expected * 0.02)); // ~2% or ≥8 px
@ -555,7 +557,7 @@ public class Helpers{
return CosineSimilarity(vector1, vector2);
}
private static readonly char[] Delimiters ={ ' ', ',', '.', ';', ':', '-', '_', '\'' };
private static readonly char[] Delimiters = { ' ', ',', '.', ';', ':', '-', '_', '\'' };
public static Dictionary<string, double> ComputeWordFrequency(string text){
var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
@ -718,7 +720,7 @@ public class Helpers{
bool isValid = !folderName.Any(c => invalidChars.Contains(c));
// Check for reserved names on Windows
string[] reservedNames =["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
string[] reservedNames = ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
bool isReservedName = reservedNames.Contains(folderName.ToUpperInvariant());
if (isValid && !isReservedName && folderName.Length <= 255){
@ -845,30 +847,46 @@ public class Helpers{
}
}
public static void MergePlaylistData(
Dictionary<string, ServerData> target,
Dictionary<string, ServerData> source){
foreach (var kvp in source){
if (target.TryGetValue(kvp.Key, out var existing)){
// Merge audio
existing.audio ??=[];
if (kvp.Value.audio != null)
existing.audio.AddRange(kvp.Value.audio);
// Merge video
existing.video ??=[];
if (kvp.Value.video != null)
existing.video.AddRange(kvp.Value.video);
} else{
// Add new entry (clone lists to avoid reference issues)
target[kvp.Key] = new ServerData{
audio = kvp.Value.audio != null ? new List<AudioPlaylist>(kvp.Value.audio) : new List<AudioPlaylist>(),
video = kvp.Value.video != null ? new List<VideoPlaylist>(kvp.Value.video) : new List<VideoPlaylist>()
};
public static void MergePlaylistData(
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 (mergeVideo && src.video != null){
target.video ??= [];
target.video.AddRange(src.video);
}
}
}
private static readonly SemaphoreSlim ShutdownLock = new(1, 1);
public static async Task ShutdownComputer(){

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;
@ -270,6 +296,7 @@ public static class ApiUrls{
public static string Auth => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/auth/v1/token";
public static string Profile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/profile";
public static string Profiles => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/multiprofile";
public static string CmsToken => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/index/v2";
public static string Search => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/search";
public static string Browse => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/browse";

View file

@ -16,9 +16,6 @@ public class Merger{
public Merger(MergerOptions options){
this.options = options;
if (this.options.SkipSubMux != null && this.options.SkipSubMux == true){
this.options.Subtitles = new();
}
if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){
this.options.VideoTitle = this.options.VideoTitle.Replace("\"", "'");
@ -74,35 +71,39 @@ public class Merger{
index++;
}
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
if (!options.SkipSubMux){
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
if (sub.value.Delay != null && sub.value.Delay != 0){
double delay = sub.value.Delay / 1000.0 ?? 0;
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
if (sub.value.Delay != null && sub.value.Delay != 0){
double delay = sub.value.Delay / 1000.0 ?? 0;
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
}
args.Add($"-i \"{sub.value.File}\"");
metaData.Add($"-map {index}:s");
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
&& sub.value.ClosedCaption == false){
metaData.Add($"-disposition:s:{sub.i} default");
} else{
metaData.Add($"-disposition:s:{sub.i} 0");
}
index++;
}
args.Add($"-i \"{sub.value.File}\"");
metaData.Add($"-map {index}:s");
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
&& sub.value.ClosedCaption == false){
metaData.Add($"-disposition:s:{sub.i} default");
} else{
metaData.Add($"-disposition:s:{sub.i} 0");
}
index++;
}
args.AddRange(metaData);
// args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
args.Add("-c:v copy");
args.Add("-c:a copy");
args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass");
args.AddRange(options.Subtitles.Select((sub, subindex) =>
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
if (!options.SkipSubMux){
args.AddRange(options.Subtitles.Select((sub, subindex) =>
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
}
if (!string.IsNullOrEmpty(options.VideoTitle)){
args.Add($"-metadata title=\"{options.VideoTitle}\"");
@ -134,9 +135,9 @@ public class Merger{
}
var audio = options.OnlyAudio.First();
args.Add($"-i \"{audio.Path}\"");
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : "") );
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : ""));
args.Add($"\"{options.Output}\"");
return string.Join(" ", args);
}
@ -170,7 +171,7 @@ public class Merger{
// var sortedAudio = options.OnlyAudio
// .OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue)
// .ToList();
var rank = options.DubLangList
.Select((val, i) => new{ val, i })
.ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase);
@ -204,7 +205,7 @@ public class Merger{
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\"");
}
if (options.Subtitles.Count > 0){
if (options.Subtitles.Count > 0 && !options.SkipSubMux){
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
var sortedSubtitles = options.Subtitles
@ -274,7 +275,7 @@ public class Merger{
if (options.Description is{ Count: > 0 }){
args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\"");
}
if (options.Cover.Count > 0){
if (File.Exists(options.Cover.First().Path)){
args.Add($"--attach-file \"{options.Cover.First().Path}\"");
@ -446,14 +447,16 @@ public class Merger{
allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".new.resume"));
options.Description?.ForEach(description => Helpers.DeleteFile(description.Path));
options.Cover?.ForEach(cover => Helpers.DeleteFile(cover.Path));
// Delete chapter files if any
options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
// Delete subtitle files
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
if (!options.SkipSubMux){
// Delete subtitle files
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
}
}
}
@ -486,7 +489,7 @@ public class CrunchyMuxOptions{
public List<string> DubLangList{ get; set; } = new List<string>();
public List<string> SubLangList{ get; set; } = new List<string>();
public string Output{ get; set; }
public bool? SkipSubMux{ get; set; }
public bool SkipSubMux{ get; set; }
public bool? KeepAllVideos{ get; set; }
public bool? Novids{ get; set; }
public bool Mp4{ get; set; }
@ -524,7 +527,7 @@ public class MergerOptions{
public string VideoTitle{ get; set; }
public bool? KeepAllVideos{ get; set; }
public List<ParsedFont> Fonts{ get; set; } = new List<ParsedFont>();
public bool? SkipSubMux{ get; set; }
public bool SkipSubMux{ get; set; }
public MuxOptions Options{ get; set; }
public Defaults Defaults{ get; set; }
public bool mp3{ get; set; }

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
@ -150,6 +154,9 @@ public class CrDownloadOptions{
[JsonProperty("hard_sub_lang")]
public string Hslang{ get; set; } = "";
[JsonProperty("hard_sub_raw_fallback")]
public bool HsRawFallback{ get; set; }
[JsonIgnore]
public int Kstream{ get; set; }
@ -298,14 +305,11 @@ 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; }
[JsonProperty("stream_endpoint")]
public string? StreamEndpoint{ get; set; }
[JsonProperty("stream_endpoint_settings")]
public CrAuthSettings? StreamEndpoint{ get; set; }
[JsonProperty("stream_endpoint_secondary_settings")]
public CrAuthSettings? StreamEndpointSecondSettings{ get; set; }

View file

@ -11,6 +11,9 @@ public class CrProfile{
[JsonProperty("profile_name")]
public string? ProfileName{ get; set; }
[JsonProperty("profile_id")]
public string? ProfileId{ get; set; }
[JsonProperty("preferred_content_audio_language")]
public string? PreferredContentAudioLanguage{ get; set; }

View file

@ -30,6 +30,8 @@ public class StreamDetails{
public class UrlWithAuth{
public CrAuth? CrAuth{ get; set; }
public bool Video{ get; set; }
public bool Audio{ get; set; }
public string? Url{ get; set; }

View file

@ -14,12 +14,19 @@ 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; }
public string Device_name{ get; set; }
public bool Video{ get; set; }
public bool Audio{ get; set; }
}
public class StreamInfo{
public string Playlist { get; set; }
public bool Audio { get; set; }
public bool Video { get; set; }
}
public class DrmAuthData{
@ -58,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>

View file

@ -29,7 +29,7 @@ A simple crunchyroll downloader that allows you to download your favorite series
## 🛠️ System Requirements
- **Operating System:** Windows 10 or Windows 11
- **.NET Desktop Runtime:** Version 8.0
- **.NET Desktop Runtime:** Version 10.0
- **Visual C++ Redistributable:** 20152022
## 🖥️ Features