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.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Downloader.Crunchyroll.Utils;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
@ -67,6 +68,10 @@ public class CalendarManager{
return forDate; return forDate;
} }
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
await LoadAnilistUpcoming();
}
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us") 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[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); : 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.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"); 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.IsOk){
if (response.ResponseContent.Contains("<title>Just a moment...</title>") || if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
response.ResponseContent.Contains("<title>Access denied</title>") || response.ResponseContent.Contains("<title>Access denied</title>") ||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") || response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
response.ResponseContent.Trim().Equals("error code: 1020") || response.ResponseContent.Trim().Equals("error code: 1020") ||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){ 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)); 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."); Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar.");
} else{ } else{
Console.Error.WriteLine($"Calendar request failed"); Console.Error.WriteLine($"Calendar request failed");
} }
return new CalendarWeek(); return new CalendarWeek();
} }
@ -164,6 +176,24 @@ public class CalendarManager{
Console.Error.WriteLine("No days found in the HTML document."); 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; calendar[weeksMondayDate] = week;
@ -172,14 +202,14 @@ public class CalendarManager{
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){ 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)){ if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
return forDate; return forDate;
} }
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
await LoadAnilistUpcoming();
}
CalendarWeek week = new CalendarWeek(); CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List<CalendarDay>(); week.CalendarDays = new List<CalendarDay>();
@ -201,7 +231,7 @@ public class CalendarManager{
var firstDayOfWeek = week.CalendarDays.First().DateTime; var firstDayOfWeek = week.CalendarDays.First().DateTime;
week.FirstDayOfWeek = firstDayOfWeek; 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 }){ if (newEpisodesBase is{ Data.Count: > 0 }){
var newEpisodes = newEpisodesBase.Data; var newEpisodes = newEpisodesBase.Data;
@ -222,36 +252,22 @@ public class CalendarManager{
DateTime targetDate; DateTime targetDate;
if (CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate){
targetDate = episodeAirDate;
if (targetDate >= oneYearFromNow){ targetDate = premiumAvailableStart;
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
if (freeAvailableStart <= oneYearFromNow){ if (targetDate >= oneYearFromNow){
targetDate = freeAvailableStart; DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
} else{ ? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
targetDate = premiumAvailableStart; : crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
}
}
} else{
targetDate = premiumAvailableStart;
if (targetDate >= oneYearFromNow){ if (freeAvailableStart <= oneYearFromNow){
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc targetDate = freeAvailableStart;
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime() } else{
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate; targetDate = episodeAirDate;
if (freeAvailableStart <= oneYearFromNow){
targetDate = freeAvailableStart;
} else{
targetDate = episodeAirDate;
}
} }
} }
var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter; var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter;
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && 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) : Regex.IsMatch(crBrowseEpisode.EpisodeMetadata.SeasonTitle, @"^Season\s+\d+$", RegexOptions.IgnoreCase)
? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}" ? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}"
: crBrowseEpisode.EpisodeMetadata.SeasonTitle; : crBrowseEpisode.EpisodeMetadata.SeasonTitle;
calEpisode.DateTime = targetDate; calEpisode.DateTime = targetDate;
calEpisode.HasPassed = DateTime.Now > targetDate; calEpisode.HasPassed = DateTime.Now > targetDate;
calEpisode.EpisodeName = crBrowseEpisode.Title; calEpisode.EpisodeName = crBrowseEpisode.Title;
@ -340,7 +356,8 @@ public class CalendarManager{
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")]; 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) 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); calendarDay.CalendarEpisodes.Add(calendarEpisode);
} }
} }
@ -427,7 +444,7 @@ public class CalendarManager{
aniListResponse ??= currentResponse; aniListResponse ??= currentResponse;
if (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; hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false;
@ -436,12 +453,12 @@ public class CalendarManager{
} while (hasNextPage && currentPage < 20); } 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 => list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external =>
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList(); string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
List<CalendarEpisode> calendarEpisodes =[]; List<CalendarEpisode> calendarEpisodes = [];
foreach (var anilistEle in list){ foreach (var anilistEle in list){
var calEp = new CalendarEpisode(); var calEp = new CalendarEpisode();
@ -541,7 +558,7 @@ public class CalendarManager{
oldestRelease.Second, oldestRelease.Second,
calEp.DateTime.Kind calEp.DateTime.Kind
); );
if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){ if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){
adjustedDate = oldestRelease.AddDays(7); adjustedDate = oldestRelease.AddDays(7);
} }

View file

@ -43,11 +43,11 @@ public class CrEpisode(){
} }
if (epsidoe is{ Total: 1, Data: not null } && if (epsidoe is{ Total: 1, Data: not null } &&
(epsidoe.Data.First().Versions ??[]) (epsidoe.Data.First().Versions ?? [])
.GroupBy(v => v.AudioLocale) .GroupBy(v => v.AudioLocale)
.Any(g => g.Count() > 1)){ .Any(g => g.Count() > 1)){
Console.Error.WriteLine("Episode has Duplicate Audio Locales"); 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 //guid for episode id
foreach (var episodeVersionse in list){ foreach (var episodeVersionse in list){
foreach (var version in episodeVersionse){ foreach (var version in episodeVersionse){
@ -173,7 +173,7 @@ public class CrEpisode(){
} }
var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key; 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\)"); 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){ public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){
await crunInstance.CrAuthEndpoint1.RefreshToken(true); await crunInstance.CrAuthEndpoint1.RefreshToken(true);
CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase();
complete.Data =[];
var i = 0;
do{ if (string.IsNullOrEmpty(crLocale)){
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); crLocale = "en-US";
}
if (!string.IsNullOrEmpty(crLocale)){ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
query["locale"] = crLocale;
if (forcedLang){ if (!string.IsNullOrEmpty(crLocale)){
query["force_locale"] = crLocale; query["locale"] = crLocale;
} if (forcedLang){
query["force_locale"] = crLocale;
} }
}
query["start"] = i + ""; query["n"] = requestAmount + "";
query["n"] = "50"; query["sort_by"] = "newly_added";
query["sort_by"] = "newly_added"; query["type"] = "episode";
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){ if (!response.IsOk){
Console.Error.WriteLine("Series Request Failed"); Console.Error.WriteLine("Series Request Failed");
return null; return null;
} }
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
if (series != null){ series?.Data?.Sort((a, b) =>
complete.Total = series.Total; b.EpisodeMetadata.PremiumAvailableDate.CompareTo(a.EpisodeMetadata.PremiumAvailableDate));
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;
}
i += 50; return series;
} while (i < requestAmount && requestAmount < 500);
return complete;
} }
public async Task MarkAsWatched(string episodeId){ 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); var response = await HttpClientReq.Instance.SendHttpRequest(request);

View file

@ -338,7 +338,7 @@ public class CrSeries{
} }
if (episodeList.Total < 1){ if (episodeList.Total < 1){
Console.Error.WriteLine("Season is empty!"); Console.Error.WriteLine($"Season is empty! Uri: {episodeRequest.RequestUri}");
} }
return episodeList; return episodeList;

View file

@ -42,7 +42,7 @@ public class CrunchyrollManager{
public ObservableCollection<HistorySeries> HistoryList = new(); public ObservableCollection<HistorySeries> HistoryList = new();
public HistorySeries SelectedSeries = new HistorySeries{ public HistorySeries SelectedSeries = new HistorySeries{
Seasons =[] Seasons = []
}; };
#endregion #endregion
@ -107,8 +107,8 @@ public class CrunchyrollManager{
options.Partsize = 10; options.Partsize = 10;
options.DlSubs = new List<string>{ "en-US" }; options.DlSubs = new List<string>{ "en-US" };
options.SkipMuxing = false; options.SkipMuxing = false;
options.MkvmergeOptions =[]; options.MkvmergeOptions = [];
options.FfmpegOptions =[]; options.FfmpegOptions = [];
options.DefaultAudio = "ja-JP"; options.DefaultAudio = "ja-JP";
options.DefaultSub = "en-US"; options.DefaultSub = "en-US";
options.QualityAudio = "best"; options.QualityAudio = "best";
@ -128,7 +128,7 @@ public class CrunchyrollManager{
options.CalendarDubFilter = "none"; options.CalendarDubFilter = "none";
options.CustomCalendar = true; options.CustomCalendar = true;
options.DlVideoOnce = 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.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
options.HistoryLang = DefaultLocale; options.HistoryLang = DefaultLocale;
options.FixCccSubtitles = true; options.FixCccSubtitles = true;
@ -201,13 +201,17 @@ public class CrunchyrollManager{
DefaultAndroidAuthSettings = new CrAuthSettings(){ DefaultAndroidAuthSettings = new CrAuthSettings(){
Endpoint = "android/phone", Endpoint = "android/phone",
Authorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=", Client_ID = "pd6uw3dfyhzghs0wxae3",
UserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0", Authorization = "Basic cGQ2dXczZGZ5aHpnaHMwd3hhZTM6NXJ5SjJFQXR3TFc0UklIOEozaWk1anVqbnZrRWRfTkY=",
UserAgent = "Crunchyroll/3.95.2 Android/16 okhttp/4.12.0",
Device_name = "CPH2449", 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(){ CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
Endpoint = "tv/android_tv", Endpoint = "tv/android_tv",
Authorization = "Basic ZGsxYndzemRyc3lkeTR1N2xvenE6bDl0SU1BdTlzTGc4ZjA4ajlfQkQ4eWZmQmZTSms0R0o=", Authorization = "Basic ZGsxYndzemRyc3lkeTR1N2xvenE6bDl0SU1BdTlzTGc4ZjA4ajlfQkQ4eWZmQmZTSms0R0o=",
@ -236,7 +240,7 @@ public class CrunchyrollManager{
// ApiUrls.authBasicMob = "Basic " + token; // 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){ foreach (var file in jsonFiles){
try{ try{
@ -276,13 +280,13 @@ public class CrunchyrollManager{
} }
}); });
} else{ } else{
HistoryList =[]; HistoryList = [];
} }
} else{ } else{
HistoryList =[]; HistoryList = [];
} }
} else{ } else{
HistoryList =[]; HistoryList = [];
} }
@ -303,7 +307,14 @@ public class CrunchyrollManager{
Doing = "Starting" Doing = "Starting"
}; };
QueueManager.Instance.Queue.Refresh(); 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){ if (res.Error){
QueueManager.Instance.DecrementDownloads(); QueueManager.Instance.DecrementDownloads();
@ -356,7 +367,7 @@ public class CrunchyrollManager{
var fileNameAndPath = options.DownloadToTempFolder var fileNameAndPath = options.DownloadToTempFolder
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty) ? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? 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 groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List<Merger>(); var mergers = new List<Merger>();
foreach (var keyValue in groupByDub){ foreach (var keyValue in groupByDub){
@ -421,7 +432,7 @@ public class CrunchyrollManager{
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data); if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
} }
if (options.DownloadToTempFolder){ if (options.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.options.Subtitles); await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.options.Subtitles);
} }
@ -481,7 +492,22 @@ public class CrunchyrollManager{
} }
if (options.DownloadToTempFolder){ 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){ foreach (var downloadedMedia in subs){
var subt = new SubtitleFonts(); var subt = new SubtitleFonts();
subt.Language = downloadedMedia.Language; subt.Language = downloadedMedia.Language;
subt.Fonts = downloadedMedia.Fonts ??[]; subt.Fonts = downloadedMedia.Fonts ?? [];
subsList.Add(subt); subsList.Add(subt);
} }
@ -701,7 +727,7 @@ public class CrunchyrollManager{
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput 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(), { File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
KeepAllVideos = options.KeepAllVideos, 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(), Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle, VideoTitle = options.VideoTitle,
Options = new MuxOptions(){ Options = new MuxOptions(){
@ -718,8 +744,8 @@ public class CrunchyrollManager{
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay, DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
CcSubsMuxingFlag = options.CcSubsMuxingFlag, CcSubsMuxingFlag = options.CcSubsMuxingFlag,
SignsSubsAsForced = options.SignsSubsAsForced, SignsSubsAsForced = options.SignsSubsAsForced,
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).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() :[], 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)){ if (!File.Exists(CfgManager.PathFFMPEG)){
@ -731,7 +757,7 @@ public class CrunchyrollManager{
} }
bool isMuxed, syncError = false; 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){ 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 (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
if (!File.Exists(CfgManager.PathFFMPEG)){ if (!File.Exists(CfgManager.PathFFMPEG)){
Console.Error.WriteLine("Missing ffmpeg"); 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{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
@ -823,7 +850,8 @@ public class CrunchyrollManager{
if (!File.Exists(CfgManager.PathMKVMERGE)){ if (!File.Exists(CfgManager.PathMKVMERGE)){
Console.Error.WriteLine("Missing Mkvmerge"); 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{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
@ -857,7 +885,8 @@ public class CrunchyrollManager{
if (!_widevine.canDecrypt){ if (!_widevine.canDecrypt){
Console.Error.WriteLine("CDM files missing"); 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{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
@ -904,11 +933,10 @@ public class CrunchyrollManager{
options.Partsize = options.Partsize > 0 ? options.Partsize : 1; options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
if (options.DownloadDescriptionAudio){ if (options.DownloadDescriptionAudio){
var alreadyAdr = new HashSet<string>( var alreadyAdr = new HashSet<string>(
data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err") data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err")
); );
bool HasDescriptionRole(IEnumerable<string>? roles) => bool HasDescriptionRole(IEnumerable<string>? roles) =>
roles?.Any(r => string.Equals(r, "description", StringComparison.OrdinalIgnoreCase)) == true; 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")) .Where(m => m.Versions?.Any(v => (v.AudioLocale == (m.Lang?.CrLocale ?? "err"))
&& HasDescriptionRole(v.roles)) == true) && HasDescriptionRole(v.roles)) == true)
.ToList(); .ToList();
var additions = toDuplicate.Select(m => new CrunchyEpMetaData{ var additions = toDuplicate.Select(m => new CrunchyEpMetaData{
MediaId = m.MediaId, MediaId = m.MediaId,
Lang = m.Lang, Lang = m.Lang,
@ -1024,13 +1052,18 @@ public class CrunchyrollManager{
#endregion #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; (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; var errorJson = fetchPlaybackData.error;
if (!string.IsNullOrEmpty(errorJson)){ if (!string.IsNullOrEmpty(errorJson)){
var error = StreamError.FromJson(errorJson); var error = StreamError.FromJson(errorJson);
@ -1077,7 +1110,7 @@ public class CrunchyrollManager{
} }
if (fetchPlaybackData2.IsOk){ 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){ foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){
var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data; var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data;
if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){ if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){
@ -1101,13 +1134,16 @@ public class CrunchyrollManager{
} }
} }
} }
} else{
fetchPlaybackData = fetchPlaybackData2;
}
} }
var pbData = fetchPlaybackData.pbData; var pbData = fetchPlaybackData.pbData;
List<string> hsLangs = new List<string>(); List<string> hsLangs = new List<string>();
var pbStreams = pbData.Data; var pbStreams = pbData?.Data;
var streams = new List<StreamDetailsPop>(); var streams = new List<StreamDetailsPop>();
variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true)); 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("seriesTitle", data.SeriesTitle ?? string.Empty, true));
variables.Add(new Variable("seasonTitle", data.SeasonTitle ?? 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("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){ if (pbStreams?.Keys != null){
var pb = pbStreams.Select(v => { 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); hsLangs.Add(v.Value.HardsubLang.CrLocale);
} }
@ -1216,14 +1252,22 @@ public class CrunchyrollManager{
}; };
} }
} else{ } 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{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = dlFailed, Error = dlFailed,
FileName = "./unknown", FileName = "./unknown",
ErrorText = "No Hardsubs available" ErrorText = "No Hardsubs available"
}; };
}
} }
} }
} }
@ -1263,7 +1307,7 @@ public class CrunchyrollManager{
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang }; var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){ 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){ foreach (var streamUrl in curStream.Url){
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl.Url ?? string.Empty, HttpMethod.Get, true, streamUrl.CrAuth?.Token?.access_token); 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")){ 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); // List<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
if (streamPlaylistsReqResponseList.Count > 0){ if (streamPlaylistsReqResponseList.Count > 0){
HashSet<string> streamServers =[]; HashSet<string> streamServers = [];
Dictionary<string, ServerData> playListData = new Dictionary<string, ServerData>(); ServerData playListData = new ServerData();
foreach (var curStreams in streamPlaylistsReqResponseList){ foreach (var curStreams in streamPlaylistsReqResponseList){
var match = Regex.Match(curStreams.Key ?? string.Empty, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))"); var match = Regex.Match(curStreams.Key ?? string.Empty, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))");
@ -1328,15 +1376,16 @@ public class CrunchyrollManager{
} }
try{ 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); streamServers.UnionWith(streamPlaylists.Data.Keys);
Helpers.MergePlaylistData(playListData, streamPlaylists.Data); Helpers.MergePlaylistData(playListData, streamPlaylists.Data, entry.Audio, entry.Video);
} catch (Exception e){ } catch (Exception e){
Console.Error.WriteLine(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){ if (streamServers.Count == 0){
return new DownloadResponse{ return new DownloadResponse{
@ -1347,17 +1396,20 @@ public class CrunchyrollManager{
}; };
} }
if (options.StreamServer == 0){ playListData.video ??= [];
options.StreamServer = 1; playListData.audio ??= [];
}
// if (options.StreamServer == 0){
// options.StreamServer = 1;
// }
// string selectedServer = streamServers[options.StreamServer - 1]; // string selectedServer = streamServers[options.StreamServer - 1];
// ServerData selectedList = streamPlaylists.Data[selectedServer]; // ServerData selectedList = streamPlaylists.Data[selectedServer];
string selectedServer = streamServers.ToList()[options.StreamServer - 1]; // string selectedServer = streamServers.ToList()[options.StreamServer - 1];
ServerData selectedList = playListData[selectedServer]; // ServerData selectedList = playListData[selectedServer];
var videos = selectedList.video.Select(item => new VideoItem{ var videos = playListData.video.Select(item => new VideoItem{
segments = item.segments, segments = item.segments,
pssh = item.pssh, pssh = item.pssh,
quality = item.quality, 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)" resolutionText = $"{item.quality.width}x{item.quality.height} ({Math.Round(item.bandwidth / 1024.0)}KiB/s)"
}).ToList(); }).ToList();
var audios = selectedList.audio.Select(item => new AudioItem{ var audios = playListData.audio.Select(item => new AudioItem{
@default = item.@default, @default = item.@default,
segments = item.segments, segments = item.segments,
pssh = item.pssh, pssh = item.pssh,
@ -1486,7 +1538,7 @@ public class CrunchyrollManager{
sb.AppendLine($"Selected quality:"); sb.AppendLine($"Selected quality:");
sb.AppendLine($"\tVideo: {chosenVideoSegments.resolutionText}"); sb.AppendLine($"\tVideo: {chosenVideoSegments.resolutionText}");
sb.AppendLine($"\tAudio: {chosenAudioSegments.resolutionText} / {chosenAudioSegments.audioSamplingRate}"); sb.AppendLine($"\tAudio: {chosenAudioSegments.resolutionText} / {chosenAudioSegments.audioSamplingRate}");
sb.AppendLine($"\tServer: {selectedServer}"); sb.AppendLine($"\tServer: {string.Join(", ", playListData.servers)}");
string qualityConsoleLog = sb.ToString(); string qualityConsoleLog = sb.ToString();
Console.WriteLine(qualityConsoleLog); Console.WriteLine(qualityConsoleLog);
@ -1543,9 +1595,12 @@ public class CrunchyrollManager{
Console.WriteLine("Skipping video download..."); Console.WriteLine("Skipping video download...");
} else{ } else{
await CrAuthEndpoint1.RefreshToken(true); await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
Dictionary<string, string> authDataDict = new Dictionary<string, string> 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 } }; { "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); 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){ if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
await CrAuthEndpoint1.RefreshToken(true); await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
if (chosenVideoSegments.encryptionKeys.Count == 0){ if (chosenVideoSegments.encryptionKeys.Count == 0){
Dictionary<string, string> authDataDict = new Dictionary<string, string> 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 } }; { "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); chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
@ -1635,9 +1693,12 @@ public class CrunchyrollManager{
} }
await CrAuthEndpoint1.RefreshToken(true); await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
Dictionary<string, string> authDataDict = new Dictionary<string, string> 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 } }; { "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; var encryptionKeys = chosenVideoSegments.encryptionKeys;
@ -1895,7 +1956,7 @@ public class CrunchyrollManager{
var isAbsolute = Path.IsPathRooted(outFile); var isAbsolute = Path.IsPathRooted(outFile);
// Get all directory parts of the path except the last segment (assuming it's a file) // 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 // Initialize the cumulative path based on whether the original path is absolute or not
var cumulativePath = isAbsolute ? "" : fileDir; var cumulativePath = isAbsolute ? "" : fileDir;
@ -1990,7 +2051,7 @@ public class CrunchyrollManager{
Console.WriteLine($"{fileName}.xml has been created with the description."); 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")){ if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
var bitmap = await Helpers.LoadImage(data.ImageBig); var bitmap = await Helpers.LoadImage(data.ImageBig);
if (bitmap != null){ if (bitmap != null){
@ -2040,8 +2101,8 @@ public class CrunchyrollManager{
videoDownloadMedia.Lang = pbData.Meta.AudioLocale; videoDownloadMedia.Lang = pbData.Meta.AudioLocale;
} }
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[]; List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ?? [];
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ??[]; List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ?? [];
var subsDataMapped = subsData.Select(s => { var subsDataMapped = subsData.Select(s => {
var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue()); var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
return new{ return new{
@ -2348,7 +2409,8 @@ public class CrunchyrollManager{
#region Fetch Playback Data #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{ var temppbData = new PlaybackData{
Total = 0, Total = 0,
Data = new Dictionary<string, StreamDetails>() Data = new Dictionary<string, StreamDetails>()
@ -2364,7 +2426,7 @@ public class CrunchyrollManager{
} }
if (playbackRequestResponse.IsOk){ if (playbackRequestResponse.IsOk){
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint); temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
} else{ } else{
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback"); Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play{(auioRoleDesc ? "?audioRole=description" : "")}"; playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play{(auioRoleDesc ? "?audioRole=description" : "")}";
@ -2375,7 +2437,7 @@ public class CrunchyrollManager{
} }
if (playbackRequestResponse.IsOk){ if (playbackRequestResponse.IsOk){
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint); temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
} else{ } else{
Console.Error.WriteLine("Fallback Request Stream URLs FAILED!"); Console.Error.WriteLine("Fallback Request Stream URLs FAILED!");
} }
@ -2405,7 +2467,7 @@ public class CrunchyrollManager{
return response; 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{ var temppbData = new PlaybackData{
Total = 0, Total = 0,
Data = new Dictionary<string, StreamDetails>() Data = new Dictionary<string, StreamDetails>()
@ -2424,7 +2486,7 @@ public class CrunchyrollManager{
foreach (var hardsub in playStream.HardSubs){ foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value; var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ 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, IsHardsubbed = true,
HardsubLocale = stream.Hlang, HardsubLocale = stream.Hlang,
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue()) HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
@ -2433,7 +2495,7 @@ public class CrunchyrollManager{
} }
derivedPlayCrunchyStreams[""] = new StreamDetails{ 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, IsHardsubbed = false,
HardsubLocale = Locale.DefaulT, HardsubLocale = Locale.DefaulT,
HardsubLang = Languages.DEFAULT_lang 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] [ObservableProperty]
private ComboBoxItem _selectedHSLang; private ComboBoxItem _selectedHSLang;
[ObservableProperty]
private bool _hsRawFallback;
[ObservableProperty] [ObservableProperty]
private ComboBoxItem _selectedDescriptionLang; private ComboBoxItem _selectedDescriptionLang;
@ -151,15 +154,18 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private ComboBoxItem _selectedStreamEndpoint; private ComboBoxItem _selectedStreamEndpoint;
[ObservableProperty]
private bool _firstEndpointVideo;
[ObservableProperty]
private bool _firstEndpointAudio;
[ObservableProperty] [ObservableProperty]
private ComboBoxItem _SelectedStreamEndpointSecondary; private ComboBoxItem _SelectedStreamEndpointSecondary;
[ObservableProperty] [ObservableProperty]
private string _endpointAuthorization = ""; private string _endpointAuthorization = "";
[ObservableProperty]
private string _endpointClientId = "";
[ObservableProperty] [ObservableProperty]
private string _endpointUserAgent = ""; private string _endpointUserAgent = "";
@ -169,6 +175,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private string _endpointDeviceType = ""; private string _endpointDeviceType = "";
[ObservableProperty]
private bool _endpointVideo;
[ObservableProperty]
private bool _endpointAudio;
[ObservableProperty] [ObservableProperty]
private bool _isLoggingIn; private bool _isLoggingIn;
@ -188,7 +200,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
private ComboBoxItem? _selectedAudioQuality; private ComboBoxItem? _selectedAudioQuality;
[ObservableProperty] [ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedSubLang =[]; private ObservableCollection<ListBoxItem> _selectedSubLang = [];
[ObservableProperty] [ObservableProperty]
private Color _listBoxColor; private Color _listBoxColor;
@ -231,12 +243,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "ar-SA" } 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; } =[ public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
@ -277,7 +289,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "tv/android_tv" }, new(){ Content = "tv/android_tv" },
]; ];
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[]; public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } = [];
[ObservableProperty] [ObservableProperty]
private StringItemWithDisplayName _selectedFFmpegHWAccel; 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; ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0]; 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]; SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null; ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0]; SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty; EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty;
EndpointClientId = options.StreamEndpointSecondSettings?.Client_ID ?? string.Empty;
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty; EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty; EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? 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 == "???"){ if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
EndpointNotSignedWarning = true; EndpointNotSignedWarning = true;
@ -363,6 +379,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions()); 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; StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0]; SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0];
@ -390,6 +413,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options); SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
HsRawFallback = options.HsRawFallback;
FixCccSubtitles = options.FixCccSubtitles; FixCccSubtitles = options.FixCccSubtitles;
ConvertVtt2Ass = options.ConvertVtt2Ass; ConvertVtt2Ass = options.ConvertVtt2Ass;
SubsDownloadDuplicate = options.SubsDownloadDuplicate; 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.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.HsRawFallback = HsRawFallback;
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + ""; CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
var endpointSettingsFirst = new CrAuthSettings();
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + ""; endpointSettingsFirst.Endpoint = SelectedStreamEndpoint.Content + "";
endpointSettingsFirst.Video = FirstEndpointVideo;
endpointSettingsFirst.Audio = FirstEndpointAudio;
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = endpointSettingsFirst;
var endpointSettings = new CrAuthSettings(); var endpointSettings = new CrAuthSettings();
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + ""; endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
endpointSettings.Authorization = EndpointAuthorization; endpointSettings.Authorization = EndpointAuthorization;
endpointSettings.Client_ID = EndpointClientId;
endpointSettings.UserAgent = EndpointUserAgent; endpointSettings.UserAgent = EndpointUserAgent;
endpointSettings.Device_name = EndpointDeviceName; endpointSettings.Device_name = EndpointDeviceName;
endpointSettings.Device_type = EndpointDeviceType; endpointSettings.Device_type = EndpointDeviceType;
endpointSettings.Video = EndpointVideo;
endpointSettings.Audio = EndpointAudio;
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings; CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
@ -657,13 +686,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
} }
} }
} else{ } else{
CrunchyrollManager.Instance.HistoryList =[]; CrunchyrollManager.Instance.HistoryList = [];
} }
} }
_ = SonarrClient.Instance.RefreshSonarrLite(); _ = SonarrClient.Instance.RefreshSonarrLite();
} else{ } else{
CrunchyrollManager.Instance.HistoryList =[]; CrunchyrollManager.Instance.HistoryList = [];
} }
} }
} }
@ -703,7 +732,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0]; SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization; EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization;
EndpointClientId = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Client_ID;
EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent; EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent;
EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name; EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name;
EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type; EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type;
@ -759,11 +787,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
return MapHWAccelOptions(accels); return MapHWAccelOptions(accels);
} }
} catch (Exception e){ } 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){ private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){

View file

@ -75,6 +75,12 @@
</ComboBox> </ComboBox>
</controls:SettingsExpander.Footer> </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> </controls:SettingsExpander>
@ -249,12 +255,27 @@
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Stream Endpoint " IsEnabled="False"> <controls:SettingsExpanderItem Content="Stream Endpoint ">
<controls:SettingsExpanderItem.Footer> <controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400" <StackPanel>
ItemsSource="{Binding StreamEndpoints}" <ComboBox IsEnabled="False" HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
SelectedItem="{Binding SelectedStreamEndpoint}"> ItemsSource="{Binding StreamEndpoints}"
</ComboBox> 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.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
@ -266,33 +287,38 @@
SelectedItem="{Binding SelectedStreamEndpointSecondary}"> SelectedItem="{Binding SelectedStreamEndpointSecondary}">
</ComboBox> </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"> <StackPanel Margin="0,5">
<TextBlock Text="Authorization" /> <TextBlock Text="Authorization" />
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250" <TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointAuthorization}" /> Text="{Binding EndpointAuthorization}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Client Id" />
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding EndpointClientId}" />
</StackPanel>
<StackPanel Margin="0,5"> <StackPanel Margin="0,5">
<TextBlock Text="User Agent" /> <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}" /> Text="{Binding EndpointUserAgent}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,5"> <StackPanel Margin="0,5">
<TextBlock Text="Device Type" /> <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}" /> Text="{Binding EndpointDeviceType}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,5"> <StackPanel Margin="0,5">
<TextBlock Text="Device Name" /> <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}" /> Text="{Binding EndpointDeviceName}" />
</StackPanel> </StackPanel>

View file

@ -1,6 +1,7 @@
using System; using System;
using Avalonia; using Avalonia;
using System.Linq; using System.Linq;
using ReactiveUI.Avalonia;
namespace CRD; namespace CRD;
@ -26,7 +27,8 @@ sealed class Program{
var builder = AppBuilder.Configure<App>() var builder = AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()
.LogToTrace(); .LogToTrace()
.UseReactiveUI() ;
if (isHeadless){ if (isHeadless){
Console.WriteLine("Running in headless mode..."); 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 ToKbps(int bps) => (int)Math.Round(bps / 1000.0);
public static int SnapToAudioBucket(int kbps){ 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(); return buckets.OrderBy(b => Math.Abs(b - kbps)).First();
} }
public static int WidthBucket(int width, int height){ public static int WidthBucket(int width, int height){
int expected = (int)Math.Round(height * 16 / 9.0); int expected = (int)Math.Round(height * 16 / 9.0);
int tol = Math.Max(8, (int)(expected * 0.02)); // ~2% or ≥8 px int tol = Math.Max(8, (int)(expected * 0.02)); // ~2% or ≥8 px
@ -555,7 +557,7 @@ public class Helpers{
return CosineSimilarity(vector1, vector2); return CosineSimilarity(vector1, vector2);
} }
private static readonly char[] Delimiters ={ ' ', ',', '.', ';', ':', '-', '_', '\'' }; private static readonly char[] Delimiters = { ' ', ',', '.', ';', ':', '-', '_', '\'' };
public static Dictionary<string, double> ComputeWordFrequency(string text){ public static Dictionary<string, double> ComputeWordFrequency(string text){
var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase); var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
@ -718,7 +720,7 @@ public class Helpers{
bool isValid = !folderName.Any(c => invalidChars.Contains(c)); bool isValid = !folderName.Any(c => invalidChars.Contains(c));
// Check for reserved names on Windows // 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()); bool isReservedName = reservedNames.Contains(folderName.ToUpperInvariant());
if (isValid && !isReservedName && folderName.Length <= 255){ 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 public static void MergePlaylistData(
existing.video ??=[]; ServerData target,
if (kvp.Value.video != null) Dictionary<string, ServerData> source,
existing.video.AddRange(kvp.Value.video); bool mergeAudio,
} else{ bool mergeVideo){
// Add new entry (clone lists to avoid reference issues) if (target == null) throw new ArgumentNullException(nameof(target));
target[kvp.Key] = new ServerData{ if (source == null) throw new ArgumentNullException(nameof(source));
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>() 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); private static readonly SemaphoreSlim ShutdownLock = new(1, 1);
public static async Task ShutdownComputer(){ 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; private HttpClient client;
public readonly bool useFlareSolverr;
private FlareSolverrClient flareSolverrClient;
public HttpClientReq(){ public HttpClientReq(){
IWebProxy systemProxy = WebRequest.DefaultWebProxy; IWebProxy systemProxy = WebRequest.DefaultWebProxy;
@ -79,6 +82,11 @@ public class HttpClientReq{
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br"); client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
// client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.5"); // client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.5");
client.DefaultRequestHeaders.Connection.ParseAdd("keep-alive"); 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(){ 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){ private void CaptureResponseCookies(HttpResponseMessage response, Uri requestUri, Dictionary<string, CookieCollection>? cookieStore){
if (cookieStore == null){ if (cookieStore == null){
return; 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 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 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 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 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"; 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){ public Merger(MergerOptions options){
this.options = 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){ if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){
this.options.VideoTitle = this.options.VideoTitle.Replace("\"", "'"); this.options.VideoTitle = this.options.VideoTitle.Replace("\"", "'");
@ -74,35 +71,39 @@ public class Merger{
index++; 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 })){ foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
if (sub.value.Delay != null && sub.value.Delay != 0){ if (sub.value.Delay != null && sub.value.Delay != 0){
double delay = sub.value.Delay / 1000.0 ?? 0; double delay = sub.value.Delay / 1000.0 ?? 0;
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}"); 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(metaData);
// args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}")); // args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
args.Add("-c:v copy"); args.Add("-c:v copy");
args.Add("-c:a copy"); args.Add("-c:a copy");
args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass"); args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass");
args.AddRange(options.Subtitles.Select((sub, subindex) => if (!options.SkipSubMux){
$"-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}")); 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)){ if (!string.IsNullOrEmpty(options.VideoTitle)){
args.Add($"-metadata title=\"{options.VideoTitle}\""); args.Add($"-metadata title=\"{options.VideoTitle}\"");
@ -134,9 +135,9 @@ public class Merger{
} }
var audio = options.OnlyAudio.First(); var audio = options.OnlyAudio.First();
args.Add($"-i \"{audio.Path}\""); 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}\""); args.Add($"\"{options.Output}\"");
return string.Join(" ", args); return string.Join(" ", args);
} }
@ -170,7 +171,7 @@ public class Merger{
// var sortedAudio = options.OnlyAudio // var sortedAudio = options.OnlyAudio
// .OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue) // .OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue)
// .ToList(); // .ToList();
var rank = options.DubLangList var rank = options.DubLangList
.Select((val, i) => new{ val, i }) .Select((val, i) => new{ val, i })
.ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase); .ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase);
@ -204,7 +205,7 @@ public class Merger{
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\""); 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); bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
var sortedSubtitles = options.Subtitles var sortedSubtitles = options.Subtitles
@ -274,7 +275,7 @@ public class Merger{
if (options.Description is{ Count: > 0 }){ if (options.Description is{ Count: > 0 }){
args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\""); args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\"");
} }
if (options.Cover.Count > 0){ if (options.Cover.Count > 0){
if (File.Exists(options.Cover.First().Path)){ if (File.Exists(options.Cover.First().Path)){
args.Add($"--attach-file \"{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")); allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".new.resume"));
options.Description?.ForEach(description => Helpers.DeleteFile(description.Path)); options.Description?.ForEach(description => Helpers.DeleteFile(description.Path));
options.Cover?.ForEach(cover => Helpers.DeleteFile(cover.Path)); options.Cover?.ForEach(cover => Helpers.DeleteFile(cover.Path));
// Delete chapter files if any // Delete chapter files if any
options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path)); options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
// Delete subtitle files if (!options.SkipSubMux){
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File)); // 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> DubLangList{ get; set; } = new List<string>();
public List<string> SubLangList{ get; set; } = new List<string>(); public List<string> SubLangList{ get; set; } = new List<string>();
public string Output{ get; set; } public string Output{ get; set; }
public bool? SkipSubMux{ get; set; } public bool SkipSubMux{ get; set; }
public bool? KeepAllVideos{ get; set; } public bool? KeepAllVideos{ get; set; }
public bool? Novids{ get; set; } public bool? Novids{ get; set; }
public bool Mp4{ get; set; } public bool Mp4{ get; set; }
@ -524,7 +527,7 @@ public class MergerOptions{
public string VideoTitle{ get; set; } public string VideoTitle{ get; set; }
public bool? KeepAllVideos{ get; set; } public bool? KeepAllVideos{ get; set; }
public List<ParsedFont> Fonts{ get; set; } = new List<ParsedFont>(); 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 MuxOptions Options{ get; set; }
public Defaults Defaults{ get; set; } public Defaults Defaults{ get; set; }
public bool mp3{ get; set; } public bool mp3{ get; set; }

View file

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

View file

@ -134,6 +134,10 @@ public class CrDownloadOptions{
[JsonProperty("proxy_password")] [JsonProperty("proxy_password")]
public string? ProxyPassword{ get; set; } public string? ProxyPassword{ get; set; }
[JsonProperty("flare_solverr_properties")]
public FlareSolverrProperties? FlareSolverrProperties{ get; set; }
#endregion #endregion
@ -150,6 +154,9 @@ public class CrDownloadOptions{
[JsonProperty("hard_sub_lang")] [JsonProperty("hard_sub_lang")]
public string Hslang{ get; set; } = ""; public string Hslang{ get; set; } = "";
[JsonProperty("hard_sub_raw_fallback")]
public bool HsRawFallback{ get; set; }
[JsonIgnore] [JsonIgnore]
public int Kstream{ get; set; } public int Kstream{ get; set; }
@ -298,14 +305,11 @@ public class CrDownloadOptions{
[JsonProperty("calendar_hide_dubs")] [JsonProperty("calendar_hide_dubs")]
public bool CalendarHideDubs{ get; set; } public bool CalendarHideDubs{ get; set; }
[JsonProperty("calendar_filter_by_air_date")]
public bool CalendarFilterByAirDate{ get; set; }
[JsonProperty("calendar_show_upcoming_episodes")] [JsonProperty("calendar_show_upcoming_episodes")]
public bool CalendarShowUpcomingEpisodes{ get; set; } public bool CalendarShowUpcomingEpisodes{ get; set; }
[JsonProperty("stream_endpoint")] [JsonProperty("stream_endpoint_settings")]
public string? StreamEndpoint{ get; set; } public CrAuthSettings? StreamEndpoint{ get; set; }
[JsonProperty("stream_endpoint_secondary_settings")] [JsonProperty("stream_endpoint_secondary_settings")]
public CrAuthSettings? StreamEndpointSecondSettings{ get; set; } public CrAuthSettings? StreamEndpointSecondSettings{ get; set; }

View file

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

View file

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

View file

@ -14,12 +14,19 @@ public class AuthData{
public class CrAuthSettings{ public class CrAuthSettings{
public string Endpoint{ get; set; } public string Endpoint{ get; set; }
public string Client_ID{ get; set; }
public string Authorization{ get; set; } public string Authorization{ get; set; }
public string UserAgent{ get; set; } public string UserAgent{ get; set; }
public string Device_type{ get; set; } public string Device_type{ get; set; }
public string Device_name{ 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{ public class DrmAuthData{
@ -58,8 +65,8 @@ public class CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool?
} }
public class CrunchySeriesList{ public class CrunchySeriesList{
public List<Episode> List{ get; set; } public List<Episode> List{ get; set; } = [];
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; } public Dictionary<string, EpisodeAndLanguage> Data{ get; set; } = [];
} }
public class Episode{ public class Episode{

View file

@ -16,6 +16,8 @@ using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music; using CRD.Utils.Structs.Crunchyroll.Music;
using CRD.Views;
using ReactiveUI;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -210,6 +212,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
} }
} else if (AddAllEpisodes){ } else if (AddAllEpisodes){
var musicClass = CrunchyrollManager.Instance.CrMusic; var musicClass = CrunchyrollManager.Instance.CrMusic;
if (currentMusicVideoList == null) return;
foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){ foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){
QueueManager.Instance.CrAddMusicMetaToQueue(meta); 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){ private string DetermineLocale(string locale){
@ -525,15 +530,35 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
var list = await FetchSeriesListAsync(value.Id); var list = await FetchSeriesListAsync(value.Id);
if (list != null){ if (list is{ List.Count: > 0 }){
currentSeriesList = list; currentSeriesList = list;
await SearchPopulateEpisodesBySeason(value.Id); await SearchPopulateEpisodesBySeason(value.Id);
UpdateUiForEpisodeSelection(); UpdateUiForEpisodeSelection();
} else{ } 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(){ private void UpdateUiForSearchSelection(){
SearchPopupVisible = false; SearchPopupVisible = false;
RaisePropertyChanged(nameof(SearchVisible)); 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(){ private void UpdateUiForEpisodeSelection(){

View file

@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Downloader.Crunchyroll.Utils;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using DynamicData; using DynamicData;
@ -18,7 +19,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _prevButtonEnabled = true; private bool _prevButtonEnabled = true;
[ObservableProperty] [ObservableProperty]
private bool _nextButtonEnabled = true; private bool _nextButtonEnabled = true;
@ -28,9 +29,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _customCalendar; private bool _customCalendar;
[ObservableProperty]
private bool _filterByAirDate;
[ObservableProperty] [ObservableProperty]
private bool _showUpcomingEpisodes; private bool _showUpcomingEpisodes;
@ -75,7 +73,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar; CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs; HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate;
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes; ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null; 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(){ private string GetThisWeeksMondayDate(){
DateTime today = DateTime.Today; DateTime today = DateTime.Today;
@ -104,13 +100,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
return formattedDate; return formattedDate;
} }
public async void LoadCalendar(string mondayDate,DateTime customCalDate, bool forceUpdate){ public async void LoadCalendar(string mondayDate, DateTime customCalDate, bool forceUpdate){
ShowLoading = true; ShowLoading = true;
CalendarWeek week; CalendarWeek week;
if (CustomCalendar){ if (CustomCalendar){
if (customCalDate.Date == DateTime.Now.Date){ if (customCalDate.Date == DateTime.Now.Date){
PrevButtonEnabled = false; PrevButtonEnabled = false;
NextButtonEnabled = true; NextButtonEnabled = true;
@ -118,7 +113,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
PrevButtonEnabled = true; PrevButtonEnabled = true;
NextButtonEnabled = false; NextButtonEnabled = false;
} }
week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate); week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate);
} else{ } else{
PrevButtonEnabled = true; PrevButtonEnabled = true;
@ -140,29 +135,24 @@ public partial class CalendarPageViewModel : ViewModelBase{
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){ foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
if (calendarDayCalendarEpisode.ImageBitmap == null){ if (calendarDayCalendarEpisode.ImageBitmap == null){
if (calendarDayCalendarEpisode.AnilistEpisode){ if (calendarDayCalendarEpisode.AnilistEpisode){
_ = calendarDayCalendarEpisode.LoadImage(100,150); _ = calendarDayCalendarEpisode.LoadImage(100, 150);
} else{ } else{
_ = calendarDayCalendarEpisode.LoadImage(); _ = calendarDayCalendarEpisode.LoadImage();
} }
} }
} }
} }
} else{ } else{
foreach (var calendarDay in CalendarDays){ foreach (var calendarDay in CalendarDays){
var episodesCopy = new List<CalendarEpisode>(calendarDay.CalendarEpisodes); if (HideDubs)
foreach (var calendarDayCalendarEpisode in episodesCopy){ calendarDay.CalendarEpisodes.RemoveAll(e => CrSimulcastCalendarFilter.IsDubOrAltLanguageSeason(e.SeasonName));
if (calendarDayCalendarEpisode.SeasonName != null && HideDubs && calendarDayCalendarEpisode.SeasonName.EndsWith("Dub)")){
calendarDay.CalendarEpisodes.Remove(calendarDayCalendarEpisode);
continue;
}
if (calendarDayCalendarEpisode.ImageBitmap == null){ foreach (var e in calendarDay.CalendarEpisodes){
if (calendarDayCalendarEpisode.AnilistEpisode){ if (e.ImageBitmap == null){
_ = calendarDayCalendarEpisode.LoadImage(100,150); if (e.AnilistEpisode)
} else{ _ = e.LoadImage(100, 150);
_ = calendarDayCalendarEpisode.LoadImage(); else
} _ = e.LoadImage();
} }
} }
} }
@ -199,7 +189,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
refreshDate = currentWeek.FirstDayOfWeek.AddDays(6); refreshDate = currentWeek.FirstDayOfWeek.AddDays(6);
} }
LoadCalendar(mondayDate,refreshDate, true); LoadCalendar(mondayDate, refreshDate, true);
} }
[RelayCommand] [RelayCommand]
@ -215,13 +205,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
} else{ } else{
mondayDate = GetThisWeeksMondayDate(); mondayDate = GetThisWeeksMondayDate();
} }
var refreshDate = DateTime.Now; var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){ if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1); refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
} }
LoadCalendar(mondayDate,refreshDate, false); LoadCalendar(mondayDate, refreshDate, false);
} }
[RelayCommand] [RelayCommand]
@ -237,15 +227,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
} else{ } else{
mondayDate = GetThisWeeksMondayDate(); mondayDate = GetThisWeeksMondayDate();
} }
var refreshDate = DateTime.Now; var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){ if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13); 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; CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value;
LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true); LoadCalendar(GetThisWeeksMondayDate(), DateTime.Now, true);
CfgManager.WriteCrSettings(); CfgManager.WriteCrSettings();
} }
@ -282,15 +270,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
CfgManager.WriteCrSettings(); CfgManager.WriteCrSettings();
} }
partial void OnFilterByAirDateChanged(bool value){
if (loading){
return;
}
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
CfgManager.WriteCrSettings();
}
partial void OnShowUpcomingEpisodesChanged(bool value){ partial void OnShowUpcomingEpisodesChanged(bool value){
if (loading){ if (loading){
return; return;
@ -310,7 +289,4 @@ public partial class CalendarPageViewModel : ViewModelBase{
CfgManager.WriteCrSettings(); CfgManager.WriteCrSettings();
} }
} }
} }

View file

@ -207,6 +207,18 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private string _proxyPassword; private string _proxyPassword;
[ObservableProperty]
private string _flareSolverrHost = "localhost";
[ObservableProperty]
private string _flareSolverrPort = "8191";
[ObservableProperty]
private bool _flareSolverrUseSsl = false;
[ObservableProperty]
private bool _useFlareSolverr = false;
[ObservableProperty] [ObservableProperty]
private string _tempDownloadDirPath; private string _tempDownloadDirPath;
@ -264,6 +276,15 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
SonarrPort = props.Port + ""; SonarrPort = props.Port + "";
SonarrApiKey = props.ApiKey + ""; 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; ProxyEnabled = options.ProxyEnabled;
ProxySocks = options.ProxySocks; ProxySocks = options.ProxySocks;
@ -362,9 +383,22 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
} }
props.ApiKey = SonarrApiKey; props.ApiKey = SonarrApiKey;
settings.SonarrProperties = props; 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; settings.LogMode = LogMode;

View file

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

View file

@ -120,7 +120,10 @@ public partial class MainWindow : AppWindow{
.Subscribe(message => ShowToast(message.Message ?? string.Empty, message.Type, message.Seconds)); .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)) if (activeErrors.Contains(message))
return; return;
@ -132,14 +135,14 @@ public partial class MainWindow : AppWindow{
CloseButtonText = "Close" CloseButtonText = "Close"
}; };
if (githubWikiButton){ if (!string.IsNullOrEmpty(urlButtonText)){
dialog.PrimaryButtonText = "Github Wiki"; dialog.PrimaryButtonText = urlButtonText;
} }
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary){ if (result == ContentDialogResult.Primary){
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki"); Helpers.OpenUrl(url);
} }
activeErrors.Remove(message); activeErrors.Remove(message);

View file

@ -75,7 +75,7 @@
<CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox> <CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox>
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </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 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> <controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadAllowEarlyStart}"> </CheckBox> <CheckBox IsChecked="{Binding DownloadAllowEarlyStart}"> </CheckBox>
@ -186,7 +186,7 @@
HorizontalAlignment="Stretch" /> HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </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 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:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="10" <controls:NumberBox Minimum="0" Maximum="10"
@ -385,6 +385,40 @@
</controls:SettingsExpander> </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" <controls:SettingsExpander Header="App Appearance"
IconSource="DarkTheme" IconSource="DarkTheme"
Description="Customize the look and feel of the application" Description="Customize the look and feel of the application"
@ -617,7 +651,7 @@
DockPanel.Dock="Left" /> DockPanel.Dock="Left" />
<ColorPicker Color="{Binding CustomAccentColor}" <ColorPicker Color="{Binding CustomAccentColor}"
DockPanel.Dock="Right" /> DockPanel.Dock="Right" />
</DockPanel> </DockPanel>
</StackPanel> </StackPanel>
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>

View file

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