mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 12:00:34 +00:00
- Added **FlareSolverr support** for the default simulcast calendar
- Added **upcoming episodes** to the default simulcast calendar - Adjusted **default simulcast calendar** to filter dubs correctly - Removed **FFmpeg and MKVMerge** from the GitHub package - Adjusted **custom calendar episode fetching** - Fixed **download error** when audio and video were served from different servers - Fixed **crash** when a searched series had no episodes
This commit is contained in:
parent
c5660a87e7
commit
c7687c80e8
19 changed files with 620 additions and 215 deletions
|
|
@ -8,6 +8,7 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Downloader.Crunchyroll.Utils;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.History;
|
||||
|
|
@ -67,6 +68,10 @@ public class CalendarManager{
|
|||
return forDate;
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
await LoadAnilistUpcoming();
|
||||
}
|
||||
|
||||
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us")
|
||||
? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false)
|
||||
: HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false);
|
||||
|
|
@ -75,19 +80,26 @@ public class CalendarManager{
|
|||
request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip, deflate, br");
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
(bool IsOk, string ResponseContent, string error) response;
|
||||
if (!HttpClientReq.Instance.useFlareSolverr){
|
||||
response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
} else{
|
||||
response = await HttpClientReq.Instance.SendFlareSolverrHttpRequest(request);
|
||||
}
|
||||
|
||||
|
||||
if (!response.IsOk){
|
||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||
MessageBus.Current.SendMessage(new ToastMessage("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5));
|
||||
Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar.");
|
||||
} else{
|
||||
Console.Error.WriteLine($"Calendar request failed");
|
||||
}
|
||||
|
||||
return new CalendarWeek();
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +176,24 @@ public class CalendarManager{
|
|||
Console.Error.WriteLine("No days found in the HTML document.");
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
foreach (var calendarDay in week.CalendarDays){
|
||||
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
|
||||
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
|
||||
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
||||
|
||||
foreach (var calendarEpisode in list
|
||||
.Where(e => calendarDay.DateTime.Date.Day == e.DateTime.Date.Day)
|
||||
.Where(e => calendarDay.CalendarEpisodes.All(ele =>
|
||||
ele.CrSeriesID != e.CrSeriesID &&
|
||||
!CrSimulcastCalendarFilter.IsMatch(ele.SeasonName, e.SeasonName, similarityThreshold: 0.5)))){
|
||||
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calendar[weeksMondayDate] = week;
|
||||
|
||||
|
||||
|
|
@ -172,14 +202,14 @@ public class CalendarManager{
|
|||
|
||||
|
||||
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
await LoadAnilistUpcoming();
|
||||
}
|
||||
|
||||
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
|
||||
return forDate;
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
await LoadAnilistUpcoming();
|
||||
}
|
||||
|
||||
|
||||
CalendarWeek week = new CalendarWeek();
|
||||
week.CalendarDays = new List<CalendarDay>();
|
||||
|
|
@ -201,7 +231,7 @@ public class CalendarManager{
|
|||
var firstDayOfWeek = week.CalendarDays.First().DateTime;
|
||||
week.FirstDayOfWeek = firstDayOfWeek;
|
||||
|
||||
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes("", 200, firstDayOfWeek, true);
|
||||
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true);
|
||||
|
||||
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||
var newEpisodes = newEpisodesBase.Data;
|
||||
|
|
@ -222,36 +252,22 @@ public class CalendarManager{
|
|||
|
||||
DateTime targetDate;
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate){
|
||||
targetDate = episodeAirDate;
|
||||
|
||||
if (targetDate >= oneYearFromNow){
|
||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
||||
targetDate = premiumAvailableStart;
|
||||
|
||||
if (freeAvailableStart <= oneYearFromNow){
|
||||
targetDate = freeAvailableStart;
|
||||
} else{
|
||||
targetDate = premiumAvailableStart;
|
||||
}
|
||||
}
|
||||
} else{
|
||||
targetDate = premiumAvailableStart;
|
||||
if (targetDate >= oneYearFromNow){
|
||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
||||
|
||||
if (targetDate >= oneYearFromNow){
|
||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
||||
|
||||
if (freeAvailableStart <= oneYearFromNow){
|
||||
targetDate = freeAvailableStart;
|
||||
} else{
|
||||
targetDate = episodeAirDate;
|
||||
}
|
||||
if (freeAvailableStart <= oneYearFromNow){
|
||||
targetDate = freeAvailableStart;
|
||||
} else{
|
||||
targetDate = episodeAirDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter;
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
|
||||
|
|
@ -280,7 +296,7 @@ public class CalendarManager{
|
|||
: Regex.IsMatch(crBrowseEpisode.EpisodeMetadata.SeasonTitle, @"^Season\s+\d+$", RegexOptions.IgnoreCase)
|
||||
? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}"
|
||||
: crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
||||
|
||||
|
||||
calEpisode.DateTime = targetDate;
|
||||
calEpisode.HasPassed = DateTime.Now > targetDate;
|
||||
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
||||
|
|
@ -340,7 +356,8 @@ public class CalendarManager{
|
|||
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
||||
|
||||
foreach (var calendarEpisode in list.Where(calendarEpisodeAnilist => calendarDay.DateTime.Date.Day == calendarEpisodeAnilist.DateTime.Date.Day)
|
||||
.Where(calendarEpisodeAnilist => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisodeAnilist.CrSeriesID && ele.SeasonName != calendarEpisodeAnilist.SeasonName))){
|
||||
.Where(calendarEpisodeAnilist =>
|
||||
calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisodeAnilist.CrSeriesID && ele.SeasonName != calendarEpisodeAnilist.SeasonName))){
|
||||
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
||||
}
|
||||
}
|
||||
|
|
@ -427,7 +444,7 @@ public class CalendarManager{
|
|||
aniListResponse ??= currentResponse;
|
||||
|
||||
if (aniListResponse != currentResponse){
|
||||
aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ??[]);
|
||||
aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ?? []);
|
||||
}
|
||||
|
||||
hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false;
|
||||
|
|
@ -436,12 +453,12 @@ public class CalendarManager{
|
|||
} while (hasNextPage && currentPage < 20);
|
||||
|
||||
|
||||
var list = aniListResponse.Data?.Page?.AiringSchedules ??[];
|
||||
var list = aniListResponse.Data?.Page?.AiringSchedules ?? [];
|
||||
|
||||
list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external =>
|
||||
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
|
||||
List<CalendarEpisode> calendarEpisodes =[];
|
||||
List<CalendarEpisode> calendarEpisodes = [];
|
||||
|
||||
foreach (var anilistEle in list){
|
||||
var calEp = new CalendarEpisode();
|
||||
|
|
@ -541,7 +558,7 @@ public class CalendarManager{
|
|||
oldestRelease.Second,
|
||||
calEp.DateTime.Kind
|
||||
);
|
||||
|
||||
|
||||
if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){
|
||||
adjustedDate = oldestRelease.AddDays(7);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ public class CrEpisode(){
|
|||
}
|
||||
|
||||
if (epsidoe is{ Total: 1, Data: not null } &&
|
||||
(epsidoe.Data.First().Versions ??[])
|
||||
(epsidoe.Data.First().Versions ?? [])
|
||||
.GroupBy(v => v.AudioLocale)
|
||||
.Any(g => g.Count() > 1)){
|
||||
Console.Error.WriteLine("Episode has Duplicate Audio Locales");
|
||||
var list = (epsidoe.Data.First().Versions ??[]).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList();
|
||||
var list = (epsidoe.Data.First().Versions ?? []).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList();
|
||||
//guid for episode id
|
||||
foreach (var episodeVersionse in list){
|
||||
foreach (var version in episodeVersionse){
|
||||
|
|
@ -173,7 +173,7 @@ public class CrEpisode(){
|
|||
}
|
||||
|
||||
var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key;
|
||||
var images = (item.Images?.Thumbnail ??[new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
|
||||
var images = (item.Images?.Thumbnail ?? [new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
|
||||
|
||||
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
|
||||
|
||||
|
|
@ -237,60 +237,45 @@ public class CrEpisode(){
|
|||
|
||||
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){
|
||||
await crunInstance.CrAuthEndpoint1.RefreshToken(true);
|
||||
CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase();
|
||||
complete.Data =[];
|
||||
|
||||
var i = 0;
|
||||
|
||||
do{
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
if (string.IsNullOrEmpty(crLocale)){
|
||||
crLocale = "en-US";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
if (forcedLang){
|
||||
query["force_locale"] = crLocale;
|
||||
}
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
if (forcedLang){
|
||||
query["force_locale"] = crLocale;
|
||||
}
|
||||
}
|
||||
|
||||
query["start"] = i + "";
|
||||
query["n"] = "50";
|
||||
query["sort_by"] = "newly_added";
|
||||
query["type"] = "episode";
|
||||
query["n"] = requestAmount + "";
|
||||
query["sort_by"] = "newly_added";
|
||||
query["type"] = "episode";
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query);
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
|
||||
if (series != null){
|
||||
complete.Total = series.Total;
|
||||
if (series.Data != null){
|
||||
complete.Data.AddRange(series.Data);
|
||||
if (firstWeekDay != null){
|
||||
if (firstWeekDay.Value.Date <= series.Data.Last().LastPublic && i + 50 == requestAmount){
|
||||
requestAmount += 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else{
|
||||
break;
|
||||
}
|
||||
series?.Data?.Sort((a, b) =>
|
||||
b.EpisodeMetadata.PremiumAvailableDate.CompareTo(a.EpisodeMetadata.PremiumAvailableDate));
|
||||
|
||||
i += 50;
|
||||
} while (i < requestAmount && requestAmount < 500);
|
||||
|
||||
|
||||
return complete;
|
||||
return series;
|
||||
}
|
||||
|
||||
public async Task MarkAsWatched(string episodeId){
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true, crunInstance.CrAuthEndpoint1.Token?.access_token, null);
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true,
|
||||
crunInstance.CrAuthEndpoint1.Token?.access_token, null);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ public class CrSeries{
|
|||
}
|
||||
|
||||
if (episodeList.Total < 1){
|
||||
Console.Error.WriteLine("Season is empty!");
|
||||
Console.Error.WriteLine($"Season is empty! Uri: {episodeRequest.RequestUri}");
|
||||
}
|
||||
|
||||
return episodeList;
|
||||
|
|
|
|||
|
|
@ -838,7 +838,8 @@ public class CrunchyrollManager{
|
|||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
|
||||
if (!File.Exists(CfgManager.PathFFMPEG)){
|
||||
Console.Error.WriteLine("Missing ffmpeg");
|
||||
MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}");
|
||||
MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}", "FFmpeg",
|
||||
"https://github.com/GyanD/codexffmpeg/releases/latest");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
|
|
@ -849,7 +850,8 @@ public class CrunchyrollManager{
|
|||
|
||||
if (!File.Exists(CfgManager.PathMKVMERGE)){
|
||||
Console.Error.WriteLine("Missing Mkvmerge");
|
||||
MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}");
|
||||
MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}", "Mkvmerge",
|
||||
"https://mkvtoolnix.download/downloads.html#windows");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
|
|
@ -883,7 +885,8 @@ public class CrunchyrollManager{
|
|||
|
||||
if (!_widevine.canDecrypt){
|
||||
Console.Error.WriteLine("CDM files missing");
|
||||
MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", true);
|
||||
MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", "GitHub Wiki",
|
||||
"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
|
|
@ -1361,7 +1364,7 @@ public class CrunchyrollManager{
|
|||
// List<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
|
||||
if (streamPlaylistsReqResponseList.Count > 0){
|
||||
HashSet<string> streamServers = [];
|
||||
Dictionary<string, ServerData> playListData = new Dictionary<string, ServerData>();
|
||||
ServerData playListData = new ServerData();
|
||||
|
||||
foreach (var curStreams in streamPlaylistsReqResponseList){
|
||||
var match = Regex.Match(curStreams.Key ?? string.Empty, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))");
|
||||
|
|
@ -1382,7 +1385,7 @@ public class CrunchyrollManager{
|
|||
}
|
||||
}
|
||||
|
||||
options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
|
||||
// options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
|
||||
|
||||
if (streamServers.Count == 0){
|
||||
return new DownloadResponse{
|
||||
|
|
@ -1393,17 +1396,20 @@ public class CrunchyrollManager{
|
|||
};
|
||||
}
|
||||
|
||||
if (options.StreamServer == 0){
|
||||
options.StreamServer = 1;
|
||||
}
|
||||
playListData.video ??= [];
|
||||
playListData.audio ??= [];
|
||||
|
||||
// if (options.StreamServer == 0){
|
||||
// options.StreamServer = 1;
|
||||
// }
|
||||
|
||||
// string selectedServer = streamServers[options.StreamServer - 1];
|
||||
// ServerData selectedList = streamPlaylists.Data[selectedServer];
|
||||
|
||||
string selectedServer = streamServers.ToList()[options.StreamServer - 1];
|
||||
ServerData selectedList = playListData[selectedServer];
|
||||
// string selectedServer = streamServers.ToList()[options.StreamServer - 1];
|
||||
// ServerData selectedList = playListData[selectedServer];
|
||||
|
||||
var videos = selectedList.video.Select(item => new VideoItem{
|
||||
var videos = playListData.video.Select(item => new VideoItem{
|
||||
segments = item.segments,
|
||||
pssh = item.pssh,
|
||||
quality = item.quality,
|
||||
|
|
@ -1411,7 +1417,7 @@ public class CrunchyrollManager{
|
|||
resolutionText = $"{item.quality.width}x{item.quality.height} ({Math.Round(item.bandwidth / 1024.0)}KiB/s)"
|
||||
}).ToList();
|
||||
|
||||
var audios = selectedList.audio.Select(item => new AudioItem{
|
||||
var audios = playListData.audio.Select(item => new AudioItem{
|
||||
@default = item.@default,
|
||||
segments = item.segments,
|
||||
pssh = item.pssh,
|
||||
|
|
@ -1532,7 +1538,7 @@ public class CrunchyrollManager{
|
|||
sb.AppendLine($"Selected quality:");
|
||||
sb.AppendLine($"\tVideo: {chosenVideoSegments.resolutionText}");
|
||||
sb.AppendLine($"\tAudio: {chosenAudioSegments.resolutionText} / {chosenAudioSegments.audioSamplingRate}");
|
||||
sb.AppendLine($"\tServer: {selectedServer}");
|
||||
sb.AppendLine($"\tServer: {string.Join(", ", playListData.servers)}");
|
||||
|
||||
string qualityConsoleLog = sb.ToString();
|
||||
Console.WriteLine(qualityConsoleLog);
|
||||
|
|
@ -1591,8 +1597,10 @@ public class CrunchyrollManager{
|
|||
await CrAuthEndpoint1.RefreshToken(true);
|
||||
await CrAuthEndpoint2.RefreshToken(true);
|
||||
|
||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
||||
{ { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
|
||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>{
|
||||
{ "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },
|
||||
{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty }
|
||||
};
|
||||
|
||||
chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
||||
|
||||
|
|
@ -1626,8 +1634,10 @@ public class CrunchyrollManager{
|
|||
await CrAuthEndpoint2.RefreshToken(true);
|
||||
|
||||
if (chosenVideoSegments.encryptionKeys.Count == 0){
|
||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
||||
{ { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
|
||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>{
|
||||
{ "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },
|
||||
{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty }
|
||||
};
|
||||
|
||||
chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
||||
|
||||
|
|
@ -1685,8 +1695,10 @@ public class CrunchyrollManager{
|
|||
await CrAuthEndpoint1.RefreshToken(true);
|
||||
await CrAuthEndpoint2.RefreshToken(true);
|
||||
|
||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
||||
{ { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
|
||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>{
|
||||
{ "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },
|
||||
{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty }
|
||||
};
|
||||
|
||||
var encryptionKeys = chosenVideoSegments.encryptionKeys;
|
||||
|
||||
|
|
|
|||
147
CRD/Downloader/Crunchyroll/Utils/CrSimulcastCalendarFilter.cs
Normal file
147
CRD/Downloader/Crunchyroll/Utils/CrSimulcastCalendarFilter.cs
Normal 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
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedStreamEndpoint;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _firstEndpointVideo;
|
||||
|
||||
|
|
@ -166,9 +166,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private string _endpointAuthorization = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _endpointClientId = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _endpointUserAgent = "";
|
||||
|
||||
|
|
@ -367,13 +364,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||
|
||||
EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty;
|
||||
EndpointClientId = options.StreamEndpointSecondSettings?.Client_ID ?? string.Empty;
|
||||
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
|
||||
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
|
||||
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty;
|
||||
EndpointVideo = options.StreamEndpointSecondSettings?.Video ?? true;
|
||||
EndpointAudio = options.StreamEndpointSecondSettings?.Audio ?? true;
|
||||
|
||||
|
||||
FirstEndpointVideo = options.StreamEndpoint?.Video ?? true;
|
||||
FirstEndpointAudio = options.StreamEndpoint?.Audio ?? true;
|
||||
|
||||
|
|
@ -383,6 +379,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||
|
||||
if (FFmpegHWAccel.Count == 0){
|
||||
FFmpegHWAccel.Add(new StringItemWithDisplayName{
|
||||
DisplayName = "No hardware acceleration (error)",
|
||||
value = "error"
|
||||
});
|
||||
}
|
||||
|
||||
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
||||
SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0];
|
||||
|
||||
|
|
@ -554,7 +557,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
var endpointSettings = new CrAuthSettings();
|
||||
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
|
||||
endpointSettings.Authorization = EndpointAuthorization;
|
||||
endpointSettings.Client_ID = EndpointClientId;
|
||||
endpointSettings.UserAgent = EndpointUserAgent;
|
||||
endpointSettings.Device_name = EndpointDeviceName;
|
||||
endpointSettings.Device_type = EndpointDeviceType;
|
||||
|
|
@ -730,7 +732,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||
|
||||
EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization;
|
||||
EndpointClientId = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Client_ID;
|
||||
EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent;
|
||||
EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name;
|
||||
EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type;
|
||||
|
|
@ -786,11 +787,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
return MapHWAccelOptions(accels);
|
||||
}
|
||||
} catch (Exception e){
|
||||
Console.WriteLine("Failed to get Available HW Accel Options" + e);
|
||||
Console.Error.WriteLine("Failed to get Available HW Accel Options" + e);
|
||||
}
|
||||
|
||||
var result = new List<StringItemWithDisplayName>();
|
||||
result.Add(new StringItemWithDisplayName{
|
||||
DisplayName = "No hardware acceleration / error",
|
||||
value = "error"
|
||||
});
|
||||
|
||||
return [];
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){
|
||||
|
|
|
|||
|
|
@ -304,12 +304,6 @@
|
|||
Text="{Binding EndpointAuthorization}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5">
|
||||
<TextBlock Text="Client Id" />
|
||||
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||
Text="{Binding EndpointClientId}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5">
|
||||
<TextBlock Text="User Agent" />
|
||||
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||
|
|
|
|||
|
|
@ -847,36 +847,41 @@ public class Helpers{
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static void MergePlaylistData(
|
||||
Dictionary<string, ServerData> target,
|
||||
ServerData target,
|
||||
Dictionary<string, ServerData> source,
|
||||
bool mergeAudio,
|
||||
bool mergeVideo){
|
||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
|
||||
var serverSet = new HashSet<string>(target.servers);
|
||||
|
||||
void AddServer(string s){
|
||||
if (!string.IsNullOrWhiteSpace(s) && serverSet.Add(s))
|
||||
target.servers.Add(s);
|
||||
}
|
||||
|
||||
foreach (var kvp in source){
|
||||
var key = kvp.Key;
|
||||
var src = kvp.Value;
|
||||
|
||||
if (!src.servers.Contains(key))
|
||||
src.servers.Add(key);
|
||||
|
||||
AddServer(key);
|
||||
foreach (var s in src.servers)
|
||||
AddServer(s);
|
||||
|
||||
if (mergeAudio && src.audio != null){
|
||||
target.audio ??= [];
|
||||
target.audio.AddRange(src.audio);
|
||||
}
|
||||
|
||||
if (target.TryGetValue(key, out var existing)){
|
||||
if (mergeAudio){
|
||||
existing.audio ??= [];
|
||||
if (src.audio != null)
|
||||
existing.audio.AddRange(src.audio);
|
||||
}
|
||||
|
||||
if (mergeVideo){
|
||||
existing.video ??= [];
|
||||
if (src.video != null)
|
||||
existing.video.AddRange(src.video);
|
||||
}
|
||||
} else{
|
||||
target[key] = new ServerData{
|
||||
audio = (mergeAudio && src.audio != null)
|
||||
? new List<AudioPlaylist>(src.audio)
|
||||
: new List<AudioPlaylist>(),
|
||||
video = (mergeVideo && src.video != null)
|
||||
? new List<VideoPlaylist>(src.video)
|
||||
: new List<VideoPlaylist>()
|
||||
};
|
||||
if (mergeVideo && src.video != null){
|
||||
target.video ??= [];
|
||||
target.video.AddRange(src.video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
137
CRD/Utils/Http/FlareSolverrClient.cs
Normal file
137
CRD/Utils/Http/FlareSolverrClient.cs
Normal 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; }
|
||||
}
|
||||
|
|
@ -36,6 +36,9 @@ public class HttpClientReq{
|
|||
|
||||
private HttpClient client;
|
||||
|
||||
public readonly bool useFlareSolverr;
|
||||
private FlareSolverrClient flareSolverrClient;
|
||||
|
||||
public HttpClientReq(){
|
||||
IWebProxy systemProxy = WebRequest.DefaultWebProxy;
|
||||
|
||||
|
|
@ -79,6 +82,11 @@ public class HttpClientReq{
|
|||
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
|
||||
// client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.5");
|
||||
client.DefaultRequestHeaders.Connection.ParseAdd("keep-alive");
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties != null && CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties.UseFlareSolverr){
|
||||
useFlareSolverr = true;
|
||||
flareSolverrClient = new FlareSolverrClient();
|
||||
}
|
||||
}
|
||||
|
||||
private HttpMessageHandler CreateHttpClientHandler(){
|
||||
|
|
@ -150,6 +158,24 @@ public class HttpClientReq{
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<(bool IsOk, string ResponseContent, string error)> SendFlareSolverrHttpRequest(HttpRequestMessage request, bool suppressError = false){
|
||||
string content = string.Empty;
|
||||
try{
|
||||
var flareSolverrResponses = await flareSolverrClient.SendViaFlareSolverrAsync(request, []);
|
||||
|
||||
|
||||
content = flareSolverrResponses.ResponseContent;
|
||||
|
||||
return (IsOk: flareSolverrResponses.IsOk, ResponseContent: content, error: "");
|
||||
} catch (Exception e){
|
||||
if (!suppressError){
|
||||
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||
}
|
||||
|
||||
return (IsOk: false, ResponseContent: content, error: "");
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureResponseCookies(HttpResponseMessage response, Uri requestUri, Dictionary<string, CookieCollection>? cookieStore){
|
||||
if (cookieStore == null){
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ public class MPDParsed{
|
|||
}
|
||||
|
||||
public class ServerData{
|
||||
public List<AudioPlaylist> audio{ get; set; } =[];
|
||||
public List<VideoPlaylist> video{ get; set; } =[];
|
||||
public List<string> servers{ get; set; } = [];
|
||||
public List<AudioPlaylist>? audio{ get; set; } =[];
|
||||
public List<VideoPlaylist>? video{ get; set; } =[];
|
||||
}
|
||||
|
||||
public static class MPDParser{
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("proxy_password")]
|
||||
public string? ProxyPassword{ get; set; }
|
||||
|
||||
[JsonProperty("flare_solverr_properties")]
|
||||
public FlareSolverrProperties? FlareSolverrProperties{ get; set; }
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -301,9 +305,6 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("calendar_hide_dubs")]
|
||||
public bool CalendarHideDubs{ get; set; }
|
||||
|
||||
[JsonProperty("calendar_filter_by_air_date")]
|
||||
public bool CalendarFilterByAirDate{ get; set; }
|
||||
|
||||
[JsonProperty("calendar_show_upcoming_episodes")]
|
||||
public bool CalendarShowUpcomingEpisodes{ get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ public class AuthData{
|
|||
|
||||
public class CrAuthSettings{
|
||||
public string Endpoint{ get; set; }
|
||||
public string Client_ID{ get; set; }
|
||||
public string Authorization{ get; set; }
|
||||
public string UserAgent{ get; set; }
|
||||
public string Device_type{ get; set; }
|
||||
|
|
@ -66,8 +65,8 @@ public class CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool?
|
|||
}
|
||||
|
||||
public class CrunchySeriesList{
|
||||
public List<Episode> List{ get; set; }
|
||||
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; }
|
||||
public List<Episode> List{ get; set; } = [];
|
||||
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; } = [];
|
||||
}
|
||||
|
||||
public class Episode{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ using CRD.Downloader.Crunchyroll;
|
|||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.Crunchyroll.Music;
|
||||
using CRD.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
|
|
@ -210,6 +212,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
} else if (AddAllEpisodes){
|
||||
var musicClass = CrunchyrollManager.Instance.CrMusic;
|
||||
if (currentMusicVideoList == null) return;
|
||||
foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){
|
||||
QueueManager.Instance.CrAddMusicMetaToQueue(meta);
|
||||
}
|
||||
|
|
@ -421,7 +424,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
if (SeasonList.Count > 0){
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
}
|
||||
}
|
||||
|
||||
private string DetermineLocale(string locale){
|
||||
|
|
@ -525,15 +530,35 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
var list = await FetchSeriesListAsync(value.Id);
|
||||
|
||||
if (list != null){
|
||||
if (list is{ List.Count: > 0 }){
|
||||
currentSeriesList = list;
|
||||
await SearchPopulateEpisodesBySeason(value.Id);
|
||||
UpdateUiForEpisodeSelection();
|
||||
} else{
|
||||
ButtonEnabled = true;
|
||||
ResetSearch();
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to get Episodes for Series", ToastType.Error, 2));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSearch(){
|
||||
currentMusicVideoList = null;
|
||||
UrlInput = "";
|
||||
selectedEpisodes.Clear();
|
||||
SelectedItems.Clear();
|
||||
Items.Clear();
|
||||
currentSeriesList = null;
|
||||
SeasonList.Clear();
|
||||
episodesBySeason.Clear();
|
||||
AllButtonEnabled = false;
|
||||
AddAllEpisodes = false;
|
||||
ButtonEnabled = false;
|
||||
SearchVisible = true;
|
||||
SlectSeasonVisible = false;
|
||||
ShowLoading = false;
|
||||
SearchEnabled = false; // disable and enable for button text
|
||||
SearchEnabled = true;
|
||||
}
|
||||
|
||||
private void UpdateUiForSearchSelection(){
|
||||
SearchPopupVisible = false;
|
||||
RaisePropertyChanged(nameof(SearchVisible));
|
||||
|
|
@ -602,7 +627,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
if (SeasonList.Count > 0){
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUiForEpisodeSelection(){
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Downloader.Crunchyroll.Utils;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using DynamicData;
|
||||
|
|
@ -18,7 +19,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _prevButtonEnabled = true;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _nextButtonEnabled = true;
|
||||
|
||||
|
|
@ -28,9 +29,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool _customCalendar;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _filterByAirDate;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showUpcomingEpisodes;
|
||||
|
||||
|
|
@ -75,7 +73,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
|
||||
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
|
||||
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
|
||||
FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate;
|
||||
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
|
||||
|
||||
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
|
||||
|
|
@ -87,7 +84,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
|
||||
|
||||
private string GetThisWeeksMondayDate(){
|
||||
DateTime today = DateTime.Today;
|
||||
|
||||
|
|
@ -104,13 +100,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
return formattedDate;
|
||||
}
|
||||
|
||||
public async void LoadCalendar(string mondayDate,DateTime customCalDate, bool forceUpdate){
|
||||
public async void LoadCalendar(string mondayDate, DateTime customCalDate, bool forceUpdate){
|
||||
ShowLoading = true;
|
||||
|
||||
|
||||
CalendarWeek week;
|
||||
|
||||
if (CustomCalendar){
|
||||
|
||||
if (customCalDate.Date == DateTime.Now.Date){
|
||||
PrevButtonEnabled = false;
|
||||
NextButtonEnabled = true;
|
||||
|
|
@ -118,7 +113,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
PrevButtonEnabled = true;
|
||||
NextButtonEnabled = false;
|
||||
}
|
||||
|
||||
|
||||
week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate);
|
||||
} else{
|
||||
PrevButtonEnabled = true;
|
||||
|
|
@ -140,29 +135,24 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
|
||||
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
||||
if (calendarDayCalendarEpisode.AnilistEpisode){
|
||||
_ = calendarDayCalendarEpisode.LoadImage(100,150);
|
||||
_ = calendarDayCalendarEpisode.LoadImage(100, 150);
|
||||
} else{
|
||||
_ = calendarDayCalendarEpisode.LoadImage();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else{
|
||||
foreach (var calendarDay in CalendarDays){
|
||||
var episodesCopy = new List<CalendarEpisode>(calendarDay.CalendarEpisodes);
|
||||
foreach (var calendarDayCalendarEpisode in episodesCopy){
|
||||
if (calendarDayCalendarEpisode.SeasonName != null && HideDubs && calendarDayCalendarEpisode.SeasonName.EndsWith("Dub)")){
|
||||
calendarDay.CalendarEpisodes.Remove(calendarDayCalendarEpisode);
|
||||
continue;
|
||||
}
|
||||
if (HideDubs)
|
||||
calendarDay.CalendarEpisodes.RemoveAll(e => CrSimulcastCalendarFilter.IsDubOrAltLanguageSeason(e.SeasonName));
|
||||
|
||||
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
||||
if (calendarDayCalendarEpisode.AnilistEpisode){
|
||||
_ = calendarDayCalendarEpisode.LoadImage(100,150);
|
||||
} else{
|
||||
_ = calendarDayCalendarEpisode.LoadImage();
|
||||
}
|
||||
foreach (var e in calendarDay.CalendarEpisodes){
|
||||
if (e.ImageBitmap == null){
|
||||
if (e.AnilistEpisode)
|
||||
_ = e.LoadImage(100, 150);
|
||||
else
|
||||
_ = e.LoadImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -199,7 +189,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(6);
|
||||
}
|
||||
|
||||
LoadCalendar(mondayDate,refreshDate, true);
|
||||
LoadCalendar(mondayDate, refreshDate, true);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -215,13 +205,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
} else{
|
||||
mondayDate = GetThisWeeksMondayDate();
|
||||
}
|
||||
|
||||
|
||||
var refreshDate = DateTime.Now;
|
||||
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
|
||||
}
|
||||
|
||||
LoadCalendar(mondayDate,refreshDate, false);
|
||||
LoadCalendar(mondayDate, refreshDate, false);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -237,15 +227,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
} else{
|
||||
mondayDate = GetThisWeeksMondayDate();
|
||||
}
|
||||
|
||||
|
||||
var refreshDate = DateTime.Now;
|
||||
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13);
|
||||
}
|
||||
|
||||
LoadCalendar(mondayDate,refreshDate, false);
|
||||
|
||||
|
||||
LoadCalendar(mondayDate, refreshDate, false);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -268,7 +256,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
|
||||
CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value;
|
||||
|
||||
LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true);
|
||||
LoadCalendar(GetThisWeeksMondayDate(), DateTime.Now, true);
|
||||
|
||||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
|
@ -282,15 +270,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
||||
partial void OnFilterByAirDateChanged(bool value){
|
||||
if (loading){
|
||||
return;
|
||||
}
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
|
||||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
||||
partial void OnShowUpcomingEpisodesChanged(bool value){
|
||||
if (loading){
|
||||
return;
|
||||
|
|
@ -310,7 +289,4 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -207,6 +207,18 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private string _proxyPassword;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _flareSolverrHost = "localhost";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _flareSolverrPort = "8191";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _flareSolverrUseSsl = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _useFlareSolverr = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _tempDownloadDirPath;
|
||||
|
|
@ -264,6 +276,15 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
SonarrPort = props.Port + "";
|
||||
SonarrApiKey = props.ApiKey + "";
|
||||
}
|
||||
|
||||
var propsFlareSolverr = options.FlareSolverrProperties;
|
||||
|
||||
if (propsFlareSolverr != null){
|
||||
FlareSolverrUseSsl = propsFlareSolverr.UseSsl;
|
||||
UseFlareSolverr = propsFlareSolverr.UseFlareSolverr;
|
||||
FlareSolverrHost = propsFlareSolverr.Host + "";
|
||||
FlareSolverrPort = propsFlareSolverr.Port + "";
|
||||
}
|
||||
|
||||
ProxyEnabled = options.ProxyEnabled;
|
||||
ProxySocks = options.ProxySocks;
|
||||
|
|
@ -362,9 +383,22 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
props.ApiKey = SonarrApiKey;
|
||||
|
||||
|
||||
|
||||
settings.SonarrProperties = props;
|
||||
|
||||
var propsFlareSolverr = new FlareSolverrProperties();
|
||||
|
||||
propsFlareSolverr.UseSsl = FlareSolverrUseSsl;
|
||||
propsFlareSolverr.UseFlareSolverr = UseFlareSolverr;
|
||||
propsFlareSolverr.Host = FlareSolverrHost;
|
||||
|
||||
if (int.TryParse(FlareSolverrPort, out var portNumberFlare)){
|
||||
propsFlareSolverr.Port = portNumberFlare;
|
||||
} else{
|
||||
propsFlareSolverr.Port = 8989;
|
||||
}
|
||||
|
||||
settings.FlareSolverrProperties = propsFlareSolverr;
|
||||
|
||||
settings.LogMode = LogMode;
|
||||
|
||||
|
|
|
|||
|
|
@ -96,9 +96,6 @@
|
|||
SelectedItem="{Binding CurrentCalendarDubFilter}"
|
||||
ItemsSource="{Binding CalendarDubFilter}">
|
||||
</ComboBox>
|
||||
<CheckBox IsChecked="{Binding FilterByAirDate}"
|
||||
Content="Filter by episode air date" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
|
||||
Content="Show Upcoming episodes" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
|
|
@ -109,11 +106,16 @@
|
|||
</controls:SettingsExpander>
|
||||
|
||||
|
||||
<controls:SettingsExpander Header="Calendar ">
|
||||
<controls:SettingsExpander Header="Calendar " IsVisible="{Binding !CustomCalendar}">
|
||||
<controls:SettingsExpander.Footer>
|
||||
<CheckBox IsChecked="{Binding HideDubs}"
|
||||
Content="Hide Dubs" Margin="5 0 0 0">
|
||||
</CheckBox>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding HideDubs}"
|
||||
Content="Hide Dubs" Margin="5 0 0 0">
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
|
||||
Content="Show Upcoming episodes" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</controls:SettingsExpander.Footer>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
|
|
@ -232,8 +234,7 @@
|
|||
<Button HorizontalAlignment="Center" Content="Download"
|
||||
IsEnabled="{Binding HasPassed}"
|
||||
IsVisible="{Binding HasPassed}"
|
||||
Command="{Binding AddEpisodeToQue}"
|
||||
/>
|
||||
Command="{Binding AddEpisodeToQue}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
|
|
|||
|
|
@ -120,7 +120,10 @@ public partial class MainWindow : AppWindow{
|
|||
.Subscribe(message => ShowToast(message.Message ?? string.Empty, message.Type, message.Seconds));
|
||||
}
|
||||
|
||||
public async void ShowError(string message, bool githubWikiButton = false){
|
||||
//ffmpeg - https://github.com/GyanD/codexffmpeg/releases/latest
|
||||
//mkvmerge - https://mkvtoolnix.download/downloads.html#windows
|
||||
//git wiki - https://github.com/Crunchy-DL/Crunchy-Downloader/wiki
|
||||
public async void ShowError(string message, string urlButtonText = "", string url = ""){
|
||||
if (activeErrors.Contains(message))
|
||||
return;
|
||||
|
||||
|
|
@ -132,14 +135,14 @@ public partial class MainWindow : AppWindow{
|
|||
CloseButtonText = "Close"
|
||||
};
|
||||
|
||||
if (githubWikiButton){
|
||||
dialog.PrimaryButtonText = "Github Wiki";
|
||||
if (!string.IsNullOrEmpty(urlButtonText)){
|
||||
dialog.PrimaryButtonText = urlButtonText;
|
||||
}
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary){
|
||||
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
|
||||
Helpers.OpenUrl(url);
|
||||
}
|
||||
|
||||
activeErrors.Remove(message);
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
<CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Allow early start of next download" Description="When enabled, the next download starts as soon as the previous file has finished downloading, even if it is still being finalized (muxed/moved).">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding DownloadAllowEarlyStart}"> </CheckBox>
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
HorizontalAlignment="Stretch" />
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding DownloadAllowEarlyStart}" Content="Parallel Processing Jobs" Description="The maximum number of completed downloads that can be processed simultaneously (encoding, muxing, moving)">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<controls:NumberBox Minimum="0" Maximum="10"
|
||||
|
|
@ -385,6 +385,40 @@
|
|||
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<controls:SettingsExpander Header="Flare Solverr Settings"
|
||||
IconSource="Wifi2"
|
||||
Description="FlareSolverr settings (used only for the Calendar)"
|
||||
IsExpanded="False">
|
||||
|
||||
<controls:SettingsExpanderItem Content="Use Flare Solverr">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding UseFlareSolverr}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Host">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||
Text="{Binding FlareSolverrHost}" />
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Port">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||
Text="{Binding FlareSolverrPort}" />
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Use SSL">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding FlareSolverrUseSsl}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<controls:SettingsExpander Header="App Appearance"
|
||||
IconSource="DarkTheme"
|
||||
Description="Customize the look and feel of the application"
|
||||
|
|
@ -617,7 +651,7 @@
|
|||
DockPanel.Dock="Left" />
|
||||
|
||||
<ColorPicker Color="{Binding CustomAccentColor}"
|
||||
DockPanel.Dock="Right" />
|
||||
DockPanel.Dock="Right" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
|
|
|
|||
Loading…
Reference in a new issue