mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 20:10:26 +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.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,7 +80,13 @@ 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>") ||
|
||||||
|
|
@ -88,6 +99,7 @@ public class CalendarManager{
|
||||||
} 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 &&
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -838,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,
|
||||||
|
|
@ -849,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,
|
||||||
|
|
@ -883,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,
|
||||||
|
|
@ -1361,7 +1364,7 @@ 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\/))");
|
||||||
|
|
@ -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){
|
if (streamServers.Count == 0){
|
||||||
return new DownloadResponse{
|
return new DownloadResponse{
|
||||||
|
|
@ -1393,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,
|
||||||
|
|
@ -1411,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,
|
||||||
|
|
@ -1532,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);
|
||||||
|
|
@ -1591,8 +1597,10 @@ public class CrunchyrollManager{
|
||||||
await CrAuthEndpoint1.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
await CrAuthEndpoint2.RefreshToken(true);
|
await CrAuthEndpoint2.RefreshToken(true);
|
||||||
|
|
||||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
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 } };
|
{ "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);
|
||||||
|
|
||||||
|
|
@ -1626,8 +1634,10 @@ public class CrunchyrollManager{
|
||||||
await CrAuthEndpoint2.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 " + ((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 } };
|
{ "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);
|
||||||
|
|
||||||
|
|
@ -1685,8 +1695,10 @@ public class CrunchyrollManager{
|
||||||
await CrAuthEndpoint1.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
await CrAuthEndpoint2.RefreshToken(true);
|
await CrAuthEndpoint2.RefreshToken(true);
|
||||||
|
|
||||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
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 } };
|
{ "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;
|
||||||
|
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -166,9 +166,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointAuthorization = "";
|
private string _endpointAuthorization = "";
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _endpointClientId = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointUserAgent = "";
|
private string _endpointUserAgent = "";
|
||||||
|
|
||||||
|
|
@ -367,7 +364,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
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;
|
||||||
|
|
@ -383,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];
|
||||||
|
|
||||||
|
|
@ -554,7 +557,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
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;
|
||||||
|
|
@ -730,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;
|
||||||
|
|
@ -786,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){
|
||||||
|
|
|
||||||
|
|
@ -304,12 +304,6 @@
|
||||||
Text="{Binding EndpointAuthorization}" />
|
Text="{Binding EndpointAuthorization}" />
|
||||||
</StackPanel>
|
</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">
|
<StackPanel Margin="0,5">
|
||||||
<TextBlock Text="User Agent" />
|
<TextBlock Text="User Agent" />
|
||||||
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||||
|
|
|
||||||
|
|
@ -847,36 +847,41 @@ public class Helpers{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void MergePlaylistData(
|
public static void MergePlaylistData(
|
||||||
Dictionary<string, ServerData> target,
|
ServerData target,
|
||||||
Dictionary<string, ServerData> source,
|
Dictionary<string, ServerData> source,
|
||||||
bool mergeAudio,
|
bool mergeAudio,
|
||||||
bool mergeVideo){
|
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){
|
foreach (var kvp in source){
|
||||||
var key = kvp.Key;
|
var key = kvp.Key;
|
||||||
var src = kvp.Value;
|
var src = kvp.Value;
|
||||||
|
|
||||||
if (target.TryGetValue(key, out var existing)){
|
if (!src.servers.Contains(key))
|
||||||
if (mergeAudio){
|
src.servers.Add(key);
|
||||||
existing.audio ??= [];
|
|
||||||
if (src.audio != null)
|
|
||||||
existing.audio.AddRange(src.audio);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mergeVideo){
|
AddServer(key);
|
||||||
existing.video ??= [];
|
foreach (var s in src.servers)
|
||||||
if (src.video != null)
|
AddServer(s);
|
||||||
existing.video.AddRange(src.video);
|
|
||||||
}
|
if (mergeAudio && src.audio != null){
|
||||||
} else{
|
target.audio ??= [];
|
||||||
target[key] = new ServerData{
|
target.audio.AddRange(src.audio);
|
||||||
audio = (mergeAudio && src.audio != null)
|
}
|
||||||
? new List<AudioPlaylist>(src.audio)
|
|
||||||
: new List<AudioPlaylist>(),
|
if (mergeVideo && src.video != null){
|
||||||
video = (mergeVideo && src.video != null)
|
target.video ??= [];
|
||||||
? new List<VideoPlaylist>(src.video)
|
target.video.AddRange(src.video);
|
||||||
: new List<VideoPlaylist>()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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;
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,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
|
||||||
|
|
||||||
#region Crunchyroll Settings
|
#region Crunchyroll Settings
|
||||||
|
|
@ -301,9 +305,6 @@ 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; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ 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; }
|
||||||
|
|
@ -66,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{
|
||||||
|
|
|
||||||
|
|
@ -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(){
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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]
|
||||||
|
|
@ -221,7 +211,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
|
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadCalendar(mondayDate,refreshDate, false);
|
LoadCalendar(mondayDate, refreshDate, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|
@ -243,9 +233,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -208,6 +208,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;
|
||||||
|
|
||||||
|
|
@ -265,6 +277,15 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
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;
|
||||||
ProxyHost = options.ProxyHost ?? "";
|
ProxyHost = options.ProxyHost ?? "";
|
||||||
|
|
@ -363,9 +384,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;
|
||||||
|
|
||||||
CfgManager.WriteCrSettings();
|
CfgManager.WriteCrSettings();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue