mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 20:10:26 +00:00
Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7687c80e8 | ||
|
|
c5660a87e7 | ||
|
|
dc570bf420 |
40 changed files with 1971 additions and 708 deletions
|
|
@ -8,6 +8,7 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Downloader.Crunchyroll.Utils;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.History;
|
||||
|
|
@ -67,6 +68,10 @@ public class CalendarManager{
|
|||
return forDate;
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
await LoadAnilistUpcoming();
|
||||
}
|
||||
|
||||
var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us")
|
||||
? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false)
|
||||
: HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false);
|
||||
|
|
@ -75,19 +80,26 @@ public class CalendarManager{
|
|||
request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip, deflate, br");
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
(bool IsOk, string ResponseContent, string error) response;
|
||||
if (!HttpClientReq.Instance.useFlareSolverr){
|
||||
response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
} else{
|
||||
response = await HttpClientReq.Instance.SendFlareSolverrHttpRequest(request);
|
||||
}
|
||||
|
||||
|
||||
if (!response.IsOk){
|
||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||
MessageBus.Current.SendMessage(new ToastMessage("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5));
|
||||
Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar.");
|
||||
} else{
|
||||
Console.Error.WriteLine($"Calendar request failed");
|
||||
}
|
||||
|
||||
return new CalendarWeek();
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +176,24 @@ public class CalendarManager{
|
|||
Console.Error.WriteLine("No days found in the HTML document.");
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
foreach (var calendarDay in week.CalendarDays){
|
||||
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
|
||||
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
|
||||
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
||||
|
||||
foreach (var calendarEpisode in list
|
||||
.Where(e => calendarDay.DateTime.Date.Day == e.DateTime.Date.Day)
|
||||
.Where(e => calendarDay.CalendarEpisodes.All(ele =>
|
||||
ele.CrSeriesID != e.CrSeriesID &&
|
||||
!CrSimulcastCalendarFilter.IsMatch(ele.SeasonName, e.SeasonName, similarityThreshold: 0.5)))){
|
||||
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calendar[weeksMondayDate] = week;
|
||||
|
||||
|
||||
|
|
@ -172,14 +202,14 @@ public class CalendarManager{
|
|||
|
||||
|
||||
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
await LoadAnilistUpcoming();
|
||||
}
|
||||
|
||||
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
|
||||
return forDate;
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
await LoadAnilistUpcoming();
|
||||
}
|
||||
|
||||
|
||||
CalendarWeek week = new CalendarWeek();
|
||||
week.CalendarDays = new List<CalendarDay>();
|
||||
|
|
@ -201,13 +231,14 @@ public class CalendarManager{
|
|||
var firstDayOfWeek = week.CalendarDays.First().DateTime;
|
||||
week.FirstDayOfWeek = firstDayOfWeek;
|
||||
|
||||
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes("", 200, firstDayOfWeek, true);
|
||||
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true);
|
||||
|
||||
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||
var newEpisodes = newEpisodesBase.Data;
|
||||
|
||||
//EpisodeAirDate
|
||||
foreach (var crBrowseEpisode in newEpisodes){
|
||||
bool filtered = false;
|
||||
DateTime episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.Kind == DateTimeKind.Utc
|
||||
? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.EpisodeAirDate;
|
||||
|
|
@ -221,49 +252,35 @@ public class CalendarManager{
|
|||
|
||||
DateTime targetDate;
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate){
|
||||
targetDate = episodeAirDate;
|
||||
|
||||
if (targetDate >= oneYearFromNow){
|
||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
||||
targetDate = premiumAvailableStart;
|
||||
|
||||
if (freeAvailableStart <= oneYearFromNow){
|
||||
targetDate = freeAvailableStart;
|
||||
} else{
|
||||
targetDate = premiumAvailableStart;
|
||||
}
|
||||
}
|
||||
} else{
|
||||
targetDate = premiumAvailableStart;
|
||||
if (targetDate >= oneYearFromNow){
|
||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
||||
|
||||
if (targetDate >= oneYearFromNow){
|
||||
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
|
||||
|
||||
if (freeAvailableStart <= oneYearFromNow){
|
||||
targetDate = freeAvailableStart;
|
||||
} else{
|
||||
targetDate = episodeAirDate;
|
||||
}
|
||||
if (freeAvailableStart <= oneYearFromNow){
|
||||
targetDate = freeAvailableStart;
|
||||
} else{
|
||||
targetDate = episodeAirDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter;
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
|
||||
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Audio)")) &&
|
||||
(string.IsNullOrEmpty(dubFilter) || dubFilter == "none" || (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter))){
|
||||
//|| crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp
|
||||
continue;
|
||||
filtered = true;
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(dubFilter) && dubFilter != "none"){
|
||||
if (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){
|
||||
continue;
|
||||
filtered = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -274,6 +291,12 @@ public class CalendarManager{
|
|||
if (calendarDay != null){
|
||||
CalendarEpisode calEpisode = new CalendarEpisode();
|
||||
|
||||
string? seasonTitle = string.IsNullOrEmpty(crBrowseEpisode.EpisodeMetadata.SeasonTitle)
|
||||
? crBrowseEpisode.EpisodeMetadata.SeriesTitle
|
||||
: Regex.IsMatch(crBrowseEpisode.EpisodeMetadata.SeasonTitle, @"^Season\s+\d+$", RegexOptions.IgnoreCase)
|
||||
? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}"
|
||||
: crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
||||
|
||||
calEpisode.DateTime = targetDate;
|
||||
calEpisode.HasPassed = DateTime.Now > targetDate;
|
||||
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
||||
|
|
@ -282,12 +305,14 @@ public class CalendarManager{
|
|||
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault()?.Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg
|
||||
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
|
||||
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
|
||||
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
||||
calEpisode.SeasonName = seasonTitle;
|
||||
calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode;
|
||||
calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId;
|
||||
calEpisode.FilteredOut = filtered;
|
||||
calEpisode.AudioLocale = crBrowseEpisode.EpisodeMetadata.AudioLocale;
|
||||
|
||||
var existingEpisode = calendarDay.CalendarEpisodes
|
||||
.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName);
|
||||
.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName && e.AudioLocale == calEpisode.AudioLocale);
|
||||
|
||||
if (existingEpisode != null){
|
||||
if (!int.TryParse(existingEpisode.EpisodeNumber, out _)){
|
||||
|
|
@ -330,8 +355,9 @@ public class CalendarManager{
|
|||
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(calendarEpisode => calendarDay.DateTime.Date.Day == calendarEpisode.DateTime.Date.Day)
|
||||
.Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID && ele.SeasonName != calendarEpisode.SeasonName))){
|
||||
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))){
|
||||
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
||||
}
|
||||
}
|
||||
|
|
@ -342,6 +368,7 @@ public class CalendarManager{
|
|||
foreach (var weekCalendarDay in week.CalendarDays){
|
||||
if (weekCalendarDay.CalendarEpisodes.Count > 0)
|
||||
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
|
||||
.Where(e => !e.FilteredOut)
|
||||
.OrderBy(e => e.AnilistEpisode) // False first, then true
|
||||
.ThenBy(e => e.DateTime)
|
||||
.ThenBy(e => e.SeasonName)
|
||||
|
|
@ -417,7 +444,7 @@ public class CalendarManager{
|
|||
aniListResponse ??= currentResponse;
|
||||
|
||||
if (aniListResponse != currentResponse){
|
||||
aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ??[]);
|
||||
aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ?? []);
|
||||
}
|
||||
|
||||
hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false;
|
||||
|
|
@ -426,12 +453,12 @@ public class CalendarManager{
|
|||
} while (hasNextPage && currentPage < 20);
|
||||
|
||||
|
||||
var list = aniListResponse.Data?.Page?.AiringSchedules ??[];
|
||||
var list = aniListResponse.Data?.Page?.AiringSchedules ?? [];
|
||||
|
||||
list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external =>
|
||||
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
|
||||
List<CalendarEpisode> calendarEpisodes =[];
|
||||
List<CalendarEpisode> calendarEpisodes = [];
|
||||
|
||||
foreach (var anilistEle in list){
|
||||
var calEp = new CalendarEpisode();
|
||||
|
|
@ -531,7 +558,7 @@ public class CalendarManager{
|
|||
oldestRelease.Second,
|
||||
calEp.DateTime.Kind
|
||||
);
|
||||
|
||||
|
||||
if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){
|
||||
adjustedDate = oldestRelease.AddDays(7);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ public class CrEpisode(){
|
|||
}
|
||||
|
||||
if (epsidoe is{ Total: 1, Data: not null } &&
|
||||
(epsidoe.Data.First().Versions ??[])
|
||||
(epsidoe.Data.First().Versions ?? [])
|
||||
.GroupBy(v => v.AudioLocale)
|
||||
.Any(g => g.Count() > 1)){
|
||||
Console.Error.WriteLine("Episode has Duplicate Audio Locales");
|
||||
var list = (epsidoe.Data.First().Versions ??[]).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList();
|
||||
var list = (epsidoe.Data.First().Versions ?? []).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList();
|
||||
//guid for episode id
|
||||
foreach (var episodeVersionse in list){
|
||||
foreach (var version in episodeVersionse){
|
||||
|
|
@ -173,7 +173,7 @@ public class CrEpisode(){
|
|||
}
|
||||
|
||||
var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key;
|
||||
var images = (item.Images?.Thumbnail ??[new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
|
||||
var images = (item.Images?.Thumbnail ?? [new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
|
||||
|
||||
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
|
||||
|
||||
|
|
@ -237,60 +237,45 @@ public class CrEpisode(){
|
|||
|
||||
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){
|
||||
await crunInstance.CrAuthEndpoint1.RefreshToken(true);
|
||||
CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase();
|
||||
complete.Data =[];
|
||||
|
||||
var i = 0;
|
||||
|
||||
do{
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
if (string.IsNullOrEmpty(crLocale)){
|
||||
crLocale = "en-US";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
if (forcedLang){
|
||||
query["force_locale"] = crLocale;
|
||||
}
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
if (forcedLang){
|
||||
query["force_locale"] = crLocale;
|
||||
}
|
||||
}
|
||||
|
||||
query["start"] = i + "";
|
||||
query["n"] = "50";
|
||||
query["sort_by"] = "newly_added";
|
||||
query["type"] = "episode";
|
||||
query["n"] = requestAmount + "";
|
||||
query["sort_by"] = "newly_added";
|
||||
query["type"] = "episode";
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query);
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
|
||||
if (series != null){
|
||||
complete.Total = series.Total;
|
||||
if (series.Data != null){
|
||||
complete.Data.AddRange(series.Data);
|
||||
if (firstWeekDay != null){
|
||||
if (firstWeekDay.Value.Date <= series.Data.Last().LastPublic && i + 50 == requestAmount){
|
||||
requestAmount += 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else{
|
||||
break;
|
||||
}
|
||||
series?.Data?.Sort((a, b) =>
|
||||
b.EpisodeMetadata.PremiumAvailableDate.CompareTo(a.EpisodeMetadata.PremiumAvailableDate));
|
||||
|
||||
i += 50;
|
||||
} while (i < requestAmount && requestAmount < 500);
|
||||
|
||||
|
||||
return complete;
|
||||
return series;
|
||||
}
|
||||
|
||||
public async Task MarkAsWatched(string episodeId){
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true, crunInstance.CrAuthEndpoint1.Token?.access_token, null);
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true,
|
||||
crunInstance.CrAuthEndpoint1.Token?.access_token, null);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ public class CrSeries{
|
|||
}
|
||||
|
||||
if (episodeList.Total < 1){
|
||||
Console.Error.WriteLine("Season is empty!");
|
||||
Console.Error.WriteLine($"Season is empty! Uri: {episodeRequest.RequestUri}");
|
||||
}
|
||||
|
||||
return episodeList;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
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
|
||||
}
|
||||
193
CRD/Downloader/Crunchyroll/Utils/SubtitleUtils.cs
Normal file
193
CRD/Downloader/Crunchyroll/Utils/SubtitleUtils.cs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.Crunchyroll;
|
||||
|
||||
namespace CRD.Downloader.Crunchyroll.Utils;
|
||||
|
||||
public static class SubtitleUtils{
|
||||
private static readonly Dictionary<string, string> StyleTemplates = new(){
|
||||
{ "de-DE", "Style: {name},Arial,23,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,{align},0000,0000,0020,1" },
|
||||
{ "ar-SA", "Style: {name},Adobe Arabic,26,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,0,{align},0010,0010,0018,0" },
|
||||
{ "en-US", "Style: {name},Trebuchet MS,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "es-419", "Style: {name},Trebuchet MS,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,1" },
|
||||
{ "es-ES", "Style: {name},Trebuchet MS,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,1" },
|
||||
{ "fr-FR", "Style: {name},Trebuchet MS,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,1,{align},0002,0002,0025,1" },
|
||||
{ "id-ID", "Style: {name},Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H7F404040,-1,0,0,0,100,100,0,0,1,2,1,{align},0020,0020,0022,0" },
|
||||
{ "it-IT", "Style: {name},Trebuchet MS,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,{align},0010,0010,0015,1" },
|
||||
{ "ms-MY", "Style: {name},Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H7F404040,-1,0,0,0,100,100,0,0,1,2,1,{align},0020,0020,0022,0" },
|
||||
{ "pt-BR", "Style: {name},Trebuchet MS,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,2,1,{align},0040,0040,0015,0" },
|
||||
{ "ru-RU", "Style: {name},Tahoma,22,&H00FFFFFF,&H000000FF,&H00000000,&H96000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0025,204" },
|
||||
{ "th-TH", "Style: {name},Noto Sans Thai,30,&H00FFFFFF,&H0000FFFF,&H00000000,&H7F404040,-1,0,0,0,100,100,0,0,1,2,1,{align},0020,0020,0022,0" },
|
||||
{ "vi-VN", "Style: {name},Arial Unicode MS,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H7F404040,-1,0,0,0,100,100,0,0,1,2,1,{align},0020,0020,0022,0" },
|
||||
{ "zh-CN", "Style: {name},Arial Unicode MS,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H7F404040,-1,0,0,0,100,100,0,0,1,2,1,{align},0020,0020,0022,0" },
|
||||
{ "zh-HK", "Style: {name},Arial Unicode MS,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H7F404040,-1,0,0,0,100,100,0,0,1,2,1,{align},0020,0020,0022,0" },
|
||||
|
||||
// Need to check
|
||||
{ "ja-JP", "Style: {name},Arial Unicode MS,23,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "en-IN", "Style: {name},Trebuchet MS,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "pt-PT", "Style: {name},Trebuchet MS,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "pl-PL", "Style: {name},Trebuchet MS,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "ca-ES", "Style: {name},Trebuchet MS,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "tr-TR", "Style: {name},Trebuchet MS,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "hi-IN", "Style: {name},Noto Sans Devanagari,26,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "ta-IN", "Style: {name},Noto Sans Tamil,26,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "te-IN", "Style: {name},Noto Sans Telugu,26,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
{ "zh-TW", "Style: {name},Arial Unicode MS,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H7F404040,-1,0,0,0,100,100,0,0,1,2,1,{align},0020,0020,0022,0" },
|
||||
{ "ko-KR", "Style: {name},Malgun Gothic,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,{align},0010,0010,0018,0" },
|
||||
};
|
||||
|
||||
|
||||
public static string CleanAssAndEnsureScriptInfo(string assText, CrDownloadOptions options, LanguageItem langItem){
|
||||
if (string.IsNullOrEmpty(assText))
|
||||
return assText;
|
||||
|
||||
string? scaledLine = options.SubsAddScaledBorder switch{
|
||||
ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes => "ScaledBorderAndShadow: yes",
|
||||
ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo => "ScaledBorderAndShadow: no",
|
||||
_ => null
|
||||
};
|
||||
|
||||
bool isCcc = assText.Contains("www.closedcaptionconverter.com", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isCcc && options.FixCccSubtitles){
|
||||
assText = Regex.Replace(
|
||||
assText,
|
||||
@"^[ \t]*;[ \t]*Script generated by Closed Caption Converter \| www\.closedcaptionconverter\.com[ \t]*\r?\n",
|
||||
"",
|
||||
RegexOptions.Multiline
|
||||
);
|
||||
|
||||
assText = Regex.Replace(
|
||||
assText,
|
||||
@"^[ \t]*PlayDepth[ \t]*:[ \t]*0[ \t]*\r?\n?",
|
||||
"",
|
||||
RegexOptions.Multiline | RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
assText = assText.Replace(",,,,25.00,,", ",,0,0,0,,");
|
||||
|
||||
assText = FixStyles(assText, langItem.CrLocale);
|
||||
}
|
||||
|
||||
// Remove Aegisub garbage and other useless metadata
|
||||
assText = RemoveAegisubProjectGarbageBlocks(assText);
|
||||
|
||||
// Remove Aegisub-generated comments and YCbCr Matrix lines
|
||||
assText = Regex.Replace(
|
||||
assText,
|
||||
@"^[ \t]*;[^\r\n]*\r?\n?", // all comment lines starting with ';'
|
||||
"",
|
||||
RegexOptions.Multiline
|
||||
);
|
||||
|
||||
assText = Regex.Replace(
|
||||
assText,
|
||||
@"^[ \t]*YCbCr Matrix:[^\r\n]*\r?\n?",
|
||||
"",
|
||||
RegexOptions.Multiline | RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
// Remove empty lines (but keep one between sections)
|
||||
assText = Regex.Replace(assText, @"(\r?\n){3,}", "\r\n\r\n");
|
||||
|
||||
var linesToEnsure = new Dictionary<string, string>();
|
||||
|
||||
if (isCcc){
|
||||
linesToEnsure["PlayResX"] = "PlayResX: 640";
|
||||
linesToEnsure["PlayResY"] = "PlayResY: 360";
|
||||
linesToEnsure["Timer"] = "Timer: 0.0000";
|
||||
linesToEnsure["WrapStyle"] = "WrapStyle: 0";
|
||||
}
|
||||
|
||||
if (scaledLine != null)
|
||||
linesToEnsure["ScaledBorderAndShadow"] = scaledLine;
|
||||
|
||||
if (linesToEnsure.Count > 0)
|
||||
assText = UpsertScriptInfo(assText, linesToEnsure);
|
||||
|
||||
return assText;
|
||||
}
|
||||
|
||||
private static string UpsertScriptInfo(string input, IDictionary<string, string> linesToEnsure){
|
||||
var rxSection = new Regex(@"(?is)(\[Script Info\]\s*\r?\n)(.*?)(?=\r?\n\[|$)");
|
||||
var m = rxSection.Match(input);
|
||||
|
||||
string nl = input.Contains("\r\n") ? "\r\n" : "\n";
|
||||
|
||||
if (!m.Success){
|
||||
// Create whole section at top
|
||||
return "[Script Info]" + nl
|
||||
+ string.Join(nl, linesToEnsure.Values) + nl
|
||||
+ input;
|
||||
}
|
||||
|
||||
string header = m.Groups[1].Value;
|
||||
string body = m.Groups[2].Value;
|
||||
string bodyNl = header.Contains("\r\n") ? "\r\n" : "\n";
|
||||
|
||||
foreach (var kv in linesToEnsure){
|
||||
var lineRx = new Regex($@"(?im)^\s*{Regex.Escape(kv.Key)}\s*:\s*.*$");
|
||||
if (lineRx.IsMatch(body))
|
||||
body = lineRx.Replace(body, kv.Value);
|
||||
else
|
||||
body = body.TrimEnd() + bodyNl + kv.Value + bodyNl;
|
||||
}
|
||||
|
||||
return input.Substring(0, m.Index) + header + body + input.Substring(m.Index + m.Length);
|
||||
}
|
||||
|
||||
private static string FixStyles(string assContent, string crLocale){
|
||||
var pattern = @"^Style:\s*([^,]+),\s*(?:[^,\r\n]*,\s*){17}(\d+)\s*,[^\r\n]*$";
|
||||
|
||||
string template = StyleTemplates.TryGetValue(crLocale, out var tmpl) ? tmpl : StyleTemplates["en-US"];
|
||||
|
||||
return Regex.Replace(assContent, pattern, m => {
|
||||
string name = m.Groups[1].Value;
|
||||
string align = m.Groups[2].Value;
|
||||
|
||||
return template
|
||||
.Replace("{name}", name)
|
||||
.Replace("{align}", align);
|
||||
}, RegexOptions.Multiline);
|
||||
}
|
||||
|
||||
private static string RemoveAegisubProjectGarbageBlocks(string text){
|
||||
if (string.IsNullOrEmpty(text)) return text;
|
||||
|
||||
var nl = "\n";
|
||||
text = text.Replace("\r\n", "\n").Replace("\r", "\n");
|
||||
|
||||
var sb = new System.Text.StringBuilder(text.Length);
|
||||
using var sr = new System.IO.StringReader(text);
|
||||
|
||||
bool skipping = false;
|
||||
string? line;
|
||||
while ((line = sr.ReadLine()) != null){
|
||||
string trimmed = line.Trim();
|
||||
|
||||
if (!skipping && Regex.IsMatch(trimmed, @"^\[\s*Aegisub\s+Project\s+Garbage\s*\]$", RegexOptions.IgnoreCase)){
|
||||
skipping = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (skipping){
|
||||
if (trimmed.Length == 0 || Regex.IsMatch(trimmed, @"^\[.+\]$")){
|
||||
skipping = false;
|
||||
|
||||
if (trimmed.Length != 0){
|
||||
sb.Append(line).Append(nl);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(line).Append(nl);
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd('\n').Replace("\n", "\r\n");
|
||||
}
|
||||
}
|
||||
|
|
@ -35,21 +35,33 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool _downloadAudio = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _downloadDescriptionAudio = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _downloadChapters = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _addScaledBorderAndShadow;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _fixCccSubtitles;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _subsDownloadDuplicate;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _includeSignSubs;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _includeCcSubs;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _convertVtt2Ass;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showVtt2AssSettings;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedScaledBorderAndShadow;
|
||||
|
||||
|
|
@ -63,13 +75,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _muxToMp4;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _muxToMp3;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _muxFonts;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _muxCover;
|
||||
|
||||
|
|
@ -102,7 +114,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private string _fileName = "";
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _fileNameWhitespaceSubstitute = "";
|
||||
|
||||
|
|
@ -127,6 +139,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedHSLang;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hsRawFallback;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedDescriptionLang;
|
||||
|
||||
|
|
@ -138,22 +153,37 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedStreamEndpoint;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _firstEndpointVideo;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _firstEndpointAudio;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _SelectedStreamEndpointSecondary;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _endpointAuthorization = "";
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _endpointUserAgent = "";
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _endpointDeviceName = "";
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _endpointDeviceType = "";
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _endpointVideo;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _endpointAudio;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoggingIn;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _endpointNotSignedWarning;
|
||||
|
||||
|
|
@ -170,7 +200,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
private ComboBoxItem? _selectedAudioQuality;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<ListBoxItem> _selectedSubLang =[];
|
||||
private ObservableCollection<ListBoxItem> _selectedSubLang = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private Color _listBoxColor;
|
||||
|
|
@ -187,6 +217,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } =[
|
||||
new(){ Content = "best" },
|
||||
new(){ Content = "192kB/s" },
|
||||
new(){ Content = "128kB/s" },
|
||||
new(){ Content = "96kB/s" },
|
||||
new(){ Content = "64kB/s" },
|
||||
|
|
@ -212,12 +243,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
new(){ Content = "ar-SA" }
|
||||
];
|
||||
|
||||
public ObservableCollection<ListBoxItem> DubLangList{ get; } =[];
|
||||
public ObservableCollection<ListBoxItem> DubLangList{ get; } = [];
|
||||
|
||||
|
||||
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } =[];
|
||||
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = [];
|
||||
|
||||
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } =[];
|
||||
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = [];
|
||||
|
||||
|
||||
public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
|
||||
|
|
@ -240,7 +271,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
new(){ Content = "tv/vidaa" },
|
||||
new(){ Content = "tv/android_tv" },
|
||||
];
|
||||
|
||||
|
||||
public ObservableCollection<ComboBoxItem> StreamEndpointsSecondary{ get; } =[
|
||||
new(){ Content = "" },
|
||||
// new(){ Content = "web/firefox" },
|
||||
|
|
@ -258,7 +289,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
new(){ Content = "tv/android_tv" },
|
||||
];
|
||||
|
||||
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
|
||||
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private StringItemWithDisplayName _selectedFFmpegHWAccel;
|
||||
|
|
@ -326,23 +357,35 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
|
||||
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
|
||||
|
||||
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
|
||||
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint?.Endpoint ?? "")) ?? null;
|
||||
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
|
||||
|
||||
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
|
||||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||
|
||||
|
||||
EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty;
|
||||
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
|
||||
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
|
||||
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty;
|
||||
|
||||
EndpointVideo = options.StreamEndpointSecondSettings?.Video ?? true;
|
||||
EndpointAudio = options.StreamEndpointSecondSettings?.Audio ?? true;
|
||||
|
||||
FirstEndpointVideo = options.StreamEndpoint?.Video ?? true;
|
||||
FirstEndpointAudio = options.StreamEndpoint?.Audio ?? true;
|
||||
|
||||
if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
|
||||
EndpointNotSignedWarning = true;
|
||||
}
|
||||
|
||||
|
||||
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||
|
||||
if (FFmpegHWAccel.Count == 0){
|
||||
FFmpegHWAccel.Add(new StringItemWithDisplayName{
|
||||
DisplayName = "No hardware acceleration (error)",
|
||||
value = "error"
|
||||
});
|
||||
}
|
||||
|
||||
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
||||
SelectedFFmpegHWAccel = hwAccellFlag ?? FFmpegHWAccel[0];
|
||||
|
||||
|
|
@ -351,7 +394,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
.Where(a => options.DlSubs.Contains(a.Content))
|
||||
.OrderBy(a => options.DlSubs.IndexOf(a.Content))
|
||||
.ToList();
|
||||
|
||||
|
||||
SelectedSubLang.Clear();
|
||||
foreach (var listBoxItem in softSubLang){
|
||||
SelectedSubLang.Add(listBoxItem);
|
||||
|
|
@ -361,7 +404,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
.Where(a => options.DubLang.Contains(a.Content))
|
||||
.OrderBy(a => options.DubLang.IndexOf(a.Content))
|
||||
.ToList();
|
||||
|
||||
|
||||
SelectedDubLang.Clear();
|
||||
foreach (var listBoxItem in dubLang){
|
||||
SelectedDubLang.Add(listBoxItem);
|
||||
|
|
@ -370,6 +413,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
|
||||
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
|
||||
|
||||
HsRawFallback = options.HsRawFallback;
|
||||
FixCccSubtitles = options.FixCccSubtitles;
|
||||
ConvertVtt2Ass = options.ConvertVtt2Ass;
|
||||
SubsDownloadDuplicate = options.SubsDownloadDuplicate;
|
||||
MarkAsWatched = options.MarkAsWatched;
|
||||
DownloadFirstAvailableDub = options.DownloadFirstAvailableDub;
|
||||
|
|
@ -388,6 +434,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
IncludeCcSubs = options.IncludeCcSubs;
|
||||
DownloadVideo = !options.Novids;
|
||||
DownloadAudio = !options.Noaudio;
|
||||
DownloadDescriptionAudio = options.DownloadDescriptionAudio;
|
||||
DownloadVideoForEveryDub = !options.DlVideoOnce;
|
||||
KeepDubsSeparate = options.KeepDubsSeperate;
|
||||
DownloadChapters = options.Chapters;
|
||||
|
|
@ -428,6 +475,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
|
||||
SelectedSubs = string.Join(", ", subs) ?? "";
|
||||
|
||||
ShowVtt2AssSettings = IncludeCcSubs && ConvertVtt2Ass;
|
||||
|
||||
SelectedSubLang.CollectionChanged += Changes;
|
||||
SelectedDubLang.CollectionChanged += Changes;
|
||||
|
||||
|
|
@ -443,6 +492,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.SubsDownloadDuplicate = SubsDownloadDuplicate;
|
||||
CrunchyrollManager.Instance.CrunOptions.ConvertVtt2Ass = ConvertVtt2Ass;
|
||||
CrunchyrollManager.Instance.CrunOptions.FixCccSubtitles = FixCccSubtitles;
|
||||
CrunchyrollManager.Instance.CrunOptions.MarkAsWatched = MarkAsWatched;
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub = DownloadFirstAvailableDub;
|
||||
CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi = UseCrBetaApi;
|
||||
|
|
@ -457,6 +508,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
CrunchyrollManager.Instance.CrunOptions.VideoTitle = FileTitle;
|
||||
CrunchyrollManager.Instance.CrunOptions.Novids = !DownloadVideo;
|
||||
CrunchyrollManager.Instance.CrunOptions.Noaudio = !DownloadAudio;
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadDescriptionAudio = DownloadDescriptionAudio;
|
||||
CrunchyrollManager.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub;
|
||||
CrunchyrollManager.Instance.CrunOptions.KeepDubsSeperate = KeepDubsSeparate;
|
||||
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
|
||||
|
|
@ -489,14 +541,18 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
string descLang = SelectedDescriptionLang.Content + "";
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
|
||||
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
|
||||
CrunchyrollManager.Instance.CrunOptions.HsRawFallback = HsRawFallback;
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
|
||||
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
|
||||
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
|
||||
var endpointSettingsFirst = new CrAuthSettings();
|
||||
endpointSettingsFirst.Endpoint = SelectedStreamEndpoint.Content + "";
|
||||
endpointSettingsFirst.Video = FirstEndpointVideo;
|
||||
endpointSettingsFirst.Audio = FirstEndpointAudio;
|
||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = endpointSettingsFirst;
|
||||
|
||||
var endpointSettings = new CrAuthSettings();
|
||||
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
|
||||
|
|
@ -504,9 +560,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
endpointSettings.UserAgent = EndpointUserAgent;
|
||||
endpointSettings.Device_name = EndpointDeviceName;
|
||||
endpointSettings.Device_type = EndpointDeviceType;
|
||||
|
||||
|
||||
endpointSettings.Video = EndpointVideo;
|
||||
endpointSettings.Audio = EndpointAudio;
|
||||
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
|
||||
CrunchyrollManager.Instance.CrAuthEndpoint2.AuthSettings = endpointSettings;
|
||||
|
||||
|
||||
List<string> dubLangs = new List<string>();
|
||||
foreach (var listBoxItem in SelectedDubLang){
|
||||
|
|
@ -609,6 +669,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
UpdateSettings();
|
||||
ShowVtt2AssSettings = IncludeCcSubs && ConvertVtt2Ass;
|
||||
|
||||
if (e.PropertyName is nameof(History)){
|
||||
if (CrunchyrollManager.Instance.CrunOptions.History){
|
||||
|
|
@ -625,13 +686,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
} else{
|
||||
CrunchyrollManager.Instance.HistoryList =[];
|
||||
CrunchyrollManager.Instance.HistoryList = [];
|
||||
}
|
||||
}
|
||||
|
||||
_ = SonarrClient.Instance.RefreshSonarrLite();
|
||||
} else{
|
||||
CrunchyrollManager.Instance.HistoryList =[];
|
||||
CrunchyrollManager.Instance.HistoryList = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -669,13 +730,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
public void ResetEndpointSettings(){
|
||||
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == ("android/phone")) ?? null;
|
||||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||
|
||||
EndpointAuthorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=";
|
||||
EndpointUserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0";
|
||||
EndpointDeviceName = "CPH2449";
|
||||
EndpointDeviceType = "OnePlus CPH2449";
|
||||
|
||||
EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization;
|
||||
EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent;
|
||||
EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name;
|
||||
EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type;
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public async Task Login(){
|
||||
var dialog = new ContentDialog(){
|
||||
|
|
@ -690,9 +751,10 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
};
|
||||
|
||||
_ = await dialog.ShowAsync();
|
||||
|
||||
IsLoggingIn = true;
|
||||
await viewModel.LoginCompleted;
|
||||
IsLoggingIn = false;
|
||||
EndpointNotSignedWarning = CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???";
|
||||
|
||||
}
|
||||
|
||||
private List<StringItemWithDisplayName> GetAvailableHWAccelOptions(){
|
||||
|
|
@ -706,7 +768,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
process.StartInfo.CreateNoWindow = true;
|
||||
|
||||
string output = string.Empty;
|
||||
|
||||
|
||||
process.OutputDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data)){
|
||||
output += e.Data + Environment.NewLine;
|
||||
|
|
@ -714,7 +776,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
// process.BeginErrorReadLine();
|
||||
|
||||
|
|
@ -725,11 +787,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
return MapHWAccelOptions(accels);
|
||||
}
|
||||
} catch (Exception e){
|
||||
Console.WriteLine("Failed to get Available HW Accel Options" + e);
|
||||
Console.Error.WriteLine("Failed to get Available HW Accel Options" + e);
|
||||
}
|
||||
|
||||
var result = new List<StringItemWithDisplayName>();
|
||||
result.Add(new StringItemWithDisplayName{
|
||||
DisplayName = "No hardware acceleration / error",
|
||||
value = "error"
|
||||
});
|
||||
|
||||
return[];
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@
|
|||
|
||||
</controls:SettingsExpander.Footer>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Download AD for Selected Dubs" Description="Downloads audio description tracks matching the selected dub languages (if available).">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding DownloadDescriptionAudio}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
</controls:SettingsExpander>
|
||||
|
||||
|
||||
|
|
@ -69,6 +75,12 @@
|
|||
</ComboBox>
|
||||
</controls:SettingsExpander.Footer>
|
||||
|
||||
<controls:SettingsExpanderItem Content="No hardsubs fallback" Description="If no hardsubs are available, automatically download the no-hardsub (raw) video.">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding HsRawFallback}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
</controls:SettingsExpander>
|
||||
|
||||
|
||||
|
|
@ -105,6 +117,12 @@
|
|||
</StackPanel>
|
||||
</controls:SettingsExpander.Footer>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Fix Ccc Subtitles" Description="Automatically adjusts subtitle styles that were created with ClosedCaptionConverter">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding FixCccSubtitles}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Download Duplicate" Description="Download subtitles from all dubs where they're available">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding SubsDownloadDuplicate}"> </CheckBox>
|
||||
|
|
@ -187,7 +205,13 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding IncludeCcSubs}" Content="CC Subtitles" Description="Font">
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding IncludeCcSubs}" Content="Convert CC Subtitles to ASS" Description="When enabled, closed-caption WEBVTT subtitles are converted into ASS format">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding ConvertVtt2Ass}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding ShowVtt2AssSettings}" Content="CC Subtitles" Description="Font">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||
Text="{Binding CCSubsFont}" />
|
||||
|
|
@ -231,12 +255,27 @@
|
|||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Stream Endpoint " IsEnabled="False">
|
||||
<controls:SettingsExpanderItem Content="Stream Endpoint ">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
||||
ItemsSource="{Binding StreamEndpoints}"
|
||||
SelectedItem="{Binding SelectedStreamEndpoint}">
|
||||
</ComboBox>
|
||||
<StackPanel>
|
||||
<ComboBox IsEnabled="False" HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
||||
ItemsSource="{Binding StreamEndpoints}"
|
||||
SelectedItem="{Binding SelectedStreamEndpoint}">
|
||||
</ComboBox>
|
||||
|
||||
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||
IsChecked="{Binding FirstEndpointVideo}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||
IsChecked="{Binding FirstEndpointAudio}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
|
@ -248,27 +287,38 @@
|
|||
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
|
||||
</ComboBox>
|
||||
|
||||
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||
IsChecked="{Binding EndpointVideo}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
|
||||
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
|
||||
IsChecked="{Binding EndpointAudio}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5">
|
||||
<TextBlock Text="Authorization" />
|
||||
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250"
|
||||
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||
Text="{Binding EndpointAuthorization}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5">
|
||||
<TextBlock Text="User Agent" />
|
||||
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250"
|
||||
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||
Text="{Binding EndpointUserAgent}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5">
|
||||
<TextBlock Text="Device Type" />
|
||||
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250"
|
||||
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||
Text="{Binding EndpointDeviceType}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5">
|
||||
<TextBlock Text="Device Name" />
|
||||
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250"
|
||||
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
|
||||
Text="{Binding EndpointDeviceName}" />
|
||||
</StackPanel>
|
||||
|
||||
|
|
@ -286,6 +336,12 @@
|
|||
<TextBlock Text="Login" HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="12" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<controls:ProgressRing Width="24" Height="24"
|
||||
Margin="8,0,0,0"
|
||||
IsActive="{Binding IsLoggingIn}"
|
||||
IsVisible="{Binding IsLoggingIn}" />
|
||||
|
||||
<controls:SymbolIcon Symbol="CloudOff"
|
||||
IsVisible="{Binding EndpointNotSignedWarning}"
|
||||
Foreground="OrangeRed"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ public partial class QueueManager : ObservableObject{
|
|||
|
||||
public int ActiveDownloads => Volatile.Read(ref activeDownloads);
|
||||
|
||||
public readonly SemaphoreSlim activeProcessingJobs = new SemaphoreSlim(initialCount: CrunchyrollManager.Instance.CrunOptions.SimultaneousProcessingJobs, maxCount: int.MaxValue);
|
||||
private int _limit = CrunchyrollManager.Instance.CrunOptions.SimultaneousProcessingJobs;
|
||||
private int _borrowed = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
[ObservableProperty]
|
||||
|
|
@ -60,6 +64,10 @@ public partial class QueueManager : ObservableObject{
|
|||
Interlocked.Increment(ref activeDownloads);
|
||||
}
|
||||
|
||||
public void ResetDownloads(){
|
||||
Interlocked.Exchange(ref activeDownloads, 0);
|
||||
}
|
||||
|
||||
public void DecrementDownloads(){
|
||||
while (true){
|
||||
int current = Volatile.Read(ref activeDownloads);
|
||||
|
|
@ -69,7 +77,6 @@ public partial class QueueManager : ObservableObject{
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){
|
||||
if (e.Action == NotifyCollectionChangedAction.Remove){
|
||||
|
|
@ -315,7 +322,7 @@ public partial class QueueManager : ObservableObject{
|
|||
MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1));
|
||||
}
|
||||
|
||||
public async Task CrAddMusicVideoToQueue(string epId){
|
||||
public async Task CrAddMusicVideoToQueue(string epId, string overrideDownloadPath = ""){
|
||||
await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true);
|
||||
|
||||
var musicVideo = await CrunchyrollManager.Instance.CrMusic.ParseMusicVideoByIdAsync(epId, "");
|
||||
|
|
@ -329,6 +336,7 @@ public partial class QueueManager : ObservableObject{
|
|||
historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(musicVideoMeta.SeriesId, musicVideoMeta.SeasonId, musicVideoMeta.Data.First().MediaId);
|
||||
}
|
||||
|
||||
musicVideoMeta.DownloadPath = !string.IsNullOrEmpty(overrideDownloadPath) ? overrideDownloadPath : (!string.IsNullOrEmpty(historyEpisode.downloadDirPath) ? historyEpisode.downloadDirPath : "");
|
||||
musicVideoMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
|
||||
|
||||
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
||||
|
|
@ -339,7 +347,7 @@ public partial class QueueManager : ObservableObject{
|
|||
}
|
||||
}
|
||||
|
||||
public async Task CrAddConcertToQueue(string epId){
|
||||
public async Task CrAddConcertToQueue(string epId, string overrideDownloadPath = ""){
|
||||
await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true);
|
||||
|
||||
var concert = await CrunchyrollManager.Instance.CrMusic.ParseConcertByIdAsync(epId, "");
|
||||
|
|
@ -353,6 +361,7 @@ public partial class QueueManager : ObservableObject{
|
|||
historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(concertMeta.SeriesId, concertMeta.SeasonId, concertMeta.Data.First().MediaId);
|
||||
}
|
||||
|
||||
concertMeta.DownloadPath = !string.IsNullOrEmpty(overrideDownloadPath) ? overrideDownloadPath : (!string.IsNullOrEmpty(historyEpisode.downloadDirPath) ? historyEpisode.downloadDirPath : "");
|
||||
concertMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
|
||||
|
||||
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
||||
|
|
@ -438,7 +447,7 @@ public partial class QueueManager : ObservableObject{
|
|||
crunchyEpMeta.HighlightAllAvailable = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Queue.Add(crunchyEpMeta);
|
||||
|
||||
|
|
@ -467,4 +476,32 @@ public partial class QueueManager : ObservableObject{
|
|||
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode(s) to the queue with current dub settings", ToastType.Error, 2));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLimit(int newLimit){
|
||||
lock (activeProcessingJobs){
|
||||
if (newLimit == _limit) return;
|
||||
|
||||
if (newLimit > _limit){
|
||||
int giveBack = Math.Min(_borrowed, newLimit - _limit);
|
||||
if (giveBack > 0){
|
||||
activeProcessingJobs.Release(giveBack);
|
||||
_borrowed -= giveBack;
|
||||
}
|
||||
|
||||
int more = newLimit - _limit - giveBack;
|
||||
if (more > 0) activeProcessingJobs.Release(more);
|
||||
} else{
|
||||
int toPark = _limit - newLimit;
|
||||
|
||||
for (int i = 0; i < toPark; i++){
|
||||
_ = Task.Run(async () => {
|
||||
await activeProcessingJobs.WaitAsync().ConfigureAwait(false);
|
||||
Interlocked.Increment(ref _borrowed);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_limit = newLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using Avalonia;
|
||||
using System.Linq;
|
||||
using ReactiveUI.Avalonia;
|
||||
|
||||
namespace CRD;
|
||||
|
||||
|
|
@ -26,7 +27,8 @@ sealed class Program{
|
|||
var builder = AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
.LogToTrace()
|
||||
.UseReactiveUI() ;
|
||||
|
||||
if (isHeadless){
|
||||
Console.WriteLine("Running in headless mode...");
|
||||
|
|
|
|||
|
|
@ -173,6 +173,9 @@ public enum DownloadMediaType{
|
|||
|
||||
[EnumMember(Value = "Audio")]
|
||||
Audio,
|
||||
|
||||
[EnumMember(Value = "AudioRoleDescription")]
|
||||
AudioRoleDescription,
|
||||
|
||||
[EnumMember(Value = "Chapters")]
|
||||
Chapters,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
|
@ -11,7 +16,7 @@ namespace CRD.Utils.Files;
|
|||
|
||||
public class CfgManager{
|
||||
private static string workingDirectory = AppContext.BaseDirectory;
|
||||
|
||||
|
||||
public static readonly string PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json");
|
||||
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
|
||||
|
||||
|
|
@ -182,30 +187,105 @@ public class CfgManager{
|
|||
WriteJsonToFileCompressed(PathCrHistory, CrunchyrollManager.Instance.HistoryList);
|
||||
}
|
||||
|
||||
private static object fileLock = new object();
|
||||
private static readonly ConcurrentDictionary<string, object> _pathLocks =
|
||||
new(OperatingSystem.IsWindows()
|
||||
? StringComparer.OrdinalIgnoreCase
|
||||
: StringComparer.Ordinal);
|
||||
|
||||
public static void WriteJsonToFileCompressed(string pathToFile, object obj){
|
||||
try{
|
||||
// Check if the directory exists; if not, create it.
|
||||
string directoryPath = Path.GetDirectoryName(pathToFile);
|
||||
if (!Directory.Exists(directoryPath)){
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
public static void WriteJsonToFileCompressed(string pathToFile, object obj, int keepBackups = 5){
|
||||
string? directoryPath = Path.GetDirectoryName(pathToFile);
|
||||
if (string.IsNullOrEmpty(directoryPath))
|
||||
directoryPath = Environment.CurrentDirectory;
|
||||
|
||||
lock (fileLock){
|
||||
using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write))
|
||||
using (var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal))
|
||||
using (var streamWriter = new StreamWriter(gzipStream))
|
||||
using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
|
||||
string key = Path.GetFullPath(pathToFile);
|
||||
object gate = _pathLocks.GetOrAdd(key, _ => new object());
|
||||
|
||||
lock (gate){
|
||||
string tmp = Path.Combine(
|
||||
directoryPath,
|
||||
"." + Path.GetFileName(pathToFile) + "." + Guid.NewGuid().ToString("N") + ".tmp");
|
||||
|
||||
try{
|
||||
var fso = new FileStreamOptions{
|
||||
Mode = FileMode.CreateNew,
|
||||
Access = FileAccess.Write,
|
||||
Share = FileShare.None,
|
||||
BufferSize = 64 * 1024,
|
||||
Options = FileOptions.WriteThrough
|
||||
};
|
||||
|
||||
using (var fs = new FileStream(tmp, fso))
|
||||
using (var gzip = new GZipStream(fs, CompressionLevel.Optimal, leaveOpen: false))
|
||||
using (var sw = new StreamWriter(gzip))
|
||||
using (var jw = new JsonTextWriter(sw){ Formatting = Formatting.Indented }){
|
||||
var serializer = new JsonSerializer();
|
||||
serializer.Serialize(jsonWriter, obj);
|
||||
serializer.Serialize(jw, obj);
|
||||
}
|
||||
|
||||
if (File.Exists(pathToFile)){
|
||||
string backupPath = GetDailyBackupPath(pathToFile, DateTime.Today);
|
||||
File.Replace(tmp, pathToFile, backupPath, ignoreMetadataErrors: true);
|
||||
|
||||
PruneBackups(pathToFile, keepBackups);
|
||||
} else{
|
||||
File.Move(tmp, pathToFile, overwrite: true);
|
||||
}
|
||||
} catch (Exception ex){
|
||||
try{
|
||||
if (File.Exists(tmp)) File.Delete(tmp);
|
||||
} catch{
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"An error occurred writing {pathToFile}: {ex.Message}");
|
||||
}
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"An error occurred: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDailyBackupPath(string pathToFile, DateTime date){
|
||||
string dir = Path.GetDirectoryName(pathToFile)!;
|
||||
string name = Path.GetFileName(pathToFile);
|
||||
string backupName = $".{name}.{date:yyyy-MM-dd}.bak";
|
||||
return Path.Combine(dir, backupName);
|
||||
}
|
||||
|
||||
private static void PruneBackups(string pathToFile, int keep){
|
||||
string dir = Path.GetDirectoryName(pathToFile)!;
|
||||
string name = Path.GetFileName(pathToFile);
|
||||
|
||||
// Backups: .<name>.YYYY-MM-DD.bak
|
||||
string glob = $".{name}.*.bak";
|
||||
var rx = new Regex(@"^\." + Regex.Escape(name) + @"\.(\d{4}-\d{2}-\d{2})\.bak$", RegexOptions.CultureInvariant);
|
||||
|
||||
var datedBackups = new List<(string Path, DateTime Date)>();
|
||||
foreach (var path in Directory.EnumerateFiles(dir, glob, SearchOption.TopDirectoryOnly)){
|
||||
string file = Path.GetFileName(path);
|
||||
var m = rx.Match(file);
|
||||
if (!m.Success) continue;
|
||||
|
||||
if (DateTime.TryParseExact(m.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None, out var d)){
|
||||
datedBackups.Add((path, d));
|
||||
}
|
||||
}
|
||||
|
||||
// Newest first
|
||||
foreach (var old in datedBackups
|
||||
.OrderByDescending(x => x.Date)
|
||||
.Skip(Math.Max(keep, 0))){
|
||||
try{
|
||||
File.Delete(old.Path);
|
||||
} catch(Exception ex){
|
||||
Console.Error.WriteLine("[Backup] - Failed to delete old backups: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static object fileLock = new object();
|
||||
|
||||
public static void WriteJsonToFile(string pathToFile, object obj){
|
||||
try{
|
||||
// Check if the directory exists; if not, create it.
|
||||
|
|
@ -227,53 +307,45 @@ public class CfgManager{
|
|||
}
|
||||
}
|
||||
|
||||
public static string DecompressJsonFile(string pathToFile){
|
||||
public static string? DecompressJsonFile(string pathToFile){
|
||||
try{
|
||||
using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read)){
|
||||
// Check if the file is compressed
|
||||
if (IsFileCompressed(fileStream)){
|
||||
// Reset the stream position to the beginning
|
||||
fileStream.Position = 0;
|
||||
using (var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress))
|
||||
using (var streamReader = new StreamReader(gzipStream)){
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
var fso = new FileStreamOptions{
|
||||
Mode = FileMode.Open,
|
||||
Access = FileAccess.Read,
|
||||
Share = FileShare.ReadWrite | FileShare.Delete,
|
||||
Options = FileOptions.SequentialScan
|
||||
};
|
||||
|
||||
// If not compressed, read the file as is
|
||||
fileStream.Position = 0;
|
||||
using (var streamReader = new StreamReader(fileStream)){
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
using var fs = new FileStream(pathToFile, fso);
|
||||
|
||||
Span<byte> hdr = stackalloc byte[2];
|
||||
int read = fs.Read(hdr);
|
||||
fs.Position = 0;
|
||||
|
||||
bool looksGzip = read >= 2 && hdr[0] == 0x1F && hdr[1] == 0x8B;
|
||||
|
||||
if (looksGzip){
|
||||
using var gzip = new GZipStream(fs, CompressionMode.Decompress, leaveOpen: false);
|
||||
using var sr = new StreamReader(gzip, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
return sr.ReadToEnd();
|
||||
} else{
|
||||
using var sr = new StreamReader(fs, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
return sr.ReadToEnd();
|
||||
}
|
||||
} catch (FileNotFoundException){
|
||||
return null;
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"An error occurred: {ex.Message}");
|
||||
Console.Error.WriteLine($"Read failed for {pathToFile}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFileCompressed(FileStream fileStream){
|
||||
// Check the first two bytes for the GZip header
|
||||
var buffer = new byte[2];
|
||||
fileStream.Read(buffer, 0, 2);
|
||||
return buffer[0] == 0x1F && buffer[1] == 0x8B;
|
||||
}
|
||||
|
||||
|
||||
public static bool CheckIfFileExists(string filePath){
|
||||
string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;
|
||||
|
||||
return Directory.Exists(dirPath) && File.Exists(filePath);
|
||||
}
|
||||
|
||||
// public static T DeserializeFromFile<T>(string filePath){
|
||||
// var deserializer = new DeserializerBuilder()
|
||||
// .Build();
|
||||
//
|
||||
// using (var reader = new StreamReader(filePath)){
|
||||
// return deserializer.Deserialize<T>(reader);
|
||||
// }
|
||||
// }
|
||||
|
||||
public static T? ReadJsonFromFile<T>(string pathToFile) where T : class{
|
||||
try{
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ public class Helpers{
|
|||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
foreach (var property in originalRequest.Properties){
|
||||
clone.Properties.Add(property);
|
||||
foreach (var kvp in originalRequest.Options){
|
||||
var key = new HttpRequestOptionsKey<object?>(kvp.Key);
|
||||
clone.Options.Set(key, kvp.Value);
|
||||
}
|
||||
|
||||
return clone;
|
||||
|
|
@ -71,15 +72,33 @@ public class Helpers{
|
|||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
|
||||
public static int ToKbps(int bps) => (int)Math.Round(bps / 1000.0);
|
||||
|
||||
public static string ConvertTimeFormat(string time){
|
||||
var timeParts = time.Split(':', '.');
|
||||
int hours = int.Parse(timeParts[0]);
|
||||
int minutes = int.Parse(timeParts[1]);
|
||||
int seconds = int.Parse(timeParts[2]);
|
||||
int milliseconds = int.Parse(timeParts[3]);
|
||||
public static int SnapToAudioBucket(int kbps){
|
||||
int[] buckets = { 64, 96, 128, 192, 256 };
|
||||
return buckets.OrderBy(b => Math.Abs(b - kbps)).First();
|
||||
}
|
||||
|
||||
return $"{hours}:{minutes:D2}:{seconds:D2}.{milliseconds / 10:D2}";
|
||||
public static int WidthBucket(int width, int height){
|
||||
int expected = (int)Math.Round(height * 16 / 9.0);
|
||||
int tol = Math.Max(8, (int)(expected * 0.02)); // ~2% or ≥8 px
|
||||
return Math.Abs(width - expected) <= tol ? expected : width;
|
||||
}
|
||||
|
||||
public static string ConvertTimeFormat(string vttTime){
|
||||
if (TimeSpan.TryParseExact(vttTime, @"hh\:mm\:ss\.fff", null, out var ts) ||
|
||||
TimeSpan.TryParseExact(vttTime, @"mm\:ss\.fff", null, out ts)){
|
||||
var totalCentiseconds = (int)Math.Round(ts.TotalMilliseconds / 10.0, MidpointRounding.AwayFromZero);
|
||||
var hours = totalCentiseconds / 360000; // 100 cs * 60 * 60
|
||||
var rem = totalCentiseconds % 360000;
|
||||
var mins = rem / 6000;
|
||||
rem %= 6000;
|
||||
var secs = rem / 100;
|
||||
var cs = rem % 100;
|
||||
return $"{hours}:{mins:00}:{secs:00}.{cs:00}";
|
||||
}
|
||||
|
||||
return "0:00:00.00";
|
||||
}
|
||||
|
||||
public static string ConvertVTTStylesToASS(string dialogue){
|
||||
|
|
@ -391,6 +410,7 @@ public class Helpers{
|
|||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
process.OutputDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data)){
|
||||
|
|
@ -411,7 +431,28 @@ public class Helpers{
|
|||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
using var reg = data?.Cts.Token.Register(() => {
|
||||
try{
|
||||
if (!process.HasExited)
|
||||
process.Kill(true);
|
||||
} catch{
|
||||
// ignored
|
||||
}
|
||||
});
|
||||
|
||||
try{
|
||||
await process.WaitForExitAsync(data.Cts.Token);
|
||||
} catch (OperationCanceledException){
|
||||
if (File.Exists(tempOutputFilePath)){
|
||||
try{
|
||||
File.Delete(tempOutputFilePath);
|
||||
} catch{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return (IsOk: false, ErrorCode: -2);
|
||||
}
|
||||
|
||||
bool isSuccess = process.ExitCode == 0;
|
||||
|
||||
|
|
@ -423,7 +464,14 @@ public class Helpers{
|
|||
File.Move(tempOutputFilePath, inputFilePath);
|
||||
} else{
|
||||
// If something went wrong, delete the temporary output file
|
||||
File.Delete(tempOutputFilePath);
|
||||
if (File.Exists(tempOutputFilePath)){
|
||||
try{
|
||||
File.Delete(tempOutputFilePath);
|
||||
} catch{
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("FFmpeg processing failed.");
|
||||
Console.Error.WriteLine($"Command: {ffmpegCommand}");
|
||||
}
|
||||
|
|
@ -509,7 +557,7 @@ public class Helpers{
|
|||
return CosineSimilarity(vector1, vector2);
|
||||
}
|
||||
|
||||
private static readonly char[] Delimiters ={ ' ', ',', '.', ';', ':', '-', '_', '\'' };
|
||||
private static readonly char[] Delimiters = { ' ', ',', '.', ';', ':', '-', '_', '\'' };
|
||||
|
||||
public static Dictionary<string, double> ComputeWordFrequency(string text){
|
||||
var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -572,6 +620,12 @@ public class Helpers{
|
|||
string cNumber = match.Groups[2].Value; // Extract the C number if present
|
||||
string pNumber = match.Groups[3].Value; // Extract the P number if present
|
||||
|
||||
if (int.TryParse(sNumber, out int sNumericBig)){
|
||||
// Reject invalid S numbers (>= 1000)
|
||||
if (sNumericBig >= 1000)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(cNumber)){
|
||||
// Case for C: Return S + . + C
|
||||
return $"{sNumber}.{cNumber}";
|
||||
|
|
@ -645,10 +699,10 @@ public class Helpers{
|
|||
group.Add(descriptionMedia[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Find and add Cover media to each group
|
||||
var coverMedia = allMedia.Where(media => media.Type == DownloadMediaType.Cover).ToList();
|
||||
|
||||
|
||||
if (coverMedia.Count > 0){
|
||||
foreach (var group in languageGroups.Values){
|
||||
group.Add(coverMedia[0]);
|
||||
|
|
@ -666,7 +720,7 @@ public class Helpers{
|
|||
bool isValid = !folderName.Any(c => invalidChars.Contains(c));
|
||||
|
||||
// Check for reserved names on Windows
|
||||
string[] reservedNames =["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
|
||||
string[] reservedNames = ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
|
||||
bool isReservedName = reservedNames.Contains(folderName.ToUpperInvariant());
|
||||
|
||||
if (isValid && !isReservedName && folderName.Length <= 255){
|
||||
|
|
@ -793,30 +847,46 @@ public class Helpers{
|
|||
}
|
||||
}
|
||||
|
||||
public static void MergePlaylistData(
|
||||
Dictionary<string, ServerData> target,
|
||||
Dictionary<string, ServerData> source){
|
||||
foreach (var kvp in source){
|
||||
if (target.TryGetValue(kvp.Key, out var existing)){
|
||||
// Merge audio
|
||||
existing.audio ??=[];
|
||||
if (kvp.Value.audio != null)
|
||||
existing.audio.AddRange(kvp.Value.audio);
|
||||
|
||||
// Merge video
|
||||
existing.video ??=[];
|
||||
if (kvp.Value.video != null)
|
||||
existing.video.AddRange(kvp.Value.video);
|
||||
} else{
|
||||
// Add new entry (clone lists to avoid reference issues)
|
||||
target[kvp.Key] = new ServerData{
|
||||
audio = kvp.Value.audio != null ? new List<AudioPlaylist>(kvp.Value.audio) : new List<AudioPlaylist>(),
|
||||
video = kvp.Value.video != null ? new List<VideoPlaylist>(kvp.Value.video) : new List<VideoPlaylist>()
|
||||
};
|
||||
public static void MergePlaylistData(
|
||||
ServerData target,
|
||||
Dictionary<string, ServerData> source,
|
||||
bool mergeAudio,
|
||||
bool mergeVideo){
|
||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
|
||||
var serverSet = new HashSet<string>(target.servers);
|
||||
|
||||
void AddServer(string s){
|
||||
if (!string.IsNullOrWhiteSpace(s) && serverSet.Add(s))
|
||||
target.servers.Add(s);
|
||||
}
|
||||
|
||||
foreach (var kvp in source){
|
||||
var key = kvp.Key;
|
||||
var src = kvp.Value;
|
||||
|
||||
if (!src.servers.Contains(key))
|
||||
src.servers.Add(key);
|
||||
|
||||
AddServer(key);
|
||||
foreach (var s in src.servers)
|
||||
AddServer(s);
|
||||
|
||||
if (mergeAudio && src.audio != null){
|
||||
target.audio ??= [];
|
||||
target.audio.AddRange(src.audio);
|
||||
}
|
||||
|
||||
if (mergeVideo && src.video != null){
|
||||
target.video ??= [];
|
||||
target.video.AddRange(src.video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static readonly SemaphoreSlim ShutdownLock = new(1, 1);
|
||||
|
||||
public static async Task ShutdownComputer(){
|
||||
|
|
@ -860,7 +930,7 @@ public class Helpers{
|
|||
} else{
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
|
||||
try{
|
||||
using (var process = new Process()){
|
||||
process.StartInfo.FileName = shutdownCmd;
|
||||
|
|
@ -875,13 +945,13 @@ public class Helpers{
|
|||
Console.Error.WriteLine($"{e.Data}");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
process.OutputDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data)){
|
||||
Console.Error.WriteLine(e.Data);
|
||||
Console.Error.WriteLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
process.Start();
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
|
|
@ -892,11 +962,9 @@ public class Helpers{
|
|||
if (process.ExitCode != 0){
|
||||
Console.Error.WriteLine($"Shutdown failed with exit code {process.ExitCode}");
|
||||
}
|
||||
|
||||
}
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"Failed to start shutdown process: {ex.Message}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
|
@ -33,9 +33,12 @@ public class HttpClientReq{
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private HttpClient client;
|
||||
|
||||
|
||||
public readonly bool useFlareSolverr;
|
||||
private FlareSolverrClient flareSolverrClient;
|
||||
|
||||
public HttpClientReq(){
|
||||
IWebProxy systemProxy = WebRequest.DefaultWebProxy;
|
||||
|
||||
|
|
@ -79,6 +82,11 @@ public class HttpClientReq{
|
|||
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
|
||||
// client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.5");
|
||||
client.DefaultRequestHeaders.Connection.ParseAdd("keep-alive");
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties != null && CrunchyrollManager.Instance.CrunOptions.FlareSolverrProperties.UseFlareSolverr){
|
||||
useFlareSolverr = true;
|
||||
flareSolverrClient = new FlareSolverrClient();
|
||||
}
|
||||
}
|
||||
|
||||
private HttpMessageHandler CreateHttpClientHandler(){
|
||||
|
|
@ -137,6 +145,8 @@ public class HttpClientReq{
|
|||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
CaptureResponseCookies(response, request.RequestUri!, cookieStore);
|
||||
|
||||
return (IsOk: true, ResponseContent: content, error: "");
|
||||
} catch (Exception e){
|
||||
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||
|
|
@ -148,6 +158,48 @@ public class HttpClientReq{
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<(bool IsOk, string ResponseContent, string error)> SendFlareSolverrHttpRequest(HttpRequestMessage request, bool suppressError = false){
|
||||
string content = string.Empty;
|
||||
try{
|
||||
var flareSolverrResponses = await flareSolverrClient.SendViaFlareSolverrAsync(request, []);
|
||||
|
||||
|
||||
content = flareSolverrResponses.ResponseContent;
|
||||
|
||||
return (IsOk: flareSolverrResponses.IsOk, ResponseContent: content, error: "");
|
||||
} catch (Exception e){
|
||||
if (!suppressError){
|
||||
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||
}
|
||||
|
||||
return (IsOk: false, ResponseContent: content, error: "");
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureResponseCookies(HttpResponseMessage response, Uri requestUri, Dictionary<string, CookieCollection>? cookieStore){
|
||||
if (cookieStore == null){
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Headers.TryGetValues("Set-Cookie", out var cookieHeaders)){
|
||||
string domain = requestUri.Host.StartsWith("www.") ? requestUri.Host.Substring(4) : requestUri.Host;
|
||||
|
||||
foreach (var header in cookieHeaders){
|
||||
var cookies = header.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||
var nameValue = cookies[0].Split('=', 2);
|
||||
if (nameValue.Length != 2) continue;
|
||||
|
||||
var cookie = new Cookie(nameValue[0].Trim(), nameValue[1].Trim()){
|
||||
Domain = domain,
|
||||
Path = "/"
|
||||
};
|
||||
|
||||
AddCookie(domain, cookie, cookieStore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AttachCookies(HttpRequestMessage request, Dictionary<string, CookieCollection>? cookieStore){
|
||||
if (cookieStore == null){
|
||||
return;
|
||||
|
|
@ -177,6 +229,19 @@ public class HttpClientReq{
|
|||
}
|
||||
}
|
||||
|
||||
public string? GetCookieValue(string domain, string cookieName, Dictionary<string, CookieCollection>? cookieStore){
|
||||
if (cookieStore == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cookieStore.TryGetValue(domain, out var cookies)){
|
||||
var cookie = cookies.Cast<Cookie>().FirstOrDefault(c => c.Name == cookieName);
|
||||
return cookie?.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void AddCookie(string domain, Cookie cookie, Dictionary<string, CookieCollection>? cookieStore){
|
||||
if (cookieStore == null){
|
||||
return;
|
||||
|
|
@ -231,13 +296,14 @@ public static class ApiUrls{
|
|||
|
||||
public static string Auth => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/auth/v1/token";
|
||||
public static string Profile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/profile";
|
||||
public static string Profiles => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/multiprofile";
|
||||
public static string CmsToken => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/index/v2";
|
||||
public static string Search => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/search";
|
||||
public static string Browse => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/browse";
|
||||
public static string Cms => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/cms";
|
||||
public static string Content => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2";
|
||||
|
||||
public static string Playback => "https://cr-play-service.prd.crunchyrollsvc.com/v2";
|
||||
public static string Playback => "https://cr-play-service.prd.crunchyrollsvc.com/v3";
|
||||
//https://www.crunchyroll.com/playback/v2
|
||||
//https://cr-play-service.prd.crunchyrollsvc.com/v2
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ public class Merger{
|
|||
|
||||
public Merger(MergerOptions options){
|
||||
this.options = options;
|
||||
if (this.options.SkipSubMux != null && this.options.SkipSubMux == true){
|
||||
this.options.Subtitles = new();
|
||||
}
|
||||
|
||||
if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){
|
||||
this.options.VideoTitle = this.options.VideoTitle.Replace("\"", "'");
|
||||
|
|
@ -74,35 +71,39 @@ public class Merger{
|
|||
index++;
|
||||
}
|
||||
|
||||
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
|
||||
if (!options.SkipSubMux){
|
||||
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
|
||||
|
||||
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
|
||||
if (sub.value.Delay != null && sub.value.Delay != 0){
|
||||
double delay = sub.value.Delay / 1000.0 ?? 0;
|
||||
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
|
||||
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
|
||||
if (sub.value.Delay != null && sub.value.Delay != 0){
|
||||
double delay = sub.value.Delay / 1000.0 ?? 0;
|
||||
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
args.Add($"-i \"{sub.value.File}\"");
|
||||
metaData.Add($"-map {index}:s");
|
||||
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
|
||||
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
|
||||
&& sub.value.ClosedCaption == false){
|
||||
metaData.Add($"-disposition:s:{sub.i} default");
|
||||
} else{
|
||||
metaData.Add($"-disposition:s:{sub.i} 0");
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
args.Add($"-i \"{sub.value.File}\"");
|
||||
metaData.Add($"-map {index}:s");
|
||||
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
|
||||
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
|
||||
&& sub.value.ClosedCaption == false){
|
||||
metaData.Add($"-disposition:s:{sub.i} default");
|
||||
} else{
|
||||
metaData.Add($"-disposition:s:{sub.i} 0");
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
|
||||
args.AddRange(metaData);
|
||||
// args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
|
||||
args.Add("-c:v copy");
|
||||
args.Add("-c:a copy");
|
||||
args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass");
|
||||
args.AddRange(options.Subtitles.Select((sub, subindex) =>
|
||||
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
|
||||
|
||||
if (!options.SkipSubMux){
|
||||
args.AddRange(options.Subtitles.Select((sub, subindex) =>
|
||||
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(options.VideoTitle)){
|
||||
args.Add($"-metadata title=\"{options.VideoTitle}\"");
|
||||
|
|
@ -134,9 +135,9 @@ public class Merger{
|
|||
}
|
||||
|
||||
var audio = options.OnlyAudio.First();
|
||||
|
||||
|
||||
args.Add($"-i \"{audio.Path}\"");
|
||||
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : "") );
|
||||
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : ""));
|
||||
args.Add($"\"{options.Output}\"");
|
||||
return string.Join(" ", args);
|
||||
}
|
||||
|
|
@ -167,19 +168,31 @@ public class Merger{
|
|||
}
|
||||
}
|
||||
|
||||
// var sortedAudio = options.OnlyAudio
|
||||
// .OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue)
|
||||
// .ToList();
|
||||
|
||||
var rank = options.DubLangList
|
||||
.Select((val, i) => new{ val, i })
|
||||
.ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var sortedAudio = options.OnlyAudio
|
||||
.OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue)
|
||||
.OrderBy(m => {
|
||||
var key = m.Language?.CrLocale ?? string.Empty;
|
||||
return rank.TryGetValue(key, out var r) ? r : int.MaxValue; // unknown locales last
|
||||
})
|
||||
.ThenBy(m => m.IsAudioRoleDescription) // false first, then true
|
||||
.ToList();
|
||||
|
||||
foreach (var aud in sortedAudio){
|
||||
string trackName = aud.Language.Name;
|
||||
string trackName = aud.Language.Name + (aud.IsAudioRoleDescription ? " [AD]" : "");
|
||||
args.Add("--audio-tracks 0");
|
||||
args.Add("--no-video");
|
||||
args.Add($"--track-name 0:\"{trackName}\"");
|
||||
args.Add($"--language 0:{aud.Language.Code}");
|
||||
|
||||
|
||||
if (options.Defaults.Audio.Code == aud.Language.Code){
|
||||
if (options.Defaults.Audio.Code == aud.Language.Code && !aud.IsAudioRoleDescription){
|
||||
args.Add("--default-track 0");
|
||||
} else{
|
||||
args.Add("--default-track 0:0");
|
||||
|
|
@ -192,7 +205,7 @@ public class Merger{
|
|||
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\"");
|
||||
}
|
||||
|
||||
if (options.Subtitles.Count > 0){
|
||||
if (options.Subtitles.Count > 0 && !options.SkipSubMux){
|
||||
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
|
||||
|
||||
var sortedSubtitles = options.Subtitles
|
||||
|
|
@ -262,7 +275,7 @@ public class Merger{
|
|||
if (options.Description is{ Count: > 0 }){
|
||||
args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\"");
|
||||
}
|
||||
|
||||
|
||||
if (options.Cover.Count > 0){
|
||||
if (File.Exists(options.Cover.First().Path)){
|
||||
args.Add($"--attach-file \"{options.Cover.First().Path}\"");
|
||||
|
|
@ -434,14 +447,16 @@ public class Merger{
|
|||
allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".new.resume"));
|
||||
|
||||
options.Description?.ForEach(description => Helpers.DeleteFile(description.Path));
|
||||
|
||||
|
||||
options.Cover?.ForEach(cover => Helpers.DeleteFile(cover.Path));
|
||||
|
||||
// Delete chapter files if any
|
||||
options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
|
||||
|
||||
// Delete subtitle files
|
||||
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
|
||||
if (!options.SkipSubMux){
|
||||
// Delete subtitle files
|
||||
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -450,7 +465,7 @@ public class MergerInput{
|
|||
public LanguageItem Language{ get; set; }
|
||||
public int? Duration{ get; set; }
|
||||
public int? Delay{ get; set; }
|
||||
public bool? IsPrimary{ get; set; }
|
||||
public bool IsAudioRoleDescription{ get; set; }
|
||||
public int? Bitrate{ get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +489,7 @@ public class CrunchyMuxOptions{
|
|||
public List<string> DubLangList{ get; set; } = new List<string>();
|
||||
public List<string> SubLangList{ get; set; } = new List<string>();
|
||||
public string Output{ get; set; }
|
||||
public bool? SkipSubMux{ get; set; }
|
||||
public bool SkipSubMux{ get; set; }
|
||||
public bool? KeepAllVideos{ get; set; }
|
||||
public bool? Novids{ get; set; }
|
||||
public bool Mp4{ get; set; }
|
||||
|
|
@ -512,7 +527,7 @@ public class MergerOptions{
|
|||
public string VideoTitle{ get; set; }
|
||||
public bool? KeepAllVideos{ get; set; }
|
||||
public List<ParsedFont> Fonts{ get; set; } = new List<ParsedFont>();
|
||||
public bool? SkipSubMux{ get; set; }
|
||||
public bool SkipSubMux{ get; set; }
|
||||
public MuxOptions Options{ get; set; }
|
||||
public Defaults Defaults{ get; set; }
|
||||
public bool mp3{ get; set; }
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ public class VideoItem: VideoPlaylist{
|
|||
|
||||
public class AudioItem: AudioPlaylist{
|
||||
public string resolutionText{ get; set; }
|
||||
public string resolutionTextSnap{ get; set; }
|
||||
}
|
||||
|
||||
public class Quality{
|
||||
|
|
@ -63,8 +64,9 @@ public class MPDParsed{
|
|||
}
|
||||
|
||||
public class ServerData{
|
||||
public List<AudioPlaylist> audio{ get; set; } =[];
|
||||
public List<VideoPlaylist> video{ get; set; } =[];
|
||||
public List<string> servers{ get; set; } = [];
|
||||
public List<AudioPlaylist>? audio{ get; set; } =[];
|
||||
public List<VideoPlaylist>? video{ get; set; } =[];
|
||||
}
|
||||
|
||||
public static class MPDParser{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public partial class AnilistSeries : ObservableObject{
|
|||
public string BannerImage{ get; set; }
|
||||
public bool IsAdult{ get; set; }
|
||||
public CoverImage CoverImage{ get; set; }
|
||||
public Trailer Trailer{ get; set; }
|
||||
public Trailer? Trailer{ get; set; }
|
||||
public List<ExternalLink>? ExternalLinks{ get; set; }
|
||||
public List<Ranking> Rankings{ get; set; }
|
||||
public Studios Studios{ get; set; }
|
||||
|
|
@ -53,6 +53,9 @@ public partial class AnilistSeries : ObservableObject{
|
|||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
[ObservableProperty]
|
||||
public bool _fetchedFromCR;
|
||||
|
||||
[JsonIgnore]
|
||||
public string? CrunchyrollID;
|
||||
|
|
|
|||
|
|
@ -39,9 +39,13 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
|
|||
public string? SeasonName{ get; set; }
|
||||
|
||||
public string? CrSeriesID{ get; set; }
|
||||
|
||||
|
||||
public bool AnilistEpisode{ get; set; }
|
||||
|
||||
public bool FilteredOut{ get; set; }
|
||||
|
||||
public Locale? AudioLocale{ get; set; }
|
||||
|
||||
public List<CalendarEpisode> CalendarEpisodes{ get; set; } =[];
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
|
@ -57,13 +61,12 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
|
|||
await QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (CalendarEpisodes.Count > 0){
|
||||
foreach (var calendarEpisode in CalendarEpisodes){
|
||||
calendarEpisode.AddEpisodeToQue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task LoadImage(int width = 0, int height = 0){
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("shutdown_when_queue_empty")]
|
||||
public bool ShutdownWhenQueueEmpty{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("auto_download")]
|
||||
public bool AutoDownload{ get; set; }
|
||||
|
||||
|
|
@ -22,22 +22,25 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("retry_delay")]
|
||||
public int RetryDelay{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("retry_attempts")]
|
||||
public int RetryAttempts{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Force{ get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("download_methode_new")]
|
||||
public bool DownloadMethodeNew{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("download_allow_early_start")]
|
||||
public bool DownloadAllowEarlyStart{ get; set; }
|
||||
|
||||
[JsonProperty("simultaneous_downloads")]
|
||||
public int SimultaneousDownloads{ get; set; }
|
||||
|
||||
[JsonProperty("simultaneous_processing_jobs")]
|
||||
public int SimultaneousProcessingJobs{ get; set; }
|
||||
|
||||
[JsonProperty("theme")]
|
||||
public string Theme{ get; set; } = "";
|
||||
|
||||
|
|
@ -49,11 +52,11 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("download_finished_play_sound")]
|
||||
public bool DownloadFinishedPlaySound{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("download_finished_sound_path")]
|
||||
public string? DownloadFinishedSoundPath{ get; set; }
|
||||
|
||||
|
||||
|
||||
|
||||
[JsonProperty("background_image_opacity")]
|
||||
public double BackgroundImageOpacity{ get; set; }
|
||||
|
||||
|
|
@ -71,9 +74,9 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("history")]
|
||||
public bool History{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("history_count_missing")]
|
||||
public bool HistoryCountMissing { get; set; }
|
||||
public bool HistoryCountMissing{ get; set; }
|
||||
|
||||
[JsonProperty("history_include_cr_artists")]
|
||||
public bool HistoryIncludeCrArtists{ get; set; }
|
||||
|
|
@ -131,11 +134,18 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("proxy_password")]
|
||||
public string? ProxyPassword{ get; set; }
|
||||
|
||||
[JsonProperty("flare_solverr_properties")]
|
||||
public FlareSolverrProperties? FlareSolverrProperties{ get; set; }
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Crunchyroll Settings
|
||||
|
||||
[JsonProperty("cr_download_description_audio")]
|
||||
public bool DownloadDescriptionAudio{ get; set; }
|
||||
|
||||
[JsonProperty("cr_mark_as_watched")]
|
||||
public bool MarkAsWatched{ get; set; }
|
||||
|
||||
|
|
@ -144,6 +154,9 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("hard_sub_lang")]
|
||||
public string Hslang{ get; set; } = "";
|
||||
|
||||
[JsonProperty("hard_sub_raw_fallback")]
|
||||
public bool HsRawFallback{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int Kstream{ get; set; }
|
||||
|
|
@ -165,7 +178,7 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("file_name_whitespace_substitute")]
|
||||
public string FileNameWhitespaceSubstitute{ get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("file_name")]
|
||||
public string FileName{ get; set; } = "";
|
||||
|
||||
|
|
@ -181,6 +194,9 @@ public class CrDownloadOptions{
|
|||
[JsonIgnore]
|
||||
public bool SkipSubs{ get; set; }
|
||||
|
||||
[JsonProperty("subs_fix_ccc_subs")]
|
||||
public bool FixCccSubtitles{ get; set; }
|
||||
|
||||
[JsonProperty("mux_skip_subs")]
|
||||
public bool SkipSubsMux{ get; set; }
|
||||
|
||||
|
|
@ -189,7 +205,7 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("subs_download_duplicate")]
|
||||
public bool SubsDownloadDuplicate{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("include_signs_subs")]
|
||||
public bool IncludeSignsSubs{ get; set; }
|
||||
|
||||
|
|
@ -199,6 +215,9 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("include_cc_subs")]
|
||||
public bool IncludeCcSubs{ get; set; }
|
||||
|
||||
[JsonProperty("convert_cc_vtt_subs_to_ass")]
|
||||
public bool ConvertVtt2Ass{ get; set; }
|
||||
|
||||
[JsonProperty("cc_subs_font")]
|
||||
public string? CcSubsFont{ get; set; }
|
||||
|
||||
|
|
@ -207,13 +226,13 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("mux_mp4")]
|
||||
public bool Mp4{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("mux_audio_only_to_mp3")]
|
||||
public bool AudioOnlyToMp3 { get; set; }
|
||||
|
||||
public bool AudioOnlyToMp3{ get; set; }
|
||||
|
||||
[JsonProperty("mux_fonts")]
|
||||
public bool MuxFonts{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("mux_cover")]
|
||||
public bool MuxCover{ get; set; }
|
||||
|
||||
|
|
@ -258,7 +277,7 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("mux_sync_dubs")]
|
||||
public bool SyncTiming{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("mux_sync_hwaccel")]
|
||||
public string? FfmpegHwAccelFlag{ get; set; }
|
||||
|
||||
|
|
@ -286,17 +305,14 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("calendar_hide_dubs")]
|
||||
public bool CalendarHideDubs{ get; set; }
|
||||
|
||||
[JsonProperty("calendar_filter_by_air_date")]
|
||||
public bool CalendarFilterByAirDate{ get; set; }
|
||||
|
||||
[JsonProperty("calendar_show_upcoming_episodes")]
|
||||
public bool CalendarShowUpcomingEpisodes{ get; set; }
|
||||
|
||||
[JsonProperty("stream_endpoint")]
|
||||
public string? StreamEndpoint{ get; set; }
|
||||
|
||||
[JsonProperty("stream_endpoint_settings")]
|
||||
public CrAuthSettings? StreamEndpoint{ get; set; }
|
||||
|
||||
[JsonProperty("stream_endpoint_secondary_settings")]
|
||||
public CrAuthSettings? StreamEndpointSecondSettings { get; set; }
|
||||
public CrAuthSettings? StreamEndpointSecondSettings{ get; set; }
|
||||
|
||||
[JsonProperty("search_fetch_featured_music")]
|
||||
public bool SearchFetchFeaturedMusic{ get; set; }
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ public class CrProfile{
|
|||
[JsonProperty("profile_name")]
|
||||
public string? ProfileName{ get; set; }
|
||||
|
||||
[JsonProperty("profile_id")]
|
||||
public string? ProfileId{ get; set; }
|
||||
|
||||
[JsonProperty("preferred_content_audio_language")]
|
||||
public string? PreferredContentAudioLanguage{ get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace CRD.Utils.Structs.Crunchyroll;
|
|||
public class CrunchyStreamData{
|
||||
public string? AssetId{ get; set; }
|
||||
public Locale? AudioLocale{ get; set; }
|
||||
public string? AudioRole{ get; set; }
|
||||
public string? Bifs{ get; set; }
|
||||
public string? BurnedInLocale{ get; set; }
|
||||
public Dictionary<string, Caption>? Captions{ get; set; }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CRD.Utils.Structs.Crunchyroll;
|
||||
using CRD.Utils.Structs.History;
|
||||
|
|
@ -338,6 +339,8 @@ public class EpisodeVersion{
|
|||
[JsonProperty("season_guid")]
|
||||
public string SeasonGuid{ get; set; }
|
||||
|
||||
public string[] roles{ get; set; } =[];
|
||||
|
||||
public string Variant{ get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -394,6 +397,8 @@ public class CrunchyEpMeta{
|
|||
public CrDownloadOptions? DownloadSettings;
|
||||
|
||||
public bool HighlightAllAvailable{ get; set; }
|
||||
|
||||
public CancellationTokenSource Cts { get; } = new();
|
||||
}
|
||||
|
||||
public class DownloadProgress{
|
||||
|
|
@ -415,6 +420,8 @@ public class CrunchyEpMetaData{
|
|||
public bool IsSubbed{ get; set; }
|
||||
public bool IsDubbed{ get; set; }
|
||||
|
||||
public bool IsAudioRoleDescription{ get; set; }
|
||||
|
||||
public (string? seasonID, string? guid) GetOriginalIds(){
|
||||
var version = Versions?.FirstOrDefault(a => a.Original);
|
||||
if (version != null && !string.IsNullOrEmpty(version.Guid) && !string.IsNullOrEmpty(version.SeasonGuid)){
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ public class StreamDetails{
|
|||
public class UrlWithAuth{
|
||||
|
||||
public CrAuth? CrAuth{ get; set; }
|
||||
public bool Video{ get; set; }
|
||||
public bool Audio{ get; set; }
|
||||
|
||||
public string? Url{ get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class StreamError{
|
|||
}
|
||||
|
||||
public bool IsTooManyActiveStreamsError(){
|
||||
return Error == "TOO_MANY_ACTIVE_STREAMS";
|
||||
return Error is "TOO_MANY_ACTIVE_STREAMS" or "TOO_MANY_CONCURRENT_STREAMS";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ public class CrAuthSettings{
|
|||
public string UserAgent{ get; set; }
|
||||
public string Device_type{ get; set; }
|
||||
public string Device_name{ get; set; }
|
||||
|
||||
public bool Video{ get; set; }
|
||||
public bool Audio{ get; set; }
|
||||
}
|
||||
|
||||
public class StreamInfo{
|
||||
public string Playlist { get; set; }
|
||||
public bool Audio { get; set; }
|
||||
public bool Video { get; set; }
|
||||
}
|
||||
|
||||
public class DrmAuthData{
|
||||
|
|
@ -56,8 +65,8 @@ public class CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool?
|
|||
}
|
||||
|
||||
public class CrunchySeriesList{
|
||||
public List<Episode> List{ get; set; }
|
||||
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; }
|
||||
public List<Episode> List{ get; set; } = [];
|
||||
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; } = [];
|
||||
}
|
||||
|
||||
public class Episode{
|
||||
|
|
|
|||
|
|
@ -143,13 +143,13 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
await DownloadEpisode();
|
||||
}
|
||||
|
||||
public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){
|
||||
public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default, string overrideDownloadPath = ""){
|
||||
switch (EpisodeType){
|
||||
case EpisodeType.MusicVideo:
|
||||
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty);
|
||||
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty, overrideDownloadPath);
|
||||
break;
|
||||
case EpisodeType.Concert:
|
||||
await QueueManager.Instance.CrAddConcertToQueue(EpisodeId ?? string.Empty);
|
||||
await QueueManager.Instance.CrAddConcertToQueue(EpisodeId ?? string.Empty, overrideDownloadPath);
|
||||
break;
|
||||
case EpisodeType.Episode:
|
||||
case EpisodeType.Unknown:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ using CRD.Downloader.Crunchyroll;
|
|||
using CRD.Utils;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.Crunchyroll.Music;
|
||||
using CRD.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
|
|
@ -210,6 +212,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
} else if (AddAllEpisodes){
|
||||
var musicClass = CrunchyrollManager.Instance.CrMusic;
|
||||
if (currentMusicVideoList == null) return;
|
||||
foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){
|
||||
QueueManager.Instance.CrAddMusicMetaToQueue(meta);
|
||||
}
|
||||
|
|
@ -421,7 +424,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
if (SeasonList.Count > 0){
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
}
|
||||
}
|
||||
|
||||
private string DetermineLocale(string locale){
|
||||
|
|
@ -525,15 +530,35 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
var list = await FetchSeriesListAsync(value.Id);
|
||||
|
||||
if (list != null){
|
||||
if (list is{ List.Count: > 0 }){
|
||||
currentSeriesList = list;
|
||||
await SearchPopulateEpisodesBySeason(value.Id);
|
||||
UpdateUiForEpisodeSelection();
|
||||
} else{
|
||||
ButtonEnabled = true;
|
||||
ResetSearch();
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to get Episodes for Series", ToastType.Error, 2));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSearch(){
|
||||
currentMusicVideoList = null;
|
||||
UrlInput = "";
|
||||
selectedEpisodes.Clear();
|
||||
SelectedItems.Clear();
|
||||
Items.Clear();
|
||||
currentSeriesList = null;
|
||||
SeasonList.Clear();
|
||||
episodesBySeason.Clear();
|
||||
AllButtonEnabled = false;
|
||||
AddAllEpisodes = false;
|
||||
ButtonEnabled = false;
|
||||
SearchVisible = true;
|
||||
SlectSeasonVisible = false;
|
||||
ShowLoading = false;
|
||||
SearchEnabled = false; // disable and enable for button text
|
||||
SearchEnabled = true;
|
||||
}
|
||||
|
||||
private void UpdateUiForSearchSelection(){
|
||||
SearchPopupVisible = false;
|
||||
RaisePropertyChanged(nameof(SearchVisible));
|
||||
|
|
@ -602,7 +627,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
if (SeasonList.Count > 0){
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUiForEpisodeSelection(){
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Downloader.Crunchyroll.Utils;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using DynamicData;
|
||||
|
|
@ -18,7 +19,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _prevButtonEnabled = true;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _nextButtonEnabled = true;
|
||||
|
||||
|
|
@ -28,9 +29,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool _customCalendar;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _filterByAirDate;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showUpcomingEpisodes;
|
||||
|
||||
|
|
@ -75,7 +73,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
|
||||
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
|
||||
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
|
||||
FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate;
|
||||
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
|
||||
|
||||
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
|
||||
|
|
@ -87,7 +84,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
|
||||
|
||||
private string GetThisWeeksMondayDate(){
|
||||
DateTime today = DateTime.Today;
|
||||
|
||||
|
|
@ -104,13 +100,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
return formattedDate;
|
||||
}
|
||||
|
||||
public async void LoadCalendar(string mondayDate,DateTime customCalDate, bool forceUpdate){
|
||||
public async void LoadCalendar(string mondayDate, DateTime customCalDate, bool forceUpdate){
|
||||
ShowLoading = true;
|
||||
|
||||
|
||||
CalendarWeek week;
|
||||
|
||||
if (CustomCalendar){
|
||||
|
||||
if (customCalDate.Date == DateTime.Now.Date){
|
||||
PrevButtonEnabled = false;
|
||||
NextButtonEnabled = true;
|
||||
|
|
@ -118,7 +113,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
PrevButtonEnabled = true;
|
||||
NextButtonEnabled = false;
|
||||
}
|
||||
|
||||
|
||||
week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate);
|
||||
} else{
|
||||
PrevButtonEnabled = true;
|
||||
|
|
@ -140,29 +135,24 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
|
||||
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
||||
if (calendarDayCalendarEpisode.AnilistEpisode){
|
||||
_ = calendarDayCalendarEpisode.LoadImage(100,150);
|
||||
_ = calendarDayCalendarEpisode.LoadImage(100, 150);
|
||||
} else{
|
||||
_ = calendarDayCalendarEpisode.LoadImage();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else{
|
||||
foreach (var calendarDay in CalendarDays){
|
||||
var episodesCopy = new List<CalendarEpisode>(calendarDay.CalendarEpisodes);
|
||||
foreach (var calendarDayCalendarEpisode in episodesCopy){
|
||||
if (calendarDayCalendarEpisode.SeasonName != null && HideDubs && calendarDayCalendarEpisode.SeasonName.EndsWith("Dub)")){
|
||||
calendarDay.CalendarEpisodes.Remove(calendarDayCalendarEpisode);
|
||||
continue;
|
||||
}
|
||||
if (HideDubs)
|
||||
calendarDay.CalendarEpisodes.RemoveAll(e => CrSimulcastCalendarFilter.IsDubOrAltLanguageSeason(e.SeasonName));
|
||||
|
||||
if (calendarDayCalendarEpisode.ImageBitmap == null){
|
||||
if (calendarDayCalendarEpisode.AnilistEpisode){
|
||||
_ = calendarDayCalendarEpisode.LoadImage(100,150);
|
||||
} else{
|
||||
_ = calendarDayCalendarEpisode.LoadImage();
|
||||
}
|
||||
foreach (var e in calendarDay.CalendarEpisodes){
|
||||
if (e.ImageBitmap == null){
|
||||
if (e.AnilistEpisode)
|
||||
_ = e.LoadImage(100, 150);
|
||||
else
|
||||
_ = e.LoadImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -199,7 +189,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(6);
|
||||
}
|
||||
|
||||
LoadCalendar(mondayDate,refreshDate, true);
|
||||
LoadCalendar(mondayDate, refreshDate, true);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -215,13 +205,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
} else{
|
||||
mondayDate = GetThisWeeksMondayDate();
|
||||
}
|
||||
|
||||
|
||||
var refreshDate = DateTime.Now;
|
||||
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
|
||||
}
|
||||
|
||||
LoadCalendar(mondayDate,refreshDate, false);
|
||||
LoadCalendar(mondayDate, refreshDate, false);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -237,15 +227,13 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
} else{
|
||||
mondayDate = GetThisWeeksMondayDate();
|
||||
}
|
||||
|
||||
|
||||
var refreshDate = DateTime.Now;
|
||||
if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
|
||||
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13);
|
||||
}
|
||||
|
||||
LoadCalendar(mondayDate,refreshDate, false);
|
||||
|
||||
|
||||
LoadCalendar(mondayDate, refreshDate, false);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -268,7 +256,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
|
||||
CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value;
|
||||
|
||||
LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true);
|
||||
LoadCalendar(GetThisWeeksMondayDate(), DateTime.Now, true);
|
||||
|
||||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
|
@ -282,15 +270,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
||||
partial void OnFilterByAirDateChanged(bool value){
|
||||
if (loading){
|
||||
return;
|
||||
}
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
|
||||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
||||
partial void OnShowUpcomingEpisodesChanged(bool value){
|
||||
if (loading){
|
||||
return;
|
||||
|
|
@ -310,7 +289,4 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -270,6 +270,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
|
|||
CrunchyEpMeta? downloadItem = QueueManager.Instance.Queue.FirstOrDefault(e => e.Equals(epMeta)) ?? null;
|
||||
if (downloadItem != null){
|
||||
QueueManager.Instance.Queue.Remove(downloadItem);
|
||||
epMeta.Cts.Cancel();
|
||||
if (!Done){
|
||||
foreach (var downloadItemDownloadedFile in downloadItem.downloadedFiles){
|
||||
try{
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
FullSizeDesired = true
|
||||
};
|
||||
|
||||
var viewModel = new ContentDialogFeaturedMusicViewModel(dialog, musicList, CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists);
|
||||
var viewModel = new ContentDialogFeaturedMusicViewModel(dialog, musicList, CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists, SelectedSeries.SeriesFolderPathExists ? SelectedSeries.SeriesFolderPath : "");
|
||||
dialog.Content = new ContentDialogFeaturedMusicView(){
|
||||
DataContext = viewModel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
|
@ -17,6 +18,7 @@ using CRD.Utils.Files;
|
|||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.History;
|
||||
using CRD.Views;
|
||||
using FluentAvalonia.UI.Data;
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI;
|
||||
|
||||
|
|
@ -149,6 +151,9 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool _quickAddMode;
|
||||
|
||||
[ObservableProperty]
|
||||
private static bool _showCrFetches;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
|
|
@ -168,7 +173,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
public ObservableCollection<SeasonViewModel> Seasons{ get; set; } =[];
|
||||
|
||||
public ObservableCollection<AnilistSeries> SelectedSeason{ get; set; } =[];
|
||||
|
||||
|
||||
private SeasonViewModel currentSelection;
|
||||
|
||||
public UpcomingPageViewModel(){
|
||||
|
|
@ -198,11 +203,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
currentSelection = Seasons.Last();
|
||||
currentSelection.IsSelected = true;
|
||||
|
||||
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false);
|
||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
||||
|
||||
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false, crunchySimul);
|
||||
SelectedSeason.Clear();
|
||||
|
||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
||||
|
||||
|
||||
foreach (var anilistSeries in list){
|
||||
SelectedSeason.Add(anilistSeries);
|
||||
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
||||
|
|
@ -214,6 +219,8 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
FilterItems();
|
||||
|
||||
SortItems();
|
||||
}
|
||||
|
||||
|
|
@ -223,11 +230,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
currentSelection = selectedSeason;
|
||||
currentSelection.IsSelected = true;
|
||||
|
||||
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false);
|
||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
||||
|
||||
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false, crunchySimul);
|
||||
SelectedSeason.Clear();
|
||||
|
||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
||||
|
||||
|
||||
foreach (var anilistSeries in list){
|
||||
SelectedSeason.Add(anilistSeries);
|
||||
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
||||
|
|
@ -238,17 +245,24 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
FilterItems();
|
||||
|
||||
SortItems();
|
||||
}
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenTrailer(AnilistSeries series){
|
||||
if (series.Trailer.Site.Equals("youtube")){
|
||||
var url = "https://www.youtube.com/watch?v=" + series.Trailer.Id; // Replace with your video URL
|
||||
Process.Start(new ProcessStartInfo{
|
||||
FileName = url,
|
||||
UseShellExecute = true
|
||||
});
|
||||
if (series.Trailer != null){
|
||||
if (series.Trailer.Site.Equals("youtube")){
|
||||
var url = "https://www.youtube.com/watch?v=" + series.Trailer.Id;
|
||||
Process.Start(new ProcessStartInfo{
|
||||
FileName = url,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +272,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
MessageBus.Current.SendMessage(new ToastMessage($"History still loading", ToastType.Warning, 3));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(series.CrunchyrollID)){
|
||||
if (CrunchyrollManager.Instance.CrunOptions.History){
|
||||
series.IsInHistory = true;
|
||||
|
|
@ -280,42 +294,48 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<List<AnilistSeries>> GetSeriesForSeason(string season, int year, bool forceRefresh){
|
||||
private async Task<List<AnilistSeries>> GetSeriesForSeason(string season, int year, bool forceRefresh, CrBrowseSeriesBase? crBrowseSeriesBase){
|
||||
if (ProgramManager.Instance.AnilistSeasons.ContainsKey(season + year) && !forceRefresh){
|
||||
return ProgramManager.Instance.AnilistSeasons[season + year];
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
|
||||
var variables = new{
|
||||
season,
|
||||
year,
|
||||
format = "TV",
|
||||
page = 1
|
||||
};
|
||||
var allMedia = new List<AnilistSeries>();
|
||||
var page = 1;
|
||||
var maxPage = 10;
|
||||
bool hasNext;
|
||||
|
||||
var payload = new{
|
||||
query,
|
||||
variables
|
||||
};
|
||||
do{
|
||||
var payload = new{
|
||||
query,
|
||||
variables = new{ season, year, page }
|
||||
};
|
||||
|
||||
string jsonPayload = JsonConvert.SerializeObject(payload, Formatting.Indented);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Anilist);
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(payload, Formatting.Indented),
|
||||
Encoding.UTF8, "application/json");
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Anilist);
|
||||
request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"Anilist Request Failed for {season} {year} (page {page})");
|
||||
break;
|
||||
}
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
var ani = Helpers.Deserialize<AniListResponse>(
|
||||
response.ResponseContent,
|
||||
CrunchyrollManager.Instance.SettingsJsonSerializerSettings
|
||||
) ?? new AniListResponse();
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"Anilist Request Failed for {season} {year}");
|
||||
return[];
|
||||
}
|
||||
var pageNode = ani.Data?.Page;
|
||||
var media = pageNode?.Media ?? new List<AnilistSeries>();
|
||||
allMedia.AddRange(media);
|
||||
|
||||
AniListResponse aniListResponse = Helpers.Deserialize<AniListResponse>(response.ResponseContent, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ?? new AniListResponse();
|
||||
|
||||
var list = aniListResponse.Data?.Page?.Media ??[];
|
||||
|
||||
list = list.Where(ele => ele.ExternalLinks != null && ele.ExternalLinks.Any(external =>
|
||||
hasNext = pageNode?.PageInfo?.HasNextPage ?? false;
|
||||
page++;
|
||||
} while (hasNext || page <= maxPage);
|
||||
|
||||
var list = allMedia.Where(ele => ele.ExternalLinks != null && ele.ExternalLinks.Any(external =>
|
||||
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
|
||||
|
||||
|
|
@ -324,6 +344,8 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
anilistEle.Description = anilistEle.Description
|
||||
.Replace("<i>", "")
|
||||
.Replace("</i>", "")
|
||||
.Replace("<b>", "")
|
||||
.Replace("</b>", "")
|
||||
.Replace("<BR>", "")
|
||||
.Replace("<br>", "");
|
||||
|
||||
|
|
@ -388,6 +410,49 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
var existingIds = list
|
||||
.Where(a => !string.IsNullOrEmpty(a.CrunchyrollID))
|
||||
.Select(a => a.CrunchyrollID!)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
var notInList = (crBrowseSeriesBase?.Data ?? Enumerable.Empty<CrBrowseSeries>())
|
||||
.ExceptBy(existingIds, cs => cs.Id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
foreach (var crBrowseSeries in notInList){
|
||||
var newAnlistObject = new AnilistSeries();
|
||||
newAnlistObject.Title = new Title();
|
||||
newAnlistObject.Title.English = crBrowseSeries.Title ?? "";
|
||||
newAnlistObject.Description = crBrowseSeries.Description ?? "";
|
||||
newAnlistObject.CoverImage = new CoverImage();
|
||||
int targetW = 240, targetH = 360;
|
||||
|
||||
string posterSrc =
|
||||
crBrowseSeries.Images.PosterTall.FirstOrDefault()?
|
||||
.OrderBy(i => Math.Abs(i.Width - targetW) + Math.Abs(i.Height - targetH))
|
||||
.ThenBy(i => Math.Abs((i.Width / (double)i.Height) - (targetW / (double)targetH)))
|
||||
.Select(i => i.Source)
|
||||
.FirstOrDefault(s => !string.IsNullOrEmpty(s))
|
||||
?? crBrowseSeries.Images.PosterTall.FirstOrDefault()?.FirstOrDefault()?.Source
|
||||
?? string.Empty;
|
||||
newAnlistObject.CoverImage.ExtraLarge = posterSrc;
|
||||
newAnlistObject.ThumbnailImage = await Helpers.LoadImage(newAnlistObject.CoverImage.ExtraLarge, 185, 265);
|
||||
newAnlistObject.ExternalLinks = new List<ExternalLink>();
|
||||
newAnlistObject.ExternalLinks.Add(new ExternalLink(){ Url = $"https://www.crunchyroll.com/series/{crBrowseSeries.Id}/{crBrowseSeries.SlugTitle}" });
|
||||
newAnlistObject.FetchedFromCR = true;
|
||||
newAnlistObject.HasCrID = true;
|
||||
newAnlistObject.CrunchyrollID = crBrowseSeries.Id;
|
||||
if (CrunchyrollManager.Instance.CrunOptions.History){
|
||||
var historyIDs = new HashSet<string>(CrunchyrollManager.Instance.HistoryList.Select(item => item.SeriesId ?? ""));
|
||||
|
||||
if (newAnlistObject.CrunchyrollID != null && historyIDs.Contains(newAnlistObject.CrunchyrollID)){
|
||||
newAnlistObject.IsInHistory = true;
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(newAnlistObject);
|
||||
}
|
||||
|
||||
|
||||
ProgramManager.Instance.AnilistSeasons[season + year] = list;
|
||||
|
||||
|
|
@ -447,6 +512,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
partial void OnSelectedSeriesChanged(AnilistSeries? value){
|
||||
SelectionChangedOfSeries(value);
|
||||
}
|
||||
|
||||
partial void OnShowCrFetchesChanged(bool value){
|
||||
FilterItems();
|
||||
SortItems();
|
||||
}
|
||||
|
||||
#region Sorting
|
||||
|
||||
|
|
@ -512,6 +582,24 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
SelectedSeason.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterItems(){
|
||||
|
||||
List<AnilistSeries> filteredList;
|
||||
|
||||
if (ProgramManager.Instance.AnilistSeasons.ContainsKey(currentSelection.Season + currentSelection.Year)){
|
||||
filteredList = ProgramManager.Instance.AnilistSeasons[currentSelection.Season + currentSelection.Year];
|
||||
} else{
|
||||
return;
|
||||
}
|
||||
|
||||
filteredList = !ShowCrFetches ? filteredList.Where(e => !e.FetchedFromCR).ToList() : filteredList.ToList();
|
||||
|
||||
SelectedSeason.Clear();
|
||||
foreach (var item in filteredList){
|
||||
SelectedSeason.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Structs.Crunchyroll.Music;
|
||||
using CRD.Utils.Structs.History;
|
||||
using CRD.Utils.UI;
|
||||
|
|
@ -23,11 +24,13 @@ public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{
|
|||
private bool _musicInHistory;
|
||||
|
||||
private CrunchyMusicVideoList featuredMusic;
|
||||
private string FolderPath = "";
|
||||
|
||||
public ContentDialogFeaturedMusicViewModel(CustomContentDialog contentDialog, CrunchyMusicVideoList featuredMusic, bool crunOptionsHistoryIncludeCrArtists){
|
||||
public ContentDialogFeaturedMusicViewModel(CustomContentDialog contentDialog, CrunchyMusicVideoList featuredMusic, bool crunOptionsHistoryIncludeCrArtists, string overrideDownloadPath = ""){
|
||||
ArgumentNullException.ThrowIfNull(contentDialog);
|
||||
|
||||
this.featuredMusic = featuredMusic;
|
||||
this.FolderPath = overrideDownloadPath + "/OST";
|
||||
|
||||
dialog = contentDialog;
|
||||
dialog.Closed += DialogOnClosed;
|
||||
|
|
@ -78,7 +81,7 @@ public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{
|
|||
|
||||
[RelayCommand]
|
||||
public void DownloadEpisode(HistoryEpisode episode){
|
||||
episode.DownloadEpisode();
|
||||
episode.DownloadEpisode(EpisodeDownloadMode.Default, FolderPath);
|
||||
}
|
||||
|
||||
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.Structs;
|
||||
|
|
@ -9,15 +10,19 @@ namespace CRD.ViewModels.Utils;
|
|||
public partial class ContentDialogInputLoginViewModel : ViewModelBase{
|
||||
private readonly ContentDialog dialog;
|
||||
|
||||
private readonly TaskCompletionSource<bool> _loginTcs = new();
|
||||
|
||||
public Task LoginCompleted => _loginTcs.Task;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _email;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _password;
|
||||
|
||||
private AccountPageViewModel accountPageViewModel;
|
||||
private AccountPageViewModel? accountPageViewModel;
|
||||
|
||||
public ContentDialogInputLoginViewModel(ContentDialog dialog, AccountPageViewModel accountPageViewModel = null){
|
||||
public ContentDialogInputLoginViewModel(ContentDialog dialog, AccountPageViewModel? accountPageViewModel = null){
|
||||
if (dialog is null){
|
||||
throw new ArgumentNullException(nameof(dialog));
|
||||
}
|
||||
|
|
@ -30,15 +35,19 @@ public partial class ContentDialogInputLoginViewModel : ViewModelBase{
|
|||
|
||||
private async void LoginButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
||||
dialog.PrimaryButtonClick -= LoginButton;
|
||||
await CrunchyrollManager.Instance.CrAuthEndpoint1.Auth(new AuthData{Password = Password,Username = Email});
|
||||
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrAuthEndpoint2.AuthSettings.Endpoint)){
|
||||
await CrunchyrollManager.Instance.CrAuthEndpoint2.Auth(new AuthData{Password = Password,Username = Email});
|
||||
}
|
||||
try{
|
||||
await CrunchyrollManager.Instance.CrAuthEndpoint1.Auth(new AuthData{ Password = Password, Username = Email });
|
||||
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrAuthEndpoint2.AuthSettings.Endpoint)){
|
||||
await CrunchyrollManager.Instance.CrAuthEndpoint2.Auth(new AuthData{ Password = Password, Username = Email });
|
||||
}
|
||||
|
||||
accountPageViewModel?.UpdatetProfile();
|
||||
|
||||
|
||||
if (accountPageViewModel != null){
|
||||
accountPageViewModel.UpdatetProfile();
|
||||
_loginTcs.TrySetResult(true);
|
||||
} catch (Exception ex){
|
||||
_loginTcs.TrySetException(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
|
||||
|
|
|
|||
|
|
@ -54,9 +54,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private double? _simultaneousDownloads;
|
||||
|
||||
[ObservableProperty]
|
||||
private double? _simultaneousProcessingJobs;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _downloadMethodeNew;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _downloadAllowEarlyStart;
|
||||
|
||||
|
|
@ -204,6 +207,18 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private string _proxyPassword;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _flareSolverrHost = "localhost";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _flareSolverrPort = "8191";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _flareSolverrUseSsl = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _useFlareSolverr = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _tempDownloadDirPath;
|
||||
|
|
@ -261,6 +276,15 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
SonarrPort = props.Port + "";
|
||||
SonarrApiKey = props.ApiKey + "";
|
||||
}
|
||||
|
||||
var propsFlareSolverr = options.FlareSolverrProperties;
|
||||
|
||||
if (propsFlareSolverr != null){
|
||||
FlareSolverrUseSsl = propsFlareSolverr.UseSsl;
|
||||
UseFlareSolverr = propsFlareSolverr.UseFlareSolverr;
|
||||
FlareSolverrHost = propsFlareSolverr.Host + "";
|
||||
FlareSolverrPort = propsFlareSolverr.Port + "";
|
||||
}
|
||||
|
||||
ProxyEnabled = options.ProxyEnabled;
|
||||
ProxySocks = options.ProxySocks;
|
||||
|
|
@ -280,6 +304,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
RetryDelay = Math.Clamp((options.RetryDelay), 1, 30);
|
||||
DownloadToTempFolder = options.DownloadToTempFolder;
|
||||
SimultaneousDownloads = options.SimultaneousDownloads;
|
||||
SimultaneousProcessingJobs = options.SimultaneousProcessingJobs;
|
||||
LogMode = options.LogMode;
|
||||
|
||||
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
|
||||
|
|
@ -320,6 +345,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
settings.HistoryCountSonarr = HistoryCountSonarr;
|
||||
settings.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
|
||||
settings.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
|
||||
settings.SimultaneousProcessingJobs = Math.Clamp((int)(SimultaneousProcessingJobs ?? 0), 1, 10);
|
||||
|
||||
QueueManager.Instance.SetLimit(settings.SimultaneousProcessingJobs);
|
||||
|
||||
settings.ProxyEnabled = ProxyEnabled;
|
||||
settings.ProxySocks = ProxySocks;
|
||||
|
|
@ -355,9 +383,22 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
props.ApiKey = SonarrApiKey;
|
||||
|
||||
|
||||
|
||||
settings.SonarrProperties = props;
|
||||
|
||||
var propsFlareSolverr = new FlareSolverrProperties();
|
||||
|
||||
propsFlareSolverr.UseSsl = FlareSolverrUseSsl;
|
||||
propsFlareSolverr.UseFlareSolverr = UseFlareSolverr;
|
||||
propsFlareSolverr.Host = FlareSolverrHost;
|
||||
|
||||
if (int.TryParse(FlareSolverrPort, out var portNumberFlare)){
|
||||
propsFlareSolverr.Port = portNumberFlare;
|
||||
} else{
|
||||
propsFlareSolverr.Port = 8989;
|
||||
}
|
||||
|
||||
settings.FlareSolverrProperties = propsFlareSolverr;
|
||||
|
||||
settings.LogMode = LogMode;
|
||||
|
||||
|
|
|
|||
|
|
@ -96,9 +96,6 @@
|
|||
SelectedItem="{Binding CurrentCalendarDubFilter}"
|
||||
ItemsSource="{Binding CalendarDubFilter}">
|
||||
</ComboBox>
|
||||
<CheckBox IsChecked="{Binding FilterByAirDate}"
|
||||
Content="Filter by episode air date" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
|
||||
Content="Show Upcoming episodes" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
|
|
@ -109,11 +106,16 @@
|
|||
</controls:SettingsExpander>
|
||||
|
||||
|
||||
<controls:SettingsExpander Header="Calendar ">
|
||||
<controls:SettingsExpander Header="Calendar " IsVisible="{Binding !CustomCalendar}">
|
||||
<controls:SettingsExpander.Footer>
|
||||
<CheckBox IsChecked="{Binding HideDubs}"
|
||||
Content="Hide Dubs" Margin="5 0 0 0">
|
||||
</CheckBox>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding HideDubs}"
|
||||
Content="Hide Dubs" Margin="5 0 0 0">
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
|
||||
Content="Show Upcoming episodes" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</controls:SettingsExpander.Footer>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
|
|
@ -232,8 +234,7 @@
|
|||
<Button HorizontalAlignment="Center" Content="Download"
|
||||
IsEnabled="{Binding HasPassed}"
|
||||
IsVisible="{Binding HasPassed}"
|
||||
Command="{Binding AddEpisodeToQue}"
|
||||
/>
|
||||
Command="{Binding AddEpisodeToQue}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
|
|
|||
|
|
@ -120,7 +120,10 @@ public partial class MainWindow : AppWindow{
|
|||
.Subscribe(message => ShowToast(message.Message ?? string.Empty, message.Type, message.Seconds));
|
||||
}
|
||||
|
||||
public async void ShowError(string message, bool githubWikiButton = false){
|
||||
//ffmpeg - https://github.com/GyanD/codexffmpeg/releases/latest
|
||||
//mkvmerge - https://mkvtoolnix.download/downloads.html#windows
|
||||
//git wiki - https://github.com/Crunchy-DL/Crunchy-Downloader/wiki
|
||||
public async void ShowError(string message, string urlButtonText = "", string url = ""){
|
||||
if (activeErrors.Contains(message))
|
||||
return;
|
||||
|
||||
|
|
@ -132,14 +135,14 @@ public partial class MainWindow : AppWindow{
|
|||
CloseButtonText = "Close"
|
||||
};
|
||||
|
||||
if (githubWikiButton){
|
||||
dialog.PrimaryButtonText = "Github Wiki";
|
||||
if (!string.IsNullOrEmpty(urlButtonText)){
|
||||
dialog.PrimaryButtonText = urlButtonText;
|
||||
}
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary){
|
||||
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
|
||||
Helpers.OpenUrl(url);
|
||||
}
|
||||
|
||||
activeErrors.Remove(message);
|
||||
|
|
|
|||
|
|
@ -59,6 +59,19 @@
|
|||
|
||||
<StackPanel Grid.Column="1" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
|
||||
|
||||
<ToggleButton Width="50" Height="50" BorderThickness="0" Margin="0 0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding ShowCrFetches}"
|
||||
IsEnabled="{Binding !IsLoading}">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<controls:ImageIcon Source="../Assets/crunchy_icon_round.png" Width="25" Height="25" />
|
||||
<TextBlock Text="CR" TextWrapping="Wrap" HorizontalAlignment="Center" FontSize="12"></TextBlock>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Fetch Crunchyroll shows and append missing entries. This may create duplicates" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
|
||||
<ToggleButton Width="50" Height="50" BorderThickness="0" Margin="5 0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding QuickAddMode}"
|
||||
|
|
@ -149,7 +162,7 @@
|
|||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||
<Grid>
|
||||
<Grid >
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="185" />
|
||||
|
|
@ -268,7 +281,7 @@
|
|||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom">
|
||||
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Trailer" Margin=" 0 0 5 0"
|
||||
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" IsVisible="{Binding Trailer}" Content="Trailer" Margin=" 0 0 5 0"
|
||||
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).OpenTrailer}"
|
||||
CommandParameter="{Binding}">
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
<CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Allow early start of next download" Description="When enabled, the next download starts as soon as the previous file has finished downloading, even if it is still being finalized (muxed/moved).">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding DownloadAllowEarlyStart}"> </CheckBox>
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Simultaneous Downloads">
|
||||
<controls:SettingsExpanderItem Content="Parallel Downloads">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<controls:NumberBox Minimum="0" Maximum="10"
|
||||
Value="{Binding SimultaneousDownloads}"
|
||||
|
|
@ -187,6 +187,15 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding DownloadAllowEarlyStart}" Content="Parallel Processing Jobs" Description="The maximum number of completed downloads that can be processed simultaneously (encoding, muxing, moving)">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<controls:NumberBox Minimum="0" Maximum="10"
|
||||
Value="{Binding SimultaneousProcessingJobs}"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Play completion sound" Description="Enables a notification sound to be played when all downloads have finished">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
|
|
@ -376,6 +385,40 @@
|
|||
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<controls:SettingsExpander Header="Flare Solverr Settings"
|
||||
IconSource="Wifi2"
|
||||
Description="FlareSolverr settings (used only for the Calendar)"
|
||||
IsExpanded="False">
|
||||
|
||||
<controls:SettingsExpanderItem Content="Use Flare Solverr">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding UseFlareSolverr}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Host">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||
Text="{Binding FlareSolverrHost}" />
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Port">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||
Text="{Binding FlareSolverrPort}" />
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Use SSL">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding FlareSolverrUseSsl}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<controls:SettingsExpander Header="App Appearance"
|
||||
IconSource="DarkTheme"
|
||||
Description="Customize the look and feel of the application"
|
||||
|
|
@ -608,7 +651,7 @@
|
|||
DockPanel.Dock="Left" />
|
||||
|
||||
<ColorPicker Color="{Binding CustomAccentColor}"
|
||||
DockPanel.Dock="Right" />
|
||||
DockPanel.Dock="Right" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ A simple crunchyroll downloader that allows you to download your favorite series
|
|||
## 🛠️ System Requirements
|
||||
|
||||
- **Operating System:** Windows 10 or Windows 11
|
||||
- **.NET Desktop Runtime:** Version 8.0
|
||||
- **.NET Desktop Runtime:** Version 10.0
|
||||
- **Visual C++ Redistributable:** 2015–2022
|
||||
|
||||
## 🖥️ Features
|
||||
|
|
|
|||
Loading…
Reference in a new issue