mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 20:10:26 +00:00
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7687c80e8 | ||
|
|
c5660a87e7 |
24 changed files with 872 additions and 323 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,19 +80,26 @@ public class CalendarManager{
|
||||||
request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8");
|
request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8");
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip, deflate, br");
|
request.Headers.AcceptEncoding.ParseAdd("gzip, deflate, br");
|
||||||
|
|
||||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
(bool IsOk, string ResponseContent, string error) response;
|
||||||
|
if (!HttpClientReq.Instance.useFlareSolverr){
|
||||||
|
response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
|
} else{
|
||||||
|
response = await HttpClientReq.Instance.SendFlareSolverrHttpRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!response.IsOk){
|
if (!response.IsOk){
|
||||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||||
MessageBus.Current.SendMessage(new ToastMessage("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5));
|
MessageBus.Current.SendMessage(new ToastMessage("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5));
|
||||||
Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar.");
|
Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar.");
|
||||||
} else{
|
} else{
|
||||||
Console.Error.WriteLine($"Calendar request failed");
|
Console.Error.WriteLine($"Calendar request failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CalendarWeek();
|
return new CalendarWeek();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,6 +176,24 @@ public class CalendarManager{
|
||||||
Console.Error.WriteLine("No days found in the HTML document.");
|
Console.Error.WriteLine("No days found in the HTML document.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||||
|
foreach (var calendarDay in week.CalendarDays){
|
||||||
|
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
|
||||||
|
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
|
||||||
|
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
||||||
|
|
||||||
|
foreach (var calendarEpisode in list
|
||||||
|
.Where(e => calendarDay.DateTime.Date.Day == e.DateTime.Date.Day)
|
||||||
|
.Where(e => calendarDay.CalendarEpisodes.All(ele =>
|
||||||
|
ele.CrSeriesID != e.CrSeriesID &&
|
||||||
|
!CrSimulcastCalendarFilter.IsMatch(ele.SeasonName, e.SeasonName, similarityThreshold: 0.5)))){
|
||||||
|
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
calendar[weeksMondayDate] = week;
|
calendar[weeksMondayDate] = week;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -172,14 +202,14 @@ public class CalendarManager{
|
||||||
|
|
||||||
|
|
||||||
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
|
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
|
||||||
await LoadAnilistUpcoming();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
|
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
|
||||||
return forDate;
|
return forDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||||
|
await LoadAnilistUpcoming();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
CalendarWeek week = new CalendarWeek();
|
CalendarWeek week = new CalendarWeek();
|
||||||
week.CalendarDays = new List<CalendarDay>();
|
week.CalendarDays = new List<CalendarDay>();
|
||||||
|
|
@ -201,7 +231,7 @@ public class CalendarManager{
|
||||||
var firstDayOfWeek = week.CalendarDays.First().DateTime;
|
var firstDayOfWeek = week.CalendarDays.First().DateTime;
|
||||||
week.FirstDayOfWeek = firstDayOfWeek;
|
week.FirstDayOfWeek = firstDayOfWeek;
|
||||||
|
|
||||||
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes("", 200, firstDayOfWeek, true);
|
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true);
|
||||||
|
|
||||||
if (newEpisodesBase is{ Data.Count: > 0 }){
|
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||||
var newEpisodes = newEpisodesBase.Data;
|
var newEpisodes = newEpisodesBase.Data;
|
||||||
|
|
@ -222,36 +252,22 @@ public class CalendarManager{
|
||||||
|
|
||||||
DateTime targetDate;
|
DateTime targetDate;
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate){
|
|
||||||
targetDate = episodeAirDate;
|
|
||||||
|
|
||||||
if (targetDate >= oneYearFromNow){
|
targetDate = premiumAvailableStart;
|
||||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
|
||||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
|
||||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
|
||||||
|
|
||||||
if (freeAvailableStart <= oneYearFromNow){
|
if (targetDate >= oneYearFromNow){
|
||||||
targetDate = freeAvailableStart;
|
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||||
} else{
|
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||||
targetDate = premiumAvailableStart;
|
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
||||||
}
|
|
||||||
}
|
|
||||||
} else{
|
|
||||||
targetDate = premiumAvailableStart;
|
|
||||||
|
|
||||||
if (targetDate >= oneYearFromNow){
|
if (freeAvailableStart <= oneYearFromNow){
|
||||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
targetDate = freeAvailableStart;
|
||||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
} else{
|
||||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
targetDate = episodeAirDate;
|
||||||
|
|
||||||
if (freeAvailableStart <= oneYearFromNow){
|
|
||||||
targetDate = freeAvailableStart;
|
|
||||||
} else{
|
|
||||||
targetDate = episodeAirDate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter;
|
var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter;
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
|
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
|
||||||
|
|
@ -280,7 +296,7 @@ public class CalendarManager{
|
||||||
: Regex.IsMatch(crBrowseEpisode.EpisodeMetadata.SeasonTitle, @"^Season\s+\d+$", RegexOptions.IgnoreCase)
|
: Regex.IsMatch(crBrowseEpisode.EpisodeMetadata.SeasonTitle, @"^Season\s+\d+$", RegexOptions.IgnoreCase)
|
||||||
? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}"
|
? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}"
|
||||||
: crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
: crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
||||||
|
|
||||||
calEpisode.DateTime = targetDate;
|
calEpisode.DateTime = targetDate;
|
||||||
calEpisode.HasPassed = DateTime.Now > targetDate;
|
calEpisode.HasPassed = DateTime.Now > targetDate;
|
||||||
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
||||||
|
|
@ -340,7 +356,8 @@ public class CalendarManager{
|
||||||
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
||||||
|
|
||||||
foreach (var calendarEpisode in list.Where(calendarEpisodeAnilist => calendarDay.DateTime.Date.Day == calendarEpisodeAnilist.DateTime.Date.Day)
|
foreach (var calendarEpisode in list.Where(calendarEpisodeAnilist => calendarDay.DateTime.Date.Day == calendarEpisodeAnilist.DateTime.Date.Day)
|
||||||
.Where(calendarEpisodeAnilist => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisodeAnilist.CrSeriesID && ele.SeasonName != calendarEpisodeAnilist.SeasonName))){
|
.Where(calendarEpisodeAnilist =>
|
||||||
|
calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisodeAnilist.CrSeriesID && ele.SeasonName != calendarEpisodeAnilist.SeasonName))){
|
||||||
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -427,7 +444,7 @@ public class CalendarManager{
|
||||||
aniListResponse ??= currentResponse;
|
aniListResponse ??= currentResponse;
|
||||||
|
|
||||||
if (aniListResponse != currentResponse){
|
if (aniListResponse != currentResponse){
|
||||||
aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ??[]);
|
aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false;
|
hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false;
|
||||||
|
|
@ -436,12 +453,12 @@ public class CalendarManager{
|
||||||
} while (hasNextPage && currentPage < 20);
|
} while (hasNextPage && currentPage < 20);
|
||||||
|
|
||||||
|
|
||||||
var list = aniListResponse.Data?.Page?.AiringSchedules ??[];
|
var list = aniListResponse.Data?.Page?.AiringSchedules ?? [];
|
||||||
|
|
||||||
list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external =>
|
list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external =>
|
||||||
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
|
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
|
||||||
|
|
||||||
List<CalendarEpisode> calendarEpisodes =[];
|
List<CalendarEpisode> calendarEpisodes = [];
|
||||||
|
|
||||||
foreach (var anilistEle in list){
|
foreach (var anilistEle in list){
|
||||||
var calEp = new CalendarEpisode();
|
var calEp = new CalendarEpisode();
|
||||||
|
|
@ -541,7 +558,7 @@ public class CalendarManager{
|
||||||
oldestRelease.Second,
|
oldestRelease.Second,
|
||||||
calEp.DateTime.Kind
|
calEp.DateTime.Kind
|
||||||
);
|
);
|
||||||
|
|
||||||
if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){
|
if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){
|
||||||
adjustedDate = oldestRelease.AddDays(7);
|
adjustedDate = oldestRelease.AddDays(7);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ public class CrunchyrollManager{
|
||||||
public ObservableCollection<HistorySeries> HistoryList = new();
|
public ObservableCollection<HistorySeries> HistoryList = new();
|
||||||
|
|
||||||
public HistorySeries SelectedSeries = new HistorySeries{
|
public HistorySeries SelectedSeries = new HistorySeries{
|
||||||
Seasons =[]
|
Seasons = []
|
||||||
};
|
};
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -107,8 +107,8 @@ public class CrunchyrollManager{
|
||||||
options.Partsize = 10;
|
options.Partsize = 10;
|
||||||
options.DlSubs = new List<string>{ "en-US" };
|
options.DlSubs = new List<string>{ "en-US" };
|
||||||
options.SkipMuxing = false;
|
options.SkipMuxing = false;
|
||||||
options.MkvmergeOptions =[];
|
options.MkvmergeOptions = [];
|
||||||
options.FfmpegOptions =[];
|
options.FfmpegOptions = [];
|
||||||
options.DefaultAudio = "ja-JP";
|
options.DefaultAudio = "ja-JP";
|
||||||
options.DefaultSub = "en-US";
|
options.DefaultSub = "en-US";
|
||||||
options.QualityAudio = "best";
|
options.QualityAudio = "best";
|
||||||
|
|
@ -128,7 +128,7 @@ public class CrunchyrollManager{
|
||||||
options.CalendarDubFilter = "none";
|
options.CalendarDubFilter = "none";
|
||||||
options.CustomCalendar = true;
|
options.CustomCalendar = true;
|
||||||
options.DlVideoOnce = true;
|
options.DlVideoOnce = true;
|
||||||
options.StreamEndpoint = "web/firefox";
|
options.StreamEndpoint = new CrAuthSettings(){ Endpoint = "tv/android_tv", Audio = true, Video = true };
|
||||||
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
|
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
|
||||||
options.HistoryLang = DefaultLocale;
|
options.HistoryLang = DefaultLocale;
|
||||||
options.FixCccSubtitles = true;
|
options.FixCccSubtitles = true;
|
||||||
|
|
@ -201,13 +201,17 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
DefaultAndroidAuthSettings = new CrAuthSettings(){
|
DefaultAndroidAuthSettings = new CrAuthSettings(){
|
||||||
Endpoint = "android/phone",
|
Endpoint = "android/phone",
|
||||||
Authorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=",
|
Client_ID = "pd6uw3dfyhzghs0wxae3",
|
||||||
UserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0",
|
Authorization = "Basic cGQ2dXczZGZ5aHpnaHMwd3hhZTM6NXJ5SjJFQXR3TFc0UklIOEozaWk1anVqbnZrRWRfTkY=",
|
||||||
|
UserAgent = "Crunchyroll/3.95.2 Android/16 okhttp/4.12.0",
|
||||||
Device_name = "CPH2449",
|
Device_name = "CPH2449",
|
||||||
Device_type = "OnePlus CPH2449"
|
Device_type = "OnePlus CPH2449",
|
||||||
|
Audio = true,
|
||||||
|
Video = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
CrunOptions.StreamEndpoint = "tv/android_tv";
|
CrunOptions.StreamEndpoint ??= new CrAuthSettings(){ Endpoint = "tv/android_tv", Audio = true, Video = true };
|
||||||
|
CrunOptions.StreamEndpoint.Endpoint = "tv/android_tv";
|
||||||
CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
|
CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
|
||||||
Endpoint = "tv/android_tv",
|
Endpoint = "tv/android_tv",
|
||||||
Authorization = "Basic ZGsxYndzemRyc3lkeTR1N2xvenE6bDl0SU1BdTlzTGc4ZjA4ajlfQkQ4eWZmQmZTSms0R0o=",
|
Authorization = "Basic ZGsxYndzemRyc3lkeTR1N2xvenE6bDl0SU1BdTlzTGc4ZjA4ajlfQkQ4eWZmQmZTSms0R0o=",
|
||||||
|
|
@ -236,7 +240,7 @@ public class CrunchyrollManager{
|
||||||
// ApiUrls.authBasicMob = "Basic " + token;
|
// ApiUrls.authBasicMob = "Basic " + token;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") :[];
|
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") : [];
|
||||||
|
|
||||||
foreach (var file in jsonFiles){
|
foreach (var file in jsonFiles){
|
||||||
try{
|
try{
|
||||||
|
|
@ -276,13 +280,13 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else{
|
} else{
|
||||||
HistoryList =[];
|
HistoryList = [];
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
HistoryList =[];
|
HistoryList = [];
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
HistoryList =[];
|
HistoryList = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -303,7 +307,14 @@ public class CrunchyrollManager{
|
||||||
Doing = "Starting"
|
Doing = "Starting"
|
||||||
};
|
};
|
||||||
QueueManager.Instance.Queue.Refresh();
|
QueueManager.Instance.Queue.Refresh();
|
||||||
var res = await DownloadMediaList(data, options);
|
var res = new DownloadResponse();
|
||||||
|
try{
|
||||||
|
res = await DownloadMediaList(data, options);
|
||||||
|
} catch (Exception e){
|
||||||
|
Console.WriteLine(e);
|
||||||
|
res.Error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (res.Error){
|
if (res.Error){
|
||||||
QueueManager.Instance.DecrementDownloads();
|
QueueManager.Instance.DecrementDownloads();
|
||||||
|
|
@ -356,7 +367,7 @@ public class CrunchyrollManager{
|
||||||
var fileNameAndPath = options.DownloadToTempFolder
|
var fileNameAndPath = options.DownloadToTempFolder
|
||||||
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
||||||
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
||||||
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
|
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true } && (!options.Noaudio || !options.Novids)){
|
||||||
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
|
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
|
||||||
var mergers = new List<Merger>();
|
var mergers = new List<Merger>();
|
||||||
foreach (var keyValue in groupByDub){
|
foreach (var keyValue in groupByDub){
|
||||||
|
|
@ -421,7 +432,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
|
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.DownloadToTempFolder){
|
if (options.DownloadToTempFolder){
|
||||||
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.options.Subtitles);
|
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.options.Subtitles);
|
||||||
}
|
}
|
||||||
|
|
@ -481,7 +492,22 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.DownloadToTempFolder){
|
if (options.DownloadToTempFolder){
|
||||||
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, result.merger?.options.Subtitles ?? []);
|
var tempFolder = res.TempFolderPath ?? CfgManager.PathTEMP_DIR;
|
||||||
|
|
||||||
|
List<SubtitleInput> subtitles =
|
||||||
|
result.merger?.options.Subtitles
|
||||||
|
?? res.Data
|
||||||
|
.Where(d => d.Type == DownloadMediaType.Subtitle)
|
||||||
|
.Select(d => new SubtitleInput{
|
||||||
|
File = d.Path ?? string.Empty,
|
||||||
|
Language = d.Language,
|
||||||
|
ClosedCaption = d.Cc ?? false,
|
||||||
|
Signs = d.Signs ?? false,
|
||||||
|
RelatedVideoDownloadMedia = d.RelatedVideoDownloadMedia
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await MoveFromTempFolder(result.merger, data, options, tempFolder, subtitles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,7 +689,7 @@ public class CrunchyrollManager{
|
||||||
foreach (var downloadedMedia in subs){
|
foreach (var downloadedMedia in subs){
|
||||||
var subt = new SubtitleFonts();
|
var subt = new SubtitleFonts();
|
||||||
subt.Language = downloadedMedia.Language;
|
subt.Language = downloadedMedia.Language;
|
||||||
subt.Fonts = downloadedMedia.Fonts ??[];
|
subt.Fonts = downloadedMedia.Fonts ?? [];
|
||||||
subsList.Add(subt);
|
subsList.Add(subt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -701,7 +727,7 @@ public class CrunchyrollManager{
|
||||||
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
|
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
|
||||||
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
|
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
|
||||||
KeepAllVideos = options.KeepAllVideos,
|
KeepAllVideos = options.KeepAllVideos,
|
||||||
Fonts = options.MuxFonts ? FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList) :[],
|
Fonts = options.MuxFonts ? FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList) : [],
|
||||||
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
|
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
|
||||||
VideoTitle = options.VideoTitle,
|
VideoTitle = options.VideoTitle,
|
||||||
Options = new MuxOptions(){
|
Options = new MuxOptions(){
|
||||||
|
|
@ -718,8 +744,8 @@ public class CrunchyrollManager{
|
||||||
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
|
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
|
||||||
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
||||||
SignsSubsAsForced = options.SignsSubsAsForced,
|
SignsSubsAsForced = options.SignsSubsAsForced,
|
||||||
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[],
|
Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() : [],
|
||||||
Cover = options.MuxCover ? data.Where(a => a.Type == DownloadMediaType.Cover).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[],
|
Cover = options.MuxCover ? data.Where(a => a.Type == DownloadMediaType.Cover).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!File.Exists(CfgManager.PathFFMPEG)){
|
if (!File.Exists(CfgManager.PathFFMPEG)){
|
||||||
|
|
@ -731,7 +757,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isMuxed, syncError = false;
|
bool isMuxed, syncError = false;
|
||||||
List<string> notSyncedDubs =[];
|
List<string> notSyncedDubs = [];
|
||||||
|
|
||||||
|
|
||||||
if (options is{ SyncTiming: true, DlVideoOnce: true } && merger.options.OnlyVid.Count > 0 && merger.options.OnlyAudio.Count > 0){
|
if (options is{ SyncTiming: true, DlVideoOnce: true } && merger.options.OnlyVid.Count > 0 && merger.options.OnlyAudio.Count > 0){
|
||||||
|
|
@ -812,7 +838,8 @@ public class CrunchyrollManager{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
|
||||||
if (!File.Exists(CfgManager.PathFFMPEG)){
|
if (!File.Exists(CfgManager.PathFFMPEG)){
|
||||||
Console.Error.WriteLine("Missing ffmpeg");
|
Console.Error.WriteLine("Missing ffmpeg");
|
||||||
MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}");
|
MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}", "FFmpeg",
|
||||||
|
"https://github.com/GyanD/codexffmpeg/releases/latest");
|
||||||
return new DownloadResponse{
|
return new DownloadResponse{
|
||||||
Data = new List<DownloadedMedia>(),
|
Data = new List<DownloadedMedia>(),
|
||||||
Error = true,
|
Error = true,
|
||||||
|
|
@ -823,7 +850,8 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
if (!File.Exists(CfgManager.PathMKVMERGE)){
|
if (!File.Exists(CfgManager.PathMKVMERGE)){
|
||||||
Console.Error.WriteLine("Missing Mkvmerge");
|
Console.Error.WriteLine("Missing Mkvmerge");
|
||||||
MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}");
|
MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}", "Mkvmerge",
|
||||||
|
"https://mkvtoolnix.download/downloads.html#windows");
|
||||||
return new DownloadResponse{
|
return new DownloadResponse{
|
||||||
Data = new List<DownloadedMedia>(),
|
Data = new List<DownloadedMedia>(),
|
||||||
Error = true,
|
Error = true,
|
||||||
|
|
@ -857,7 +885,8 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
if (!_widevine.canDecrypt){
|
if (!_widevine.canDecrypt){
|
||||||
Console.Error.WriteLine("CDM files missing");
|
Console.Error.WriteLine("CDM files missing");
|
||||||
MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", true);
|
MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", "GitHub Wiki",
|
||||||
|
"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
|
||||||
return new DownloadResponse{
|
return new DownloadResponse{
|
||||||
Data = new List<DownloadedMedia>(),
|
Data = new List<DownloadedMedia>(),
|
||||||
Error = true,
|
Error = true,
|
||||||
|
|
@ -904,11 +933,10 @@ public class CrunchyrollManager{
|
||||||
options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
|
options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
|
||||||
|
|
||||||
if (options.DownloadDescriptionAudio){
|
if (options.DownloadDescriptionAudio){
|
||||||
|
|
||||||
var alreadyAdr = new HashSet<string>(
|
var alreadyAdr = new HashSet<string>(
|
||||||
data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err")
|
data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err")
|
||||||
);
|
);
|
||||||
|
|
||||||
bool HasDescriptionRole(IEnumerable<string>? roles) =>
|
bool HasDescriptionRole(IEnumerable<string>? roles) =>
|
||||||
roles?.Any(r => string.Equals(r, "description", StringComparison.OrdinalIgnoreCase)) == true;
|
roles?.Any(r => string.Equals(r, "description", StringComparison.OrdinalIgnoreCase)) == true;
|
||||||
|
|
||||||
|
|
@ -918,7 +946,7 @@ public class CrunchyrollManager{
|
||||||
.Where(m => m.Versions?.Any(v => (v.AudioLocale == (m.Lang?.CrLocale ?? "err"))
|
.Where(m => m.Versions?.Any(v => (v.AudioLocale == (m.Lang?.CrLocale ?? "err"))
|
||||||
&& HasDescriptionRole(v.roles)) == true)
|
&& HasDescriptionRole(v.roles)) == true)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var additions = toDuplicate.Select(m => new CrunchyEpMetaData{
|
var additions = toDuplicate.Select(m => new CrunchyEpMetaData{
|
||||||
MediaId = m.MediaId,
|
MediaId = m.MediaId,
|
||||||
Lang = m.Lang,
|
Lang = m.Lang,
|
||||||
|
|
@ -1024,13 +1052,18 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
|
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData = default;
|
||||||
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
|
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
|
||||||
if (CrAuthEndpoint2.Profile.Username != "???"){
|
|
||||||
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
|
if (CrAuthEndpoint1.Profile.Username != "???" && options.StreamEndpoint != null && (options.StreamEndpoint.Video || options.StreamEndpoint.Audio)){
|
||||||
|
fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription, options.StreamEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchPlaybackData.IsOk){
|
if (CrAuthEndpoint2.Profile.Username != "???" && options.StreamEndpointSecondSettings != null && (options.StreamEndpointSecondSettings.Video || options.StreamEndpointSecondSettings.Audio)){
|
||||||
|
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription, options.StreamEndpointSecondSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fetchPlaybackData.IsOk && !fetchPlaybackData2.IsOk){
|
||||||
var errorJson = fetchPlaybackData.error;
|
var errorJson = fetchPlaybackData.error;
|
||||||
if (!string.IsNullOrEmpty(errorJson)){
|
if (!string.IsNullOrEmpty(errorJson)){
|
||||||
var error = StreamError.FromJson(errorJson);
|
var error = StreamError.FromJson(errorJson);
|
||||||
|
|
@ -1077,7 +1110,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fetchPlaybackData2.IsOk){
|
if (fetchPlaybackData2.IsOk){
|
||||||
if (fetchPlaybackData.pbData.Data != null && fetchPlaybackData2.pbData?.Data != null)
|
if (fetchPlaybackData.pbData?.Data != null && fetchPlaybackData2.pbData?.Data != null){
|
||||||
foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){
|
foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){
|
||||||
var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data;
|
var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data;
|
||||||
if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){
|
if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){
|
||||||
|
|
@ -1101,13 +1134,16 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else{
|
||||||
|
fetchPlaybackData = fetchPlaybackData2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var pbData = fetchPlaybackData.pbData;
|
var pbData = fetchPlaybackData.pbData;
|
||||||
|
|
||||||
List<string> hsLangs = new List<string>();
|
List<string> hsLangs = new List<string>();
|
||||||
var pbStreams = pbData.Data;
|
var pbStreams = pbData?.Data;
|
||||||
var streams = new List<StreamDetailsPop>();
|
var streams = new List<StreamDetailsPop>();
|
||||||
|
|
||||||
variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true));
|
variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true));
|
||||||
|
|
@ -1116,12 +1152,12 @@ public class CrunchyrollManager{
|
||||||
variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true));
|
variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true));
|
||||||
variables.Add(new Variable("seasonTitle", data.SeasonTitle ?? string.Empty, true));
|
variables.Add(new Variable("seasonTitle", data.SeasonTitle ?? string.Empty, true));
|
||||||
variables.Add(new Variable("season", !string.IsNullOrEmpty(data.Season) ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false));
|
variables.Add(new Variable("season", !string.IsNullOrEmpty(data.Season) ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false));
|
||||||
variables.Add(new Variable("dubs", string.Join(", ", data.SelectedDubs ??[]), true));
|
variables.Add(new Variable("dubs", string.Join(", ", data.SelectedDubs ?? []), true));
|
||||||
|
|
||||||
|
|
||||||
if (pbStreams?.Keys != null){
|
if (pbStreams?.Keys != null){
|
||||||
var pb = pbStreams.Select(v => {
|
var pb = pbStreams.Select(v => {
|
||||||
if (v.Key != "none" && v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
|
if (v.Key != "none" && v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
|
||||||
hsLangs.Add(v.Value.HardsubLang.CrLocale);
|
hsLangs.Add(v.Value.HardsubLang.CrLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1216,14 +1252,22 @@ public class CrunchyrollManager{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
dlFailed = true;
|
if (options.HsRawFallback){
|
||||||
|
streams = streams.Where((s) => !s.IsHardsubbed).ToList();
|
||||||
|
if (streams.Count < 1){
|
||||||
|
Console.Error.WriteLine("Raw streams not available!");
|
||||||
|
dlFailed = true;
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
dlFailed = true;
|
||||||
|
|
||||||
return new DownloadResponse{
|
return new DownloadResponse{
|
||||||
Data = new List<DownloadedMedia>(),
|
Data = new List<DownloadedMedia>(),
|
||||||
Error = dlFailed,
|
Error = dlFailed,
|
||||||
FileName = "./unknown",
|
FileName = "./unknown",
|
||||||
ErrorText = "No Hardsubs available"
|
ErrorText = "No Hardsubs available"
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1263,7 +1307,7 @@ public class CrunchyrollManager{
|
||||||
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
|
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
|
||||||
|
|
||||||
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
|
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
|
||||||
Dictionary<string, string> streamPlaylistsReqResponseList =[];
|
Dictionary<string, StreamInfo> streamPlaylistsReqResponseList = [];
|
||||||
|
|
||||||
foreach (var streamUrl in curStream.Url){
|
foreach (var streamUrl in curStream.Url){
|
||||||
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl.Url ?? string.Empty, HttpMethod.Get, true, streamUrl.CrAuth?.Token?.access_token);
|
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl.Url ?? string.Empty, HttpMethod.Get, true, streamUrl.CrAuth?.Token?.access_token);
|
||||||
|
|
@ -1280,7 +1324,11 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
|
||||||
streamPlaylistsReqResponseList[streamUrl.Url ?? ""] = streamPlaylistsReqResponse.ResponseContent;
|
streamPlaylistsReqResponseList[streamUrl.Url ?? ""] = new StreamInfo(){
|
||||||
|
Playlist = streamPlaylistsReqResponse.ResponseContent,
|
||||||
|
Audio = streamUrl.Audio,
|
||||||
|
Video = streamUrl.Video
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1315,8 +1363,8 @@ public class CrunchyrollManager{
|
||||||
//
|
//
|
||||||
// List<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
|
// List<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
|
||||||
if (streamPlaylistsReqResponseList.Count > 0){
|
if (streamPlaylistsReqResponseList.Count > 0){
|
||||||
HashSet<string> streamServers =[];
|
HashSet<string> streamServers = [];
|
||||||
Dictionary<string, ServerData> playListData = new Dictionary<string, ServerData>();
|
ServerData playListData = new ServerData();
|
||||||
|
|
||||||
foreach (var curStreams in streamPlaylistsReqResponseList){
|
foreach (var curStreams in streamPlaylistsReqResponseList){
|
||||||
var match = Regex.Match(curStreams.Key ?? string.Empty, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))");
|
var match = Regex.Match(curStreams.Key ?? string.Empty, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))");
|
||||||
|
|
@ -1328,15 +1376,16 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
try{
|
||||||
MPDParsed streamPlaylists = MPDParser.Parse(curStreams.Value, Languages.FindLang(crLocal), matchedUrl);
|
var entry = curStreams.Value;
|
||||||
|
MPDParsed streamPlaylists = MPDParser.Parse(entry.Playlist, Languages.FindLang(crLocal), matchedUrl);
|
||||||
streamServers.UnionWith(streamPlaylists.Data.Keys);
|
streamServers.UnionWith(streamPlaylists.Data.Keys);
|
||||||
Helpers.MergePlaylistData(playListData, streamPlaylists.Data);
|
Helpers.MergePlaylistData(playListData, streamPlaylists.Data, entry.Audio, entry.Video);
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
Console.Error.WriteLine(e);
|
Console.Error.WriteLine(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
|
// options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
|
||||||
|
|
||||||
if (streamServers.Count == 0){
|
if (streamServers.Count == 0){
|
||||||
return new DownloadResponse{
|
return new DownloadResponse{
|
||||||
|
|
@ -1347,17 +1396,20 @@ public class CrunchyrollManager{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.StreamServer == 0){
|
playListData.video ??= [];
|
||||||
options.StreamServer = 1;
|
playListData.audio ??= [];
|
||||||
}
|
|
||||||
|
// if (options.StreamServer == 0){
|
||||||
|
// options.StreamServer = 1;
|
||||||
|
// }
|
||||||
|
|
||||||
// string selectedServer = streamServers[options.StreamServer - 1];
|
// string selectedServer = streamServers[options.StreamServer - 1];
|
||||||
// ServerData selectedList = streamPlaylists.Data[selectedServer];
|
// ServerData selectedList = streamPlaylists.Data[selectedServer];
|
||||||
|
|
||||||
string selectedServer = streamServers.ToList()[options.StreamServer - 1];
|
// string selectedServer = streamServers.ToList()[options.StreamServer - 1];
|
||||||
ServerData selectedList = playListData[selectedServer];
|
// ServerData selectedList = playListData[selectedServer];
|
||||||
|
|
||||||
var videos = selectedList.video.Select(item => new VideoItem{
|
var videos = playListData.video.Select(item => new VideoItem{
|
||||||
segments = item.segments,
|
segments = item.segments,
|
||||||
pssh = item.pssh,
|
pssh = item.pssh,
|
||||||
quality = item.quality,
|
quality = item.quality,
|
||||||
|
|
@ -1365,7 +1417,7 @@ public class CrunchyrollManager{
|
||||||
resolutionText = $"{item.quality.width}x{item.quality.height} ({Math.Round(item.bandwidth / 1024.0)}KiB/s)"
|
resolutionText = $"{item.quality.width}x{item.quality.height} ({Math.Round(item.bandwidth / 1024.0)}KiB/s)"
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var audios = selectedList.audio.Select(item => new AudioItem{
|
var audios = playListData.audio.Select(item => new AudioItem{
|
||||||
@default = item.@default,
|
@default = item.@default,
|
||||||
segments = item.segments,
|
segments = item.segments,
|
||||||
pssh = item.pssh,
|
pssh = item.pssh,
|
||||||
|
|
@ -1486,7 +1538,7 @@ public class CrunchyrollManager{
|
||||||
sb.AppendLine($"Selected quality:");
|
sb.AppendLine($"Selected quality:");
|
||||||
sb.AppendLine($"\tVideo: {chosenVideoSegments.resolutionText}");
|
sb.AppendLine($"\tVideo: {chosenVideoSegments.resolutionText}");
|
||||||
sb.AppendLine($"\tAudio: {chosenAudioSegments.resolutionText} / {chosenAudioSegments.audioSamplingRate}");
|
sb.AppendLine($"\tAudio: {chosenAudioSegments.resolutionText} / {chosenAudioSegments.audioSamplingRate}");
|
||||||
sb.AppendLine($"\tServer: {selectedServer}");
|
sb.AppendLine($"\tServer: {string.Join(", ", playListData.servers)}");
|
||||||
|
|
||||||
string qualityConsoleLog = sb.ToString();
|
string qualityConsoleLog = sb.ToString();
|
||||||
Console.WriteLine(qualityConsoleLog);
|
Console.WriteLine(qualityConsoleLog);
|
||||||
|
|
@ -1543,9 +1595,12 @@ public class CrunchyrollManager{
|
||||||
Console.WriteLine("Skipping video download...");
|
Console.WriteLine("Skipping video download...");
|
||||||
} else{
|
} else{
|
||||||
await CrAuthEndpoint1.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
await CrAuthEndpoint2.RefreshToken(true);
|
||||||
|
|
||||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
Dictionary<string, string> authDataDict = new Dictionary<string, string>{
|
||||||
{ { "authorization", "Bearer " + CrAuthEndpoint1.Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
|
{ "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },
|
||||||
|
{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty }
|
||||||
|
};
|
||||||
|
|
||||||
chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
||||||
|
|
||||||
|
|
@ -1576,10 +1631,13 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
|
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
|
||||||
await CrAuthEndpoint1.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
await CrAuthEndpoint2.RefreshToken(true);
|
||||||
|
|
||||||
if (chosenVideoSegments.encryptionKeys.Count == 0){
|
if (chosenVideoSegments.encryptionKeys.Count == 0){
|
||||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
Dictionary<string, string> authDataDict = new Dictionary<string, string>{
|
||||||
{ { "authorization", "Bearer " + CrAuthEndpoint1.Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
|
{ "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },
|
||||||
|
{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty }
|
||||||
|
};
|
||||||
|
|
||||||
chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict);
|
||||||
|
|
||||||
|
|
@ -1635,9 +1693,12 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
await CrAuthEndpoint1.RefreshToken(true);
|
await CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
await CrAuthEndpoint2.RefreshToken(true);
|
||||||
|
|
||||||
Dictionary<string, string> authDataDict = new Dictionary<string, string>
|
Dictionary<string, string> authDataDict = new Dictionary<string, string>{
|
||||||
{ { "authorization", "Bearer " + CrAuthEndpoint1.Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
|
{ "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true }) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },
|
||||||
|
{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty }
|
||||||
|
};
|
||||||
|
|
||||||
var encryptionKeys = chosenVideoSegments.encryptionKeys;
|
var encryptionKeys = chosenVideoSegments.encryptionKeys;
|
||||||
|
|
||||||
|
|
@ -1895,7 +1956,7 @@ public class CrunchyrollManager{
|
||||||
var isAbsolute = Path.IsPathRooted(outFile);
|
var isAbsolute = Path.IsPathRooted(outFile);
|
||||||
|
|
||||||
// Get all directory parts of the path except the last segment (assuming it's a file)
|
// Get all directory parts of the path except the last segment (assuming it's a file)
|
||||||
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ??[];
|
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? [];
|
||||||
|
|
||||||
// Initialize the cumulative path based on whether the original path is absolute or not
|
// Initialize the cumulative path based on whether the original path is absolute or not
|
||||||
var cumulativePath = isAbsolute ? "" : fileDir;
|
var cumulativePath = isAbsolute ? "" : fileDir;
|
||||||
|
|
@ -1990,7 +2051,7 @@ public class CrunchyrollManager{
|
||||||
Console.WriteLine($"{fileName}.xml has been created with the description.");
|
Console.WriteLine($"{fileName}.xml has been created with the description.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.MuxCover){
|
if (options is{ MuxCover: true, Noaudio: false, Novids: false }){
|
||||||
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
|
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
|
||||||
var bitmap = await Helpers.LoadImage(data.ImageBig);
|
var bitmap = await Helpers.LoadImage(data.ImageBig);
|
||||||
if (bitmap != null){
|
if (bitmap != null){
|
||||||
|
|
@ -2040,8 +2101,8 @@ public class CrunchyrollManager{
|
||||||
videoDownloadMedia.Lang = pbData.Meta.AudioLocale;
|
videoDownloadMedia.Lang = pbData.Meta.AudioLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[];
|
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ?? [];
|
||||||
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ??[];
|
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ?? [];
|
||||||
var subsDataMapped = subsData.Select(s => {
|
var subsDataMapped = subsData.Select(s => {
|
||||||
var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
|
var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
|
||||||
return new{
|
return new{
|
||||||
|
|
@ -2348,7 +2409,8 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
#region Fetch Playback Data
|
#region Fetch Playback Data
|
||||||
|
|
||||||
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music, bool auioRoleDesc){
|
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music, bool auioRoleDesc,
|
||||||
|
CrAuthSettings optionsStreamEndpointSettings){
|
||||||
var temppbData = new PlaybackData{
|
var temppbData = new PlaybackData{
|
||||||
Total = 0,
|
Total = 0,
|
||||||
Data = new Dictionary<string, StreamDetails>()
|
Data = new Dictionary<string, StreamDetails>()
|
||||||
|
|
@ -2364,7 +2426,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackRequestResponse.IsOk){
|
if (playbackRequestResponse.IsOk){
|
||||||
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
|
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
|
||||||
} else{
|
} else{
|
||||||
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
|
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
|
||||||
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play{(auioRoleDesc ? "?audioRole=description" : "")}";
|
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play{(auioRoleDesc ? "?audioRole=description" : "")}";
|
||||||
|
|
@ -2375,7 +2437,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackRequestResponse.IsOk){
|
if (playbackRequestResponse.IsOk){
|
||||||
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
|
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
|
||||||
} else{
|
} else{
|
||||||
Console.Error.WriteLine("Fallback Request Stream URLs FAILED!");
|
Console.Error.WriteLine("Fallback Request Stream URLs FAILED!");
|
||||||
}
|
}
|
||||||
|
|
@ -2405,7 +2467,7 @@ public class CrunchyrollManager{
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint){
|
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint, CrAuthSettings optionsStreamEndpointSettings){
|
||||||
var temppbData = new PlaybackData{
|
var temppbData = new PlaybackData{
|
||||||
Total = 0,
|
Total = 0,
|
||||||
Data = new Dictionary<string, StreamDetails>()
|
Data = new Dictionary<string, StreamDetails>()
|
||||||
|
|
@ -2424,7 +2486,7 @@ public class CrunchyrollManager{
|
||||||
foreach (var hardsub in playStream.HardSubs){
|
foreach (var hardsub in playStream.HardSubs){
|
||||||
var stream = hardsub.Value;
|
var stream = hardsub.Value;
|
||||||
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
|
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
|
||||||
Url =[new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint }],
|
Url = [new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint, Audio = optionsStreamEndpointSettings.Audio, Video = optionsStreamEndpointSettings.Video }],
|
||||||
IsHardsubbed = true,
|
IsHardsubbed = true,
|
||||||
HardsubLocale = stream.Hlang,
|
HardsubLocale = stream.Hlang,
|
||||||
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
|
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
|
||||||
|
|
@ -2433,7 +2495,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
derivedPlayCrunchyStreams[""] = new StreamDetails{
|
derivedPlayCrunchyStreams[""] = new StreamDetails{
|
||||||
Url =[new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint }],
|
Url = [new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint, Audio = optionsStreamEndpointSettings.Audio, Video = optionsStreamEndpointSettings.Video }],
|
||||||
IsHardsubbed = false,
|
IsHardsubbed = false,
|
||||||
HardsubLocale = Locale.DefaulT,
|
HardsubLocale = Locale.DefaulT,
|
||||||
HardsubLang = Languages.DEFAULT_lang
|
HardsubLang = Languages.DEFAULT_lang
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -139,6 +139,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedHSLang;
|
private ComboBoxItem _selectedHSLang;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _hsRawFallback;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedDescriptionLang;
|
private ComboBoxItem _selectedDescriptionLang;
|
||||||
|
|
||||||
|
|
@ -151,15 +154,18 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedStreamEndpoint;
|
private ComboBoxItem _selectedStreamEndpoint;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _firstEndpointVideo;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _firstEndpointAudio;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _SelectedStreamEndpointSecondary;
|
private ComboBoxItem _SelectedStreamEndpointSecondary;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointAuthorization = "";
|
private string _endpointAuthorization = "";
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _endpointClientId = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointUserAgent = "";
|
private string _endpointUserAgent = "";
|
||||||
|
|
||||||
|
|
@ -169,6 +175,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointDeviceType = "";
|
private string _endpointDeviceType = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _endpointVideo;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _endpointAudio;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isLoggingIn;
|
private bool _isLoggingIn;
|
||||||
|
|
||||||
|
|
@ -188,7 +200,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
private ComboBoxItem? _selectedAudioQuality;
|
private ComboBoxItem? _selectedAudioQuality;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ObservableCollection<ListBoxItem> _selectedSubLang =[];
|
private ObservableCollection<ListBoxItem> _selectedSubLang = [];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private Color _listBoxColor;
|
private Color _listBoxColor;
|
||||||
|
|
@ -231,12 +243,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
new(){ Content = "ar-SA" }
|
new(){ Content = "ar-SA" }
|
||||||
];
|
];
|
||||||
|
|
||||||
public ObservableCollection<ListBoxItem> DubLangList{ get; } =[];
|
public ObservableCollection<ListBoxItem> DubLangList{ get; } = [];
|
||||||
|
|
||||||
|
|
||||||
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } =[];
|
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = [];
|
||||||
|
|
||||||
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } =[];
|
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = [];
|
||||||
|
|
||||||
|
|
||||||
public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
|
public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
|
||||||
|
|
@ -277,7 +289,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
new(){ Content = "tv/android_tv" },
|
new(){ Content = "tv/android_tv" },
|
||||||
];
|
];
|
||||||
|
|
||||||
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
|
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } = [];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private StringItemWithDisplayName _selectedFFmpegHWAccel;
|
private StringItemWithDisplayName _selectedFFmpegHWAccel;
|
||||||
|
|
@ -345,17 +357,21 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
|
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
|
||||||
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
|
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
|
||||||
|
|
||||||
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
|
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint?.Endpoint ?? "")) ?? null;
|
||||||
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
||||||
|
|
||||||
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
|
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
|
||||||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||||
|
|
||||||
EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty;
|
EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty;
|
||||||
EndpointClientId = options.StreamEndpointSecondSettings?.Client_ID ?? string.Empty;
|
|
||||||
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
|
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
|
||||||
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
|
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
|
||||||
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty;
|
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty;
|
||||||
|
EndpointVideo = options.StreamEndpointSecondSettings?.Video ?? true;
|
||||||
|
EndpointAudio = options.StreamEndpointSecondSettings?.Audio ?? true;
|
||||||
|
|
||||||
|
FirstEndpointVideo = options.StreamEndpoint?.Video ?? true;
|
||||||
|
FirstEndpointAudio = options.StreamEndpoint?.Audio ?? true;
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
|
if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
|
||||||
EndpointNotSignedWarning = true;
|
EndpointNotSignedWarning = true;
|
||||||
|
|
@ -363,6 +379,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||||
|
|
||||||
|
if (FFmpegHWAccel.Count == 0){
|
||||||
|
FFmpegHWAccel.Add(new StringItemWithDisplayName{
|
||||||
|
DisplayName = "No hardware acceleration (error)",
|
||||||
|
value = "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
||||||
SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0];
|
SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0];
|
||||||
|
|
||||||
|
|
@ -390,6 +413,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
|
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
|
||||||
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
|
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
|
||||||
|
|
||||||
|
HsRawFallback = options.HsRawFallback;
|
||||||
FixCccSubtitles = options.FixCccSubtitles;
|
FixCccSubtitles = options.FixCccSubtitles;
|
||||||
ConvertVtt2Ass = options.ConvertVtt2Ass;
|
ConvertVtt2Ass = options.ConvertVtt2Ass;
|
||||||
SubsDownloadDuplicate = options.SubsDownloadDuplicate;
|
SubsDownloadDuplicate = options.SubsDownloadDuplicate;
|
||||||
|
|
@ -519,20 +543,25 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
|
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
|
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.HsRawFallback = HsRawFallback;
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
|
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
|
||||||
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
|
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
|
||||||
|
|
||||||
|
var endpointSettingsFirst = new CrAuthSettings();
|
||||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
|
endpointSettingsFirst.Endpoint = SelectedStreamEndpoint.Content + "";
|
||||||
|
endpointSettingsFirst.Video = FirstEndpointVideo;
|
||||||
|
endpointSettingsFirst.Audio = FirstEndpointAudio;
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = endpointSettingsFirst;
|
||||||
|
|
||||||
var endpointSettings = new CrAuthSettings();
|
var endpointSettings = new CrAuthSettings();
|
||||||
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
|
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
|
||||||
endpointSettings.Authorization = EndpointAuthorization;
|
endpointSettings.Authorization = EndpointAuthorization;
|
||||||
endpointSettings.Client_ID = EndpointClientId;
|
|
||||||
endpointSettings.UserAgent = EndpointUserAgent;
|
endpointSettings.UserAgent = EndpointUserAgent;
|
||||||
endpointSettings.Device_name = EndpointDeviceName;
|
endpointSettings.Device_name = EndpointDeviceName;
|
||||||
endpointSettings.Device_type = EndpointDeviceType;
|
endpointSettings.Device_type = EndpointDeviceType;
|
||||||
|
endpointSettings.Video = EndpointVideo;
|
||||||
|
endpointSettings.Audio = EndpointAudio;
|
||||||
|
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
|
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
|
||||||
|
|
@ -657,13 +686,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
CrunchyrollManager.Instance.HistoryList =[];
|
CrunchyrollManager.Instance.HistoryList = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = SonarrClient.Instance.RefreshSonarrLite();
|
_ = SonarrClient.Instance.RefreshSonarrLite();
|
||||||
} else{
|
} else{
|
||||||
CrunchyrollManager.Instance.HistoryList =[];
|
CrunchyrollManager.Instance.HistoryList = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -703,7 +732,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||||
|
|
||||||
EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization;
|
EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization;
|
||||||
EndpointClientId = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Client_ID;
|
|
||||||
EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent;
|
EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent;
|
||||||
EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name;
|
EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name;
|
||||||
EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type;
|
EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type;
|
||||||
|
|
@ -759,11 +787,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
return MapHWAccelOptions(accels);
|
return MapHWAccelOptions(accels);
|
||||||
}
|
}
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
Console.WriteLine("Failed to get Available HW Accel Options" + e);
|
Console.Error.WriteLine("Failed to get Available HW Accel Options" + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var result = new List<StringItemWithDisplayName>();
|
||||||
|
result.Add(new StringItemWithDisplayName{
|
||||||
|
DisplayName = "No hardware acceleration / error",
|
||||||
|
value = "error"
|
||||||
|
});
|
||||||
|
|
||||||
return[];
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){
|
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,12 @@
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</controls:SettingsExpander.Footer>
|
</controls:SettingsExpander.Footer>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="No hardsubs fallback" Description="If no hardsubs are available, automatically download the no-hardsub (raw) video.">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<CheckBox IsChecked="{Binding HsRawFallback}"> </CheckBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
</controls:SettingsExpander>
|
</controls:SettingsExpander>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -249,12 +255,27 @@
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
|
||||||
<controls:SettingsExpanderItem Content="Stream Endpoint " IsEnabled="False">
|
<controls:SettingsExpanderItem Content="Stream Endpoint ">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
<StackPanel>
|
||||||
ItemsSource="{Binding StreamEndpoints}"
|
<ComboBox IsEnabled="False" HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
||||||
SelectedItem="{Binding SelectedStreamEndpoint}">
|
ItemsSource="{Binding StreamEndpoints}"
|
||||||
</ComboBox>
|
SelectedItem="{Binding SelectedStreamEndpoint}">
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||||
|
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding FirstEndpointVideo}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||||
|
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding FirstEndpointAudio}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
|
@ -266,33 +287,38 @@
|
||||||
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
|
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
|
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||||
|
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding EndpointVideo}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||||
|
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding EndpointAudio}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Margin="0,5">
|
<StackPanel Margin="0,5">
|
||||||
<TextBlock Text="Authorization" />
|
<TextBlock Text="Authorization" />
|
||||||
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250"
|
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||||
Text="{Binding EndpointAuthorization}" />
|
Text="{Binding EndpointAuthorization}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Margin="0,5">
|
|
||||||
<TextBlock Text="Client Id" />
|
|
||||||
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250"
|
|
||||||
Text="{Binding EndpointClientId}" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Margin="0,5">
|
<StackPanel Margin="0,5">
|
||||||
<TextBlock Text="User Agent" />
|
<TextBlock Text="User Agent" />
|
||||||
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250"
|
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||||
Text="{Binding EndpointUserAgent}" />
|
Text="{Binding EndpointUserAgent}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Margin="0,5">
|
<StackPanel Margin="0,5">
|
||||||
<TextBlock Text="Device Type" />
|
<TextBlock Text="Device Type" />
|
||||||
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250"
|
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||||
Text="{Binding EndpointDeviceType}" />
|
Text="{Binding EndpointDeviceType}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Margin="0,5">
|
<StackPanel Margin="0,5">
|
||||||
<TextBlock Text="Device Name" />
|
<TextBlock Text="Device Name" />
|
||||||
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250"
|
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||||
Text="{Binding EndpointDeviceName}" />
|
Text="{Binding EndpointDeviceName}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using ReactiveUI.Avalonia;
|
||||||
|
|
||||||
namespace CRD;
|
namespace CRD;
|
||||||
|
|
||||||
|
|
@ -26,7 +27,8 @@ sealed class Program{
|
||||||
var builder = AppBuilder.Configure<App>()
|
var builder = AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.WithInterFont()
|
.WithInterFont()
|
||||||
.LogToTrace();
|
.LogToTrace()
|
||||||
|
.UseReactiveUI() ;
|
||||||
|
|
||||||
if (isHeadless){
|
if (isHeadless){
|
||||||
Console.WriteLine("Running in headless mode...");
|
Console.WriteLine("Running in headless mode...");
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,12 @@ public class Helpers{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int ToKbps(int bps) => (int)Math.Round(bps / 1000.0);
|
public static int ToKbps(int bps) => (int)Math.Round(bps / 1000.0);
|
||||||
|
|
||||||
public static int SnapToAudioBucket(int kbps){
|
public static int SnapToAudioBucket(int kbps){
|
||||||
int[] buckets ={ 64, 96, 128, 192,256 };
|
int[] buckets = { 64, 96, 128, 192, 256 };
|
||||||
return buckets.OrderBy(b => Math.Abs(b - kbps)).First();
|
return buckets.OrderBy(b => Math.Abs(b - kbps)).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int WidthBucket(int width, int height){
|
public static int WidthBucket(int width, int height){
|
||||||
int expected = (int)Math.Round(height * 16 / 9.0);
|
int expected = (int)Math.Round(height * 16 / 9.0);
|
||||||
int tol = Math.Max(8, (int)(expected * 0.02)); // ~2% or ≥8 px
|
int tol = Math.Max(8, (int)(expected * 0.02)); // ~2% or ≥8 px
|
||||||
|
|
@ -555,7 +557,7 @@ public class Helpers{
|
||||||
return CosineSimilarity(vector1, vector2);
|
return CosineSimilarity(vector1, vector2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly char[] Delimiters ={ ' ', ',', '.', ';', ':', '-', '_', '\'' };
|
private static readonly char[] Delimiters = { ' ', ',', '.', ';', ':', '-', '_', '\'' };
|
||||||
|
|
||||||
public static Dictionary<string, double> ComputeWordFrequency(string text){
|
public static Dictionary<string, double> ComputeWordFrequency(string text){
|
||||||
var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -718,7 +720,7 @@ public class Helpers{
|
||||||
bool isValid = !folderName.Any(c => invalidChars.Contains(c));
|
bool isValid = !folderName.Any(c => invalidChars.Contains(c));
|
||||||
|
|
||||||
// Check for reserved names on Windows
|
// Check for reserved names on Windows
|
||||||
string[] reservedNames =["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
|
string[] reservedNames = ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
|
||||||
bool isReservedName = reservedNames.Contains(folderName.ToUpperInvariant());
|
bool isReservedName = reservedNames.Contains(folderName.ToUpperInvariant());
|
||||||
|
|
||||||
if (isValid && !isReservedName && folderName.Length <= 255){
|
if (isValid && !isReservedName && folderName.Length <= 255){
|
||||||
|
|
@ -845,30 +847,46 @@ public class Helpers{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MergePlaylistData(
|
|
||||||
Dictionary<string, ServerData> target,
|
|
||||||
Dictionary<string, ServerData> source){
|
|
||||||
foreach (var kvp in source){
|
|
||||||
if (target.TryGetValue(kvp.Key, out var existing)){
|
|
||||||
// Merge audio
|
|
||||||
existing.audio ??=[];
|
|
||||||
if (kvp.Value.audio != null)
|
|
||||||
existing.audio.AddRange(kvp.Value.audio);
|
|
||||||
|
|
||||||
// Merge video
|
public static void MergePlaylistData(
|
||||||
existing.video ??=[];
|
ServerData target,
|
||||||
if (kvp.Value.video != null)
|
Dictionary<string, ServerData> source,
|
||||||
existing.video.AddRange(kvp.Value.video);
|
bool mergeAudio,
|
||||||
} else{
|
bool mergeVideo){
|
||||||
// Add new entry (clone lists to avoid reference issues)
|
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||||
target[kvp.Key] = new ServerData{
|
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||||
audio = kvp.Value.audio != null ? new List<AudioPlaylist>(kvp.Value.audio) : new List<AudioPlaylist>(),
|
|
||||||
video = kvp.Value.video != null ? new List<VideoPlaylist>(kvp.Value.video) : new List<VideoPlaylist>()
|
var serverSet = new HashSet<string>(target.servers);
|
||||||
};
|
|
||||||
|
void AddServer(string s){
|
||||||
|
if (!string.IsNullOrWhiteSpace(s) && serverSet.Add(s))
|
||||||
|
target.servers.Add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in source){
|
||||||
|
var key = kvp.Key;
|
||||||
|
var src = kvp.Value;
|
||||||
|
|
||||||
|
if (!src.servers.Contains(key))
|
||||||
|
src.servers.Add(key);
|
||||||
|
|
||||||
|
AddServer(key);
|
||||||
|
foreach (var s in src.servers)
|
||||||
|
AddServer(s);
|
||||||
|
|
||||||
|
if (mergeAudio && src.audio != null){
|
||||||
|
target.audio ??= [];
|
||||||
|
target.audio.AddRange(src.audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergeVideo && src.video != null){
|
||||||
|
target.video ??= [];
|
||||||
|
target.video.AddRange(src.video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static readonly SemaphoreSlim ShutdownLock = new(1, 1);
|
private static readonly SemaphoreSlim ShutdownLock = new(1, 1);
|
||||||
|
|
||||||
public static async Task ShutdownComputer(){
|
public static async Task ShutdownComputer(){
|
||||||
|
|
|
||||||
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;
|
||||||
|
|
@ -270,6 +296,7 @@ public static class ApiUrls{
|
||||||
|
|
||||||
public static string Auth => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/auth/v1/token";
|
public static string Auth => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/auth/v1/token";
|
||||||
public static string Profile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/profile";
|
public static string Profile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/profile";
|
||||||
|
public static string Profiles => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/multiprofile";
|
||||||
public static string CmsToken => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/index/v2";
|
public static string CmsToken => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/index/v2";
|
||||||
public static string Search => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/search";
|
public static string Search => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/search";
|
||||||
public static string Browse => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/browse";
|
public static string Browse => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/browse";
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@ public class Merger{
|
||||||
|
|
||||||
public Merger(MergerOptions options){
|
public Merger(MergerOptions options){
|
||||||
this.options = options;
|
this.options = options;
|
||||||
if (this.options.SkipSubMux != null && this.options.SkipSubMux == true){
|
|
||||||
this.options.Subtitles = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){
|
if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){
|
||||||
this.options.VideoTitle = this.options.VideoTitle.Replace("\"", "'");
|
this.options.VideoTitle = this.options.VideoTitle.Replace("\"", "'");
|
||||||
|
|
@ -74,35 +71,39 @@ public class Merger{
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
|
if (!options.SkipSubMux){
|
||||||
|
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
|
||||||
|
|
||||||
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
|
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
|
||||||
if (sub.value.Delay != null && sub.value.Delay != 0){
|
if (sub.value.Delay != null && sub.value.Delay != 0){
|
||||||
double delay = sub.value.Delay / 1000.0 ?? 0;
|
double delay = sub.value.Delay / 1000.0 ?? 0;
|
||||||
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
|
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Add($"-i \"{sub.value.File}\"");
|
||||||
|
metaData.Add($"-map {index}:s");
|
||||||
|
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
|
||||||
|
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
|
||||||
|
&& sub.value.ClosedCaption == false){
|
||||||
|
metaData.Add($"-disposition:s:{sub.i} default");
|
||||||
|
} else{
|
||||||
|
metaData.Add($"-disposition:s:{sub.i} 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
args.Add($"-i \"{sub.value.File}\"");
|
|
||||||
metaData.Add($"-map {index}:s");
|
|
||||||
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
|
|
||||||
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
|
|
||||||
&& sub.value.ClosedCaption == false){
|
|
||||||
metaData.Add($"-disposition:s:{sub.i} default");
|
|
||||||
} else{
|
|
||||||
metaData.Add($"-disposition:s:{sub.i} 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
args.AddRange(metaData);
|
args.AddRange(metaData);
|
||||||
// args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
|
// args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
|
||||||
args.Add("-c:v copy");
|
args.Add("-c:v copy");
|
||||||
args.Add("-c:a copy");
|
args.Add("-c:a copy");
|
||||||
args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass");
|
args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass");
|
||||||
args.AddRange(options.Subtitles.Select((sub, subindex) =>
|
if (!options.SkipSubMux){
|
||||||
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
|
args.AddRange(options.Subtitles.Select((sub, subindex) =>
|
||||||
|
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(options.VideoTitle)){
|
if (!string.IsNullOrEmpty(options.VideoTitle)){
|
||||||
args.Add($"-metadata title=\"{options.VideoTitle}\"");
|
args.Add($"-metadata title=\"{options.VideoTitle}\"");
|
||||||
|
|
@ -134,9 +135,9 @@ public class Merger{
|
||||||
}
|
}
|
||||||
|
|
||||||
var audio = options.OnlyAudio.First();
|
var audio = options.OnlyAudio.First();
|
||||||
|
|
||||||
args.Add($"-i \"{audio.Path}\"");
|
args.Add($"-i \"{audio.Path}\"");
|
||||||
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : "") );
|
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : ""));
|
||||||
args.Add($"\"{options.Output}\"");
|
args.Add($"\"{options.Output}\"");
|
||||||
return string.Join(" ", args);
|
return string.Join(" ", args);
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +171,7 @@ public class Merger{
|
||||||
// var sortedAudio = options.OnlyAudio
|
// var sortedAudio = options.OnlyAudio
|
||||||
// .OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue)
|
// .OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue)
|
||||||
// .ToList();
|
// .ToList();
|
||||||
|
|
||||||
var rank = options.DubLangList
|
var rank = options.DubLangList
|
||||||
.Select((val, i) => new{ val, i })
|
.Select((val, i) => new{ val, i })
|
||||||
.ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -204,7 +205,7 @@ public class Merger{
|
||||||
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\"");
|
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Subtitles.Count > 0){
|
if (options.Subtitles.Count > 0 && !options.SkipSubMux){
|
||||||
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
|
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
|
||||||
|
|
||||||
var sortedSubtitles = options.Subtitles
|
var sortedSubtitles = options.Subtitles
|
||||||
|
|
@ -274,7 +275,7 @@ public class Merger{
|
||||||
if (options.Description is{ Count: > 0 }){
|
if (options.Description is{ Count: > 0 }){
|
||||||
args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\"");
|
args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Cover.Count > 0){
|
if (options.Cover.Count > 0){
|
||||||
if (File.Exists(options.Cover.First().Path)){
|
if (File.Exists(options.Cover.First().Path)){
|
||||||
args.Add($"--attach-file \"{options.Cover.First().Path}\"");
|
args.Add($"--attach-file \"{options.Cover.First().Path}\"");
|
||||||
|
|
@ -446,14 +447,16 @@ public class Merger{
|
||||||
allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".new.resume"));
|
allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".new.resume"));
|
||||||
|
|
||||||
options.Description?.ForEach(description => Helpers.DeleteFile(description.Path));
|
options.Description?.ForEach(description => Helpers.DeleteFile(description.Path));
|
||||||
|
|
||||||
options.Cover?.ForEach(cover => Helpers.DeleteFile(cover.Path));
|
options.Cover?.ForEach(cover => Helpers.DeleteFile(cover.Path));
|
||||||
|
|
||||||
// Delete chapter files if any
|
// Delete chapter files if any
|
||||||
options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
|
options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
|
||||||
|
|
||||||
// Delete subtitle files
|
if (!options.SkipSubMux){
|
||||||
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
|
// Delete subtitle files
|
||||||
|
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -486,7 +489,7 @@ public class CrunchyMuxOptions{
|
||||||
public List<string> DubLangList{ get; set; } = new List<string>();
|
public List<string> DubLangList{ get; set; } = new List<string>();
|
||||||
public List<string> SubLangList{ get; set; } = new List<string>();
|
public List<string> SubLangList{ get; set; } = new List<string>();
|
||||||
public string Output{ get; set; }
|
public string Output{ get; set; }
|
||||||
public bool? SkipSubMux{ get; set; }
|
public bool SkipSubMux{ get; set; }
|
||||||
public bool? KeepAllVideos{ get; set; }
|
public bool? KeepAllVideos{ get; set; }
|
||||||
public bool? Novids{ get; set; }
|
public bool? Novids{ get; set; }
|
||||||
public bool Mp4{ get; set; }
|
public bool Mp4{ get; set; }
|
||||||
|
|
@ -524,7 +527,7 @@ public class MergerOptions{
|
||||||
public string VideoTitle{ get; set; }
|
public string VideoTitle{ get; set; }
|
||||||
public bool? KeepAllVideos{ get; set; }
|
public bool? KeepAllVideos{ get; set; }
|
||||||
public List<ParsedFont> Fonts{ get; set; } = new List<ParsedFont>();
|
public List<ParsedFont> Fonts{ get; set; } = new List<ParsedFont>();
|
||||||
public bool? SkipSubMux{ get; set; }
|
public bool SkipSubMux{ get; set; }
|
||||||
public MuxOptions Options{ get; set; }
|
public MuxOptions Options{ get; set; }
|
||||||
public Defaults Defaults{ get; set; }
|
public Defaults Defaults{ get; set; }
|
||||||
public bool mp3{ get; set; }
|
public bool mp3{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,10 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("proxy_password")]
|
[JsonProperty("proxy_password")]
|
||||||
public string? ProxyPassword{ get; set; }
|
public string? ProxyPassword{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("flare_solverr_properties")]
|
||||||
|
public FlareSolverrProperties? FlareSolverrProperties{ get; set; }
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
@ -150,6 +154,9 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("hard_sub_lang")]
|
[JsonProperty("hard_sub_lang")]
|
||||||
public string Hslang{ get; set; } = "";
|
public string Hslang{ get; set; } = "";
|
||||||
|
|
||||||
|
[JsonProperty("hard_sub_raw_fallback")]
|
||||||
|
public bool HsRawFallback{ get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public int Kstream{ get; set; }
|
public int Kstream{ get; set; }
|
||||||
|
|
@ -298,14 +305,11 @@ public class CrDownloadOptions{
|
||||||
[JsonProperty("calendar_hide_dubs")]
|
[JsonProperty("calendar_hide_dubs")]
|
||||||
public bool CalendarHideDubs{ get; set; }
|
public bool CalendarHideDubs{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("calendar_filter_by_air_date")]
|
|
||||||
public bool CalendarFilterByAirDate{ get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("calendar_show_upcoming_episodes")]
|
[JsonProperty("calendar_show_upcoming_episodes")]
|
||||||
public bool CalendarShowUpcomingEpisodes{ get; set; }
|
public bool CalendarShowUpcomingEpisodes{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("stream_endpoint")]
|
[JsonProperty("stream_endpoint_settings")]
|
||||||
public string? StreamEndpoint{ get; set; }
|
public CrAuthSettings? StreamEndpoint{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("stream_endpoint_secondary_settings")]
|
[JsonProperty("stream_endpoint_secondary_settings")]
|
||||||
public CrAuthSettings? StreamEndpointSecondSettings{ get; set; }
|
public CrAuthSettings? StreamEndpointSecondSettings{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ public class CrProfile{
|
||||||
[JsonProperty("profile_name")]
|
[JsonProperty("profile_name")]
|
||||||
public string? ProfileName{ get; set; }
|
public string? ProfileName{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("profile_id")]
|
||||||
|
public string? ProfileId{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("preferred_content_audio_language")]
|
[JsonProperty("preferred_content_audio_language")]
|
||||||
public string? PreferredContentAudioLanguage{ get; set; }
|
public string? PreferredContentAudioLanguage{ get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ public class StreamDetails{
|
||||||
public class UrlWithAuth{
|
public class UrlWithAuth{
|
||||||
|
|
||||||
public CrAuth? CrAuth{ get; set; }
|
public CrAuth? CrAuth{ get; set; }
|
||||||
|
public bool Video{ get; set; }
|
||||||
|
public bool Audio{ get; set; }
|
||||||
|
|
||||||
public string? Url{ get; set; }
|
public string? Url{ get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,19 @@ public class AuthData{
|
||||||
|
|
||||||
public class CrAuthSettings{
|
public class CrAuthSettings{
|
||||||
public string Endpoint{ get; set; }
|
public string Endpoint{ get; set; }
|
||||||
public string Client_ID{ get; set; }
|
|
||||||
public string Authorization{ get; set; }
|
public string Authorization{ get; set; }
|
||||||
public string UserAgent{ get; set; }
|
public string UserAgent{ get; set; }
|
||||||
public string Device_type{ get; set; }
|
public string Device_type{ get; set; }
|
||||||
public string Device_name{ get; set; }
|
public string Device_name{ get; set; }
|
||||||
|
|
||||||
|
public bool Video{ get; set; }
|
||||||
|
public bool Audio{ get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StreamInfo{
|
||||||
|
public string Playlist { get; set; }
|
||||||
|
public bool Audio { get; set; }
|
||||||
|
public bool Video { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DrmAuthData{
|
public class DrmAuthData{
|
||||||
|
|
@ -58,8 +65,8 @@ public class CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool?
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CrunchySeriesList{
|
public class CrunchySeriesList{
|
||||||
public List<Episode> List{ get; set; }
|
public List<Episode> List{ get; set; } = [];
|
||||||
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; }
|
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Episode{
|
public class Episode{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -18,7 +19,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _prevButtonEnabled = true;
|
private bool _prevButtonEnabled = true;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _nextButtonEnabled = true;
|
private bool _nextButtonEnabled = true;
|
||||||
|
|
||||||
|
|
@ -28,9 +29,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _customCalendar;
|
private bool _customCalendar;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _filterByAirDate;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _showUpcomingEpisodes;
|
private bool _showUpcomingEpisodes;
|
||||||
|
|
||||||
|
|
@ -75,7 +73,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
|
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
|
||||||
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
|
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
|
||||||
FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate;
|
|
||||||
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
|
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
|
||||||
|
|
||||||
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
|
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
|
||||||
|
|
@ -87,7 +84,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private string GetThisWeeksMondayDate(){
|
private string GetThisWeeksMondayDate(){
|
||||||
DateTime today = DateTime.Today;
|
DateTime today = DateTime.Today;
|
||||||
|
|
||||||
|
|
@ -104,13 +100,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void LoadCalendar(string mondayDate,DateTime customCalDate, bool forceUpdate){
|
public async void LoadCalendar(string mondayDate, DateTime customCalDate, bool forceUpdate){
|
||||||
ShowLoading = true;
|
ShowLoading = true;
|
||||||
|
|
||||||
CalendarWeek week;
|
CalendarWeek week;
|
||||||
|
|
||||||
if (CustomCalendar){
|
if (CustomCalendar){
|
||||||
|
|
||||||
if (customCalDate.Date == DateTime.Now.Date){
|
if (customCalDate.Date == DateTime.Now.Date){
|
||||||
PrevButtonEnabled = false;
|
PrevButtonEnabled = false;
|
||||||
NextButtonEnabled = true;
|
NextButtonEnabled = true;
|
||||||
|
|
@ -118,7 +113,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
PrevButtonEnabled = true;
|
PrevButtonEnabled = true;
|
||||||
NextButtonEnabled = false;
|
NextButtonEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate);
|
week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate);
|
||||||
} else{
|
} else{
|
||||||
PrevButtonEnabled = true;
|
PrevButtonEnabled = true;
|
||||||
|
|
@ -140,29 +135,24 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
|
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
|
||||||
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
||||||
if (calendarDayCalendarEpisode.AnilistEpisode){
|
if (calendarDayCalendarEpisode.AnilistEpisode){
|
||||||
_ = calendarDayCalendarEpisode.LoadImage(100,150);
|
_ = calendarDayCalendarEpisode.LoadImage(100, 150);
|
||||||
} else{
|
} else{
|
||||||
_ = calendarDayCalendarEpisode.LoadImage();
|
_ = calendarDayCalendarEpisode.LoadImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else{
|
} else{
|
||||||
foreach (var calendarDay in CalendarDays){
|
foreach (var calendarDay in CalendarDays){
|
||||||
var episodesCopy = new List<CalendarEpisode>(calendarDay.CalendarEpisodes);
|
if (HideDubs)
|
||||||
foreach (var calendarDayCalendarEpisode in episodesCopy){
|
calendarDay.CalendarEpisodes.RemoveAll(e => CrSimulcastCalendarFilter.IsDubOrAltLanguageSeason(e.SeasonName));
|
||||||
if (calendarDayCalendarEpisode.SeasonName != null && HideDubs && calendarDayCalendarEpisode.SeasonName.EndsWith("Dub)")){
|
|
||||||
calendarDay.CalendarEpisodes.Remove(calendarDayCalendarEpisode);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
foreach (var e in calendarDay.CalendarEpisodes){
|
||||||
if (calendarDayCalendarEpisode.AnilistEpisode){
|
if (e.ImageBitmap == null){
|
||||||
_ = calendarDayCalendarEpisode.LoadImage(100,150);
|
if (e.AnilistEpisode)
|
||||||
} else{
|
_ = e.LoadImage(100, 150);
|
||||||
_ = calendarDayCalendarEpisode.LoadImage();
|
else
|
||||||
}
|
_ = e.LoadImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +189,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(6);
|
refreshDate = currentWeek.FirstDayOfWeek.AddDays(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadCalendar(mondayDate,refreshDate, true);
|
LoadCalendar(mondayDate, refreshDate, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|
@ -215,13 +205,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
} else{
|
} else{
|
||||||
mondayDate = GetThisWeeksMondayDate();
|
mondayDate = GetThisWeeksMondayDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
var refreshDate = DateTime.Now;
|
var refreshDate = DateTime.Now;
|
||||||
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
||||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
|
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadCalendar(mondayDate,refreshDate, false);
|
LoadCalendar(mondayDate, refreshDate, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|
@ -237,15 +227,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
} else{
|
} else{
|
||||||
mondayDate = GetThisWeeksMondayDate();
|
mondayDate = GetThisWeeksMondayDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
var refreshDate = DateTime.Now;
|
var refreshDate = DateTime.Now;
|
||||||
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
||||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13);
|
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadCalendar(mondayDate,refreshDate, false);
|
LoadCalendar(mondayDate, refreshDate, false);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -268,7 +256,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value;
|
CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value;
|
||||||
|
|
||||||
LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true);
|
LoadCalendar(GetThisWeeksMondayDate(), DateTime.Now, true);
|
||||||
|
|
||||||
CfgManager.WriteCrSettings();
|
CfgManager.WriteCrSettings();
|
||||||
}
|
}
|
||||||
|
|
@ -282,15 +270,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
CfgManager.WriteCrSettings();
|
CfgManager.WriteCrSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnFilterByAirDateChanged(bool value){
|
|
||||||
if (loading){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
|
|
||||||
CfgManager.WriteCrSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnShowUpcomingEpisodesChanged(bool value){
|
partial void OnShowUpcomingEpisodesChanged(bool value){
|
||||||
if (loading){
|
if (loading){
|
||||||
return;
|
return;
|
||||||
|
|
@ -310,7 +289,4 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
||||||
CfgManager.WriteCrSettings();
|
CfgManager.WriteCrSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +207,18 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _proxyPassword;
|
private string _proxyPassword;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _flareSolverrHost = "localhost";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _flareSolverrPort = "8191";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _flareSolverrUseSsl = false;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _useFlareSolverr = false;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _tempDownloadDirPath;
|
private string _tempDownloadDirPath;
|
||||||
|
|
@ -264,6 +276,15 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
SonarrPort = props.Port + "";
|
SonarrPort = props.Port + "";
|
||||||
SonarrApiKey = props.ApiKey + "";
|
SonarrApiKey = props.ApiKey + "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var propsFlareSolverr = options.FlareSolverrProperties;
|
||||||
|
|
||||||
|
if (propsFlareSolverr != null){
|
||||||
|
FlareSolverrUseSsl = propsFlareSolverr.UseSsl;
|
||||||
|
UseFlareSolverr = propsFlareSolverr.UseFlareSolverr;
|
||||||
|
FlareSolverrHost = propsFlareSolverr.Host + "";
|
||||||
|
FlareSolverrPort = propsFlareSolverr.Port + "";
|
||||||
|
}
|
||||||
|
|
||||||
ProxyEnabled = options.ProxyEnabled;
|
ProxyEnabled = options.ProxyEnabled;
|
||||||
ProxySocks = options.ProxySocks;
|
ProxySocks = options.ProxySocks;
|
||||||
|
|
@ -362,9 +383,22 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
props.ApiKey = SonarrApiKey;
|
props.ApiKey = SonarrApiKey;
|
||||||
|
|
||||||
|
|
||||||
settings.SonarrProperties = props;
|
settings.SonarrProperties = props;
|
||||||
|
|
||||||
|
var propsFlareSolverr = new FlareSolverrProperties();
|
||||||
|
|
||||||
|
propsFlareSolverr.UseSsl = FlareSolverrUseSsl;
|
||||||
|
propsFlareSolverr.UseFlareSolverr = UseFlareSolverr;
|
||||||
|
propsFlareSolverr.Host = FlareSolverrHost;
|
||||||
|
|
||||||
|
if (int.TryParse(FlareSolverrPort, out var portNumberFlare)){
|
||||||
|
propsFlareSolverr.Port = portNumberFlare;
|
||||||
|
} else{
|
||||||
|
propsFlareSolverr.Port = 8989;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.FlareSolverrProperties = propsFlareSolverr;
|
||||||
|
|
||||||
settings.LogMode = LogMode;
|
settings.LogMode = LogMode;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
<CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox>
|
<CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem Content="Allow early start of next download" Description="When enabled, the next download starts as soon as the previous file has finished downloading, even if it is still being finalized (muxed/moved).">
|
<controls:SettingsExpanderItem Content="Allow early start of next download" Description="When enabled, the next download starts as soon as the previous file has finished downloading, even if it is still being finalized (muxed/moved).">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<CheckBox IsChecked="{Binding DownloadAllowEarlyStart}"> </CheckBox>
|
<CheckBox IsChecked="{Binding DownloadAllowEarlyStart}"> </CheckBox>
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
HorizontalAlignment="Stretch" />
|
HorizontalAlignment="Stretch" />
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem IsVisible="{Binding DownloadAllowEarlyStart}" Content="Parallel Processing Jobs" Description="The maximum number of completed downloads that can be processed simultaneously (encoding, muxing, moving)">
|
<controls:SettingsExpanderItem IsVisible="{Binding DownloadAllowEarlyStart}" Content="Parallel Processing Jobs" Description="The maximum number of completed downloads that can be processed simultaneously (encoding, muxing, moving)">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<controls:NumberBox Minimum="0" Maximum="10"
|
<controls:NumberBox Minimum="0" Maximum="10"
|
||||||
|
|
@ -385,6 +385,40 @@
|
||||||
|
|
||||||
</controls:SettingsExpander>
|
</controls:SettingsExpander>
|
||||||
|
|
||||||
|
<controls:SettingsExpander Header="Flare Solverr Settings"
|
||||||
|
IconSource="Wifi2"
|
||||||
|
Description="FlareSolverr settings (used only for the Calendar)"
|
||||||
|
IsExpanded="False">
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="Use Flare Solverr">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<CheckBox IsChecked="{Binding UseFlareSolverr}"> </CheckBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="Host">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||||
|
Text="{Binding FlareSolverrHost}" />
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="Port">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||||
|
Text="{Binding FlareSolverrPort}" />
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem Content="Use SSL">
|
||||||
|
<controls:SettingsExpanderItem.Footer>
|
||||||
|
<CheckBox IsChecked="{Binding FlareSolverrUseSsl}"> </CheckBox>
|
||||||
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
|
||||||
|
</controls:SettingsExpander>
|
||||||
|
|
||||||
<controls:SettingsExpander Header="App Appearance"
|
<controls:SettingsExpander Header="App Appearance"
|
||||||
IconSource="DarkTheme"
|
IconSource="DarkTheme"
|
||||||
Description="Customize the look and feel of the application"
|
Description="Customize the look and feel of the application"
|
||||||
|
|
@ -617,7 +651,7 @@
|
||||||
DockPanel.Dock="Left" />
|
DockPanel.Dock="Left" />
|
||||||
|
|
||||||
<ColorPicker Color="{Binding CustomAccentColor}"
|
<ColorPicker Color="{Binding CustomAccentColor}"
|
||||||
DockPanel.Dock="Right" />
|
DockPanel.Dock="Right" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ A simple crunchyroll downloader that allows you to download your favorite series
|
||||||
## 🛠️ System Requirements
|
## 🛠️ System Requirements
|
||||||
|
|
||||||
- **Operating System:** Windows 10 or Windows 11
|
- **Operating System:** Windows 10 or Windows 11
|
||||||
- **.NET Desktop Runtime:** Version 8.0
|
- **.NET Desktop Runtime:** Version 10.0
|
||||||
- **Visual C++ Redistributable:** 2015–2022
|
- **Visual C++ Redistributable:** 2015–2022
|
||||||
|
|
||||||
## 🖥️ Features
|
## 🖥️ Features
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue