mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-05-14 05:50:47 +00:00
- Added **toggle to also download description audio** for selected dubs
- Added **automatic history backups** retained for up to 5 days - Improved **season tab** to display series more effectively - Improved **history saving** for increased data safety - Removed **"None" option** from the hardsub selection popup
This commit is contained in:
parent
8a5c51900b
commit
dc570bf420
28 changed files with 1130 additions and 416 deletions
|
|
@ -208,6 +208,7 @@ public class CalendarManager{
|
||||||
|
|
||||||
//EpisodeAirDate
|
//EpisodeAirDate
|
||||||
foreach (var crBrowseEpisode in newEpisodes){
|
foreach (var crBrowseEpisode in newEpisodes){
|
||||||
|
bool filtered = false;
|
||||||
DateTime episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.Kind == DateTimeKind.Utc
|
DateTime episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.Kind == DateTimeKind.Utc
|
||||||
? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.ToLocalTime()
|
? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.ToLocalTime()
|
||||||
: crBrowseEpisode.EpisodeMetadata.EpisodeAirDate;
|
: crBrowseEpisode.EpisodeMetadata.EpisodeAirDate;
|
||||||
|
|
@ -257,13 +258,13 @@ public class CalendarManager{
|
||||||
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Audio)")) &&
|
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Audio)")) &&
|
||||||
(string.IsNullOrEmpty(dubFilter) || dubFilter == "none" || (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter))){
|
(string.IsNullOrEmpty(dubFilter) || dubFilter == "none" || (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter))){
|
||||||
//|| crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp
|
//|| crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp
|
||||||
continue;
|
filtered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(dubFilter) && dubFilter != "none"){
|
if (!string.IsNullOrEmpty(dubFilter) && dubFilter != "none"){
|
||||||
if (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){
|
if (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){
|
||||||
continue;
|
filtered = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,6 +275,12 @@ public class CalendarManager{
|
||||||
if (calendarDay != null){
|
if (calendarDay != null){
|
||||||
CalendarEpisode calEpisode = new CalendarEpisode();
|
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.DateTime = targetDate;
|
||||||
calEpisode.HasPassed = DateTime.Now > targetDate;
|
calEpisode.HasPassed = DateTime.Now > targetDate;
|
||||||
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
||||||
|
|
@ -282,12 +289,14 @@ public class CalendarManager{
|
||||||
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault()?.Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg
|
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault()?.Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg
|
||||||
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
|
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
|
||||||
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
|
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
|
||||||
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
calEpisode.SeasonName = seasonTitle;
|
||||||
calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode;
|
calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode;
|
||||||
calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId;
|
calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId;
|
||||||
|
calEpisode.FilteredOut = filtered;
|
||||||
|
calEpisode.AudioLocale = crBrowseEpisode.EpisodeMetadata.AudioLocale;
|
||||||
|
|
||||||
var existingEpisode = calendarDay.CalendarEpisodes
|
var existingEpisode = calendarDay.CalendarEpisodes
|
||||||
.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName);
|
.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName && e.AudioLocale == calEpisode.AudioLocale);
|
||||||
|
|
||||||
if (existingEpisode != null){
|
if (existingEpisode != null){
|
||||||
if (!int.TryParse(existingEpisode.EpisodeNumber, out _)){
|
if (!int.TryParse(existingEpisode.EpisodeNumber, out _)){
|
||||||
|
|
@ -330,8 +339,8 @@ public class CalendarManager{
|
||||||
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
|
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
|
||||||
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
||||||
|
|
||||||
foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date.Day == calendarEpisode.DateTime.Date.Day)
|
foreach (var calendarEpisode in list.Where(calendarEpisodeAnilist => calendarDay.DateTime.Date.Day == calendarEpisodeAnilist.DateTime.Date.Day)
|
||||||
.Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID && ele.SeasonName != calendarEpisode.SeasonName))){
|
.Where(calendarEpisodeAnilist => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisodeAnilist.CrSeriesID && ele.SeasonName != calendarEpisodeAnilist.SeasonName))){
|
||||||
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
calendarDay.CalendarEpisodes.Add(calendarEpisode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -342,6 +351,7 @@ public class CalendarManager{
|
||||||
foreach (var weekCalendarDay in week.CalendarDays){
|
foreach (var weekCalendarDay in week.CalendarDays){
|
||||||
if (weekCalendarDay.CalendarEpisodes.Count > 0)
|
if (weekCalendarDay.CalendarEpisodes.Count > 0)
|
||||||
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
|
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
|
||||||
|
.Where(e => !e.FilteredOut)
|
||||||
.OrderBy(e => e.AnilistEpisode) // False first, then true
|
.OrderBy(e => e.AnilistEpisode) // False first, then true
|
||||||
.ThenBy(e => e.DateTime)
|
.ThenBy(e => e.DateTime)
|
||||||
.ThenBy(e => e.SeasonName)
|
.ThenBy(e => e.SeasonName)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using CRD.Downloader.Crunchyroll.Utils;
|
||||||
using CRD.Utils;
|
using CRD.Utils;
|
||||||
using CRD.Utils.DRM;
|
using CRD.Utils.DRM;
|
||||||
using CRD.Utils.Ffmpeg_Encoding;
|
using CRD.Utils.Ffmpeg_Encoding;
|
||||||
|
|
@ -50,6 +51,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
|
|
||||||
public string DefaultLocale = "en-US";
|
public string DefaultLocale = "en-US";
|
||||||
|
public CrAuthSettings DefaultAndroidAuthSettings = new CrAuthSettings();
|
||||||
|
|
||||||
public JsonSerializerSettings? SettingsJsonSerializerSettings = new(){
|
public JsonSerializerSettings? SettingsJsonSerializerSettings = new(){
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
|
@ -119,6 +121,7 @@ public class CrunchyrollManager{
|
||||||
options.Timeout = 15000;
|
options.Timeout = 15000;
|
||||||
options.DubLang = new List<string>(){ "ja-JP" };
|
options.DubLang = new List<string>(){ "ja-JP" };
|
||||||
options.SimultaneousDownloads = 2;
|
options.SimultaneousDownloads = 2;
|
||||||
|
options.SimultaneousProcessingJobs = 2;
|
||||||
// options.AccentColor = Colors.SlateBlue.ToString();
|
// options.AccentColor = Colors.SlateBlue.ToString();
|
||||||
options.Theme = "System";
|
options.Theme = "System";
|
||||||
options.SelectedCalendarLanguage = "en-us";
|
options.SelectedCalendarLanguage = "en-us";
|
||||||
|
|
@ -128,6 +131,8 @@ public class CrunchyrollManager{
|
||||||
options.StreamEndpoint = "web/firefox";
|
options.StreamEndpoint = "web/firefox";
|
||||||
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
|
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
|
||||||
options.HistoryLang = DefaultLocale;
|
options.HistoryLang = DefaultLocale;
|
||||||
|
options.FixCccSubtitles = true;
|
||||||
|
options.ConvertVtt2Ass = true;
|
||||||
|
|
||||||
options.BackgroundImageOpacity = 0.5;
|
options.BackgroundImageOpacity = 0.5;
|
||||||
options.BackgroundImageBlurRadius = 10;
|
options.BackgroundImageBlurRadius = 10;
|
||||||
|
|
@ -194,24 +199,26 @@ public class CrunchyrollManager{
|
||||||
CfgManager.DisableLogMode();
|
CfgManager.DisableLogMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DefaultAndroidAuthSettings = new CrAuthSettings(){
|
||||||
|
Endpoint = "android/phone",
|
||||||
|
Authorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=",
|
||||||
|
UserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0",
|
||||||
|
Device_name = "CPH2449",
|
||||||
|
Device_type = "OnePlus CPH2449"
|
||||||
|
};
|
||||||
|
|
||||||
CrunOptions.StreamEndpoint = "tv/android_tv";
|
CrunOptions.StreamEndpoint = "tv/android_tv";
|
||||||
CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
|
CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
|
||||||
Endpoint = "tv/android_tv",
|
Endpoint = "tv/android_tv",
|
||||||
Authorization = "Basic Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=",
|
Authorization = "Basic ZGsxYndzemRyc3lkeTR1N2xvenE6bDl0SU1BdTlzTGc4ZjA4ajlfQkQ4eWZmQmZTSms0R0o=",
|
||||||
UserAgent = "ANDROIDTV/3.42.1_22273 Android/16",
|
UserAgent = "ANDROIDTV/3.47.0_22277 Android/16",
|
||||||
Device_name = "Android TV",
|
Device_name = "Android TV",
|
||||||
Device_type = "Android TV"
|
Device_type = "Android TV"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (CrunOptions.StreamEndpointSecondSettings == null){
|
if (CrunOptions.StreamEndpointSecondSettings == null){
|
||||||
CrunOptions.StreamEndpointSecondSettings = new CrAuthSettings(){
|
CrunOptions.StreamEndpointSecondSettings = DefaultAndroidAuthSettings;
|
||||||
Endpoint = "android/phone",
|
|
||||||
Authorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=",
|
|
||||||
UserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0",
|
|
||||||
Device_name = "CPH2449",
|
|
||||||
Device_type = "OnePlus CPH2449"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CrAuthEndpoint2.AuthSettings = CrunOptions.StreamEndpointSecondSettings;
|
CrAuthEndpoint2.AuthSettings = CrunOptions.StreamEndpointSecondSettings;
|
||||||
|
|
@ -312,43 +319,121 @@ public class CrunchyrollManager{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.DownloadAllowEarlyStart){
|
try{
|
||||||
QueueManager.Instance.DecrementDownloads();
|
if (options.DownloadAllowEarlyStart){
|
||||||
}
|
QueueManager.Instance.DecrementDownloads();
|
||||||
|
data.DownloadProgress = new DownloadProgress(){
|
||||||
if (options.SkipMuxing == false){
|
IsDownloading = true,
|
||||||
bool syncError = false;
|
Percent = 100,
|
||||||
bool muxError = false;
|
Time = 0,
|
||||||
var notSyncedDubs = "";
|
DownloadSpeed = 0,
|
||||||
|
Doing = "Waiting for Muxing/Encoding"
|
||||||
data.DownloadProgress = new DownloadProgress(){
|
};
|
||||||
IsDownloading = true,
|
QueueManager.Instance.Queue.Refresh();
|
||||||
Percent = 100,
|
await QueueManager.Instance.activeProcessingJobs.WaitAsync(data.Cts.Token);
|
||||||
Time = 0,
|
|
||||||
DownloadSpeed = 0,
|
|
||||||
Doing = "Muxing"
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueManager.Instance.Queue.Refresh();
|
|
||||||
|
|
||||||
if (options.MuxFonts){
|
|
||||||
await FontsManager.Instance.GetFontsAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileNameAndPath = options.DownloadToTempFolder
|
|
||||||
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
if (options.SkipMuxing == false){
|
||||||
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
bool syncError = false;
|
||||||
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
|
bool muxError = false;
|
||||||
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
|
var notSyncedDubs = "";
|
||||||
var mergers = new List<Merger>();
|
|
||||||
foreach (var keyValue in groupByDub){
|
data.DownloadProgress = new DownloadProgress(){
|
||||||
var result = await MuxStreams(keyValue.Value,
|
IsDownloading = true,
|
||||||
|
Percent = 100,
|
||||||
|
Time = 0,
|
||||||
|
DownloadSpeed = 0,
|
||||||
|
Doing = "Muxing"
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueManager.Instance.Queue.Refresh();
|
||||||
|
|
||||||
|
if (options.MuxFonts){
|
||||||
|
await FontsManager.Instance.GetFontsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileNameAndPath = options.DownloadToTempFolder
|
||||||
|
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
||||||
|
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
||||||
|
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
|
||||||
|
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
|
||||||
|
var mergers = new List<Merger>();
|
||||||
|
foreach (var keyValue in groupByDub){
|
||||||
|
var result = await MuxStreams(keyValue.Value,
|
||||||
|
new CrunchyMuxOptions{
|
||||||
|
DubLangList = options.DubLang,
|
||||||
|
SubLangList = options.DlSubs,
|
||||||
|
FfmpegOptions = options.FfmpegOptions,
|
||||||
|
SkipSubMux = options.SkipSubsMux,
|
||||||
|
Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}",
|
||||||
|
Mp4 = options.Mp4,
|
||||||
|
Mp3 = options.AudioOnlyToMp3,
|
||||||
|
MuxFonts = options.MuxFonts,
|
||||||
|
MuxCover = options.MuxCover,
|
||||||
|
VideoTitle = res.VideoTitle,
|
||||||
|
Novids = options.Novids,
|
||||||
|
NoCleanup = options.Nocleanup,
|
||||||
|
DefaultAudio = Languages.FindLang(options.DefaultAudio),
|
||||||
|
DefaultSub = Languages.FindLang(options.DefaultSub),
|
||||||
|
MkvmergeOptions = options.MkvmergeOptions,
|
||||||
|
ForceMuxer = options.Force,
|
||||||
|
SyncTiming = options.SyncTiming,
|
||||||
|
CcTag = options.CcTag,
|
||||||
|
KeepAllVideos = true,
|
||||||
|
MuxDescription = options.IncludeVideoDescription,
|
||||||
|
DlVideoOnce = options.DlVideoOnce,
|
||||||
|
DefaultSubSigns = options.DefaultSubSigns,
|
||||||
|
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
|
||||||
|
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
||||||
|
SignsSubsAsForced = options.SignsSubsAsForced,
|
||||||
|
},
|
||||||
|
fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", data);
|
||||||
|
|
||||||
|
if (result is{ merger: not null, isMuxed: true }){
|
||||||
|
mergers.Add(result.merger);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.isMuxed && !data.OnlySubs){
|
||||||
|
muxError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.syncError){
|
||||||
|
syncError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var merger in mergers){
|
||||||
|
merger.CleanUp();
|
||||||
|
|
||||||
|
if (options.IsEncodeEnabled){
|
||||||
|
data.DownloadProgress = new DownloadProgress(){
|
||||||
|
IsDownloading = true,
|
||||||
|
Percent = 100,
|
||||||
|
Time = 0,
|
||||||
|
DownloadSpeed = 0,
|
||||||
|
Doing = "Encoding"
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueManager.Instance.Queue.Refresh();
|
||||||
|
|
||||||
|
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
|
||||||
|
|
||||||
|
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.DownloadToTempFolder){
|
||||||
|
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.options.Subtitles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
var result = await MuxStreams(res.Data,
|
||||||
new CrunchyMuxOptions{
|
new CrunchyMuxOptions{
|
||||||
DubLangList = options.DubLang,
|
DubLangList = options.DubLang,
|
||||||
SubLangList = options.DlSubs,
|
SubLangList = options.DlSubs,
|
||||||
FfmpegOptions = options.FfmpegOptions,
|
FfmpegOptions = options.FfmpegOptions,
|
||||||
SkipSubMux = options.SkipSubsMux,
|
SkipSubMux = options.SkipSubsMux,
|
||||||
Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}",
|
Output = fileNameAndPath,
|
||||||
Mp4 = options.Mp4,
|
Mp4 = options.Mp4,
|
||||||
Mp3 = options.AudioOnlyToMp3,
|
Mp3 = options.AudioOnlyToMp3,
|
||||||
MuxFonts = options.MuxFonts,
|
MuxFonts = options.MuxFonts,
|
||||||
|
|
@ -370,25 +455,17 @@ public class CrunchyrollManager{
|
||||||
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
||||||
SignsSubsAsForced = options.SignsSubsAsForced,
|
SignsSubsAsForced = options.SignsSubsAsForced,
|
||||||
},
|
},
|
||||||
fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", data);
|
fileNameAndPath, data);
|
||||||
|
|
||||||
|
syncError = result.syncError;
|
||||||
|
notSyncedDubs = result.notSyncedDubs;
|
||||||
|
muxError = !result.isMuxed && !data.OnlySubs;
|
||||||
|
|
||||||
if (result is{ merger: not null, isMuxed: true }){
|
if (result is{ merger: not null, isMuxed: true }){
|
||||||
mergers.Add(result.merger);
|
result.merger.CleanUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.isMuxed){
|
if (options.IsEncodeEnabled && !muxError){
|
||||||
muxError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.syncError){
|
|
||||||
syncError = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var merger in mergers){
|
|
||||||
merger.CleanUp();
|
|
||||||
|
|
||||||
if (options.IsEncodeEnabled){
|
|
||||||
data.DownloadProgress = new DownloadProgress(){
|
data.DownloadProgress = new DownloadProgress(){
|
||||||
IsDownloading = true,
|
IsDownloading = true,
|
||||||
Percent = 100,
|
Percent = 100,
|
||||||
|
|
@ -400,117 +477,63 @@ public class CrunchyrollManager{
|
||||||
QueueManager.Instance.Queue.Refresh();
|
QueueManager.Instance.Queue.Refresh();
|
||||||
|
|
||||||
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
|
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
|
||||||
|
if (preset != null && result.merger != null) await Helpers.RunFFmpegWithPresetAsync(result.merger.options.Output, preset, data);
|
||||||
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.DownloadToTempFolder){
|
if (options.DownloadToTempFolder){
|
||||||
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
|
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, result.merger?.options.Subtitles ?? []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data.DownloadProgress = new DownloadProgress(){
|
||||||
|
IsDownloading = true,
|
||||||
|
Done = true,
|
||||||
|
Percent = 100,
|
||||||
|
Time = 0,
|
||||||
|
DownloadSpeed = 0,
|
||||||
|
Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? $" - Couldn't sync dubs ({notSyncedDubs})" : "")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (CrunOptions.RemoveFinishedDownload && !syncError){
|
||||||
|
QueueManager.Instance.Queue.Remove(data);
|
||||||
|
}
|
||||||
} else{
|
} else{
|
||||||
var result = await MuxStreams(res.Data,
|
Console.WriteLine("Skipping mux");
|
||||||
new CrunchyMuxOptions{
|
res.Data.ForEach(file => Helpers.DeleteFile(file.Path + ".resume"));
|
||||||
DubLangList = options.DubLang,
|
|
||||||
SubLangList = options.DlSubs,
|
|
||||||
FfmpegOptions = options.FfmpegOptions,
|
|
||||||
SkipSubMux = options.SkipSubsMux,
|
|
||||||
Output = fileNameAndPath,
|
|
||||||
Mp4 = options.Mp4,
|
|
||||||
Mp3 = options.AudioOnlyToMp3,
|
|
||||||
MuxFonts = options.MuxFonts,
|
|
||||||
MuxCover = options.MuxCover,
|
|
||||||
VideoTitle = res.VideoTitle,
|
|
||||||
Novids = options.Novids,
|
|
||||||
NoCleanup = options.Nocleanup,
|
|
||||||
DefaultAudio = Languages.FindLang(options.DefaultAudio),
|
|
||||||
DefaultSub = Languages.FindLang(options.DefaultSub),
|
|
||||||
MkvmergeOptions = options.MkvmergeOptions,
|
|
||||||
ForceMuxer = options.Force,
|
|
||||||
SyncTiming = options.SyncTiming,
|
|
||||||
CcTag = options.CcTag,
|
|
||||||
KeepAllVideos = true,
|
|
||||||
MuxDescription = options.IncludeVideoDescription,
|
|
||||||
DlVideoOnce = options.DlVideoOnce,
|
|
||||||
DefaultSubSigns = options.DefaultSubSigns,
|
|
||||||
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
|
|
||||||
CcSubsMuxingFlag = options.CcSubsMuxingFlag,
|
|
||||||
SignsSubsAsForced = options.SignsSubsAsForced,
|
|
||||||
},
|
|
||||||
fileNameAndPath, data);
|
|
||||||
|
|
||||||
syncError = result.syncError;
|
|
||||||
notSyncedDubs = result.notSyncedDubs;
|
|
||||||
muxError = !result.isMuxed;
|
|
||||||
|
|
||||||
if (result is{ merger: not null, isMuxed: true }){
|
|
||||||
result.merger.CleanUp();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.IsEncodeEnabled && !muxError){
|
|
||||||
data.DownloadProgress = new DownloadProgress(){
|
|
||||||
IsDownloading = true,
|
|
||||||
Percent = 100,
|
|
||||||
Time = 0,
|
|
||||||
DownloadSpeed = 0,
|
|
||||||
Doing = "Encoding"
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueManager.Instance.Queue.Refresh();
|
|
||||||
|
|
||||||
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
|
|
||||||
if (preset != null && result.merger != null) await Helpers.RunFFmpegWithPresetAsync(result.merger.options.Output, preset, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.DownloadToTempFolder){
|
if (options.DownloadToTempFolder){
|
||||||
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
|
if (string.IsNullOrEmpty(res.TempFolderPath) || !Directory.Exists(res.TempFolderPath)){
|
||||||
}
|
Console.WriteLine("Invalid or non-existent temp folder path.");
|
||||||
}
|
} else{
|
||||||
|
// Move files
|
||||||
|
foreach (var downloadedMedia in res.Data){
|
||||||
data.DownloadProgress = new DownloadProgress(){
|
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
|
||||||
IsDownloading = true,
|
}
|
||||||
Done = true,
|
|
||||||
Percent = 100,
|
|
||||||
Time = 0,
|
|
||||||
DownloadSpeed = 0,
|
|
||||||
Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? $" - Couldn't sync dubs ({notSyncedDubs})" : "")
|
|
||||||
};
|
|
||||||
|
|
||||||
if (CrunOptions.RemoveFinishedDownload && !syncError){
|
|
||||||
QueueManager.Instance.Queue.Remove(data);
|
|
||||||
}
|
|
||||||
} else{
|
|
||||||
Console.WriteLine("Skipping mux");
|
|
||||||
res.Data.ForEach(file => Helpers.DeleteFile(file.Path + ".resume"));
|
|
||||||
if (options.DownloadToTempFolder){
|
|
||||||
if (string.IsNullOrEmpty(res.TempFolderPath) || !Directory.Exists(res.TempFolderPath)){
|
|
||||||
Console.WriteLine("Invalid or non-existent temp folder path.");
|
|
||||||
} else{
|
|
||||||
// Move files
|
|
||||||
foreach (var downloadedMedia in res.Data){
|
|
||||||
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data.DownloadProgress = new DownloadProgress(){
|
data.DownloadProgress = new DownloadProgress(){
|
||||||
IsDownloading = true,
|
IsDownloading = true,
|
||||||
Done = true,
|
Done = true,
|
||||||
Percent = 100,
|
Percent = 100,
|
||||||
Time = 0,
|
Time = 0,
|
||||||
DownloadSpeed = 0,
|
DownloadSpeed = 0,
|
||||||
Doing = "Done - Skipped muxing"
|
Doing = "Done - Skipped muxing"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (CrunOptions.RemoveFinishedDownload){
|
if (CrunOptions.RemoveFinishedDownload){
|
||||||
QueueManager.Instance.Queue.Remove(data);
|
QueueManager.Instance.Queue.Remove(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (OperationCanceledException){
|
||||||
|
// expected when removed/canceled
|
||||||
|
} finally{
|
||||||
|
if (options.DownloadAllowEarlyStart) QueueManager.Instance.activeProcessingJobs.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!options.DownloadAllowEarlyStart){
|
if (!options.DownloadAllowEarlyStart){
|
||||||
QueueManager.Instance.IncrementDownloads();
|
QueueManager.Instance.DecrementDownloads();
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueManager.Instance.Queue.Refresh();
|
QueueManager.Instance.Queue.Refresh();
|
||||||
|
|
@ -525,6 +548,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (QueueManager.Instance.Queue.Count == 0 || QueueManager.Instance.Queue.All(e => e.DownloadProgress.Done)){
|
if (QueueManager.Instance.Queue.Count == 0 || QueueManager.Instance.Queue.All(e => e.DownloadProgress.Done)){
|
||||||
|
QueueManager.Instance.ResetDownloads();
|
||||||
try{
|
try{
|
||||||
var audioPath = CrunOptions.DownloadFinishedSoundPath;
|
var audioPath = CrunOptions.DownloadFinishedSoundPath;
|
||||||
if (!string.IsNullOrEmpty(audioPath)){
|
if (!string.IsNullOrEmpty(audioPath)){
|
||||||
|
|
@ -545,7 +569,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
#region Temp Files Move
|
#region Temp Files Move
|
||||||
|
|
||||||
private async Task MoveFromTempFolder(Merger? merger, CrunchyEpMeta data, CrDownloadOptions options, string tempFolderPath, IEnumerable<DownloadedMedia> subtitles){
|
private async Task MoveFromTempFolder(Merger? merger, CrunchyEpMeta data, CrDownloadOptions options, string tempFolderPath, List<SubtitleInput> subtitles){
|
||||||
if (!options.DownloadToTempFolder) return;
|
if (!options.DownloadToTempFolder) return;
|
||||||
|
|
||||||
data.DownloadProgress = new DownloadProgress{
|
data.DownloadProgress = new DownloadProgress{
|
||||||
|
|
@ -568,7 +592,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
// Move the subtitle files
|
// Move the subtitle files
|
||||||
foreach (var downloadedMedia in subtitles){
|
foreach (var downloadedMedia in subtitles){
|
||||||
await MoveFile(downloadedMedia.Path ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
|
await MoveFile(downloadedMedia.File ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -671,7 +695,8 @@ public class CrunchyrollManager{
|
||||||
SubLangList = options.SubLangList,
|
SubLangList = options.SubLangList,
|
||||||
OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty, Bitrate = a.bitrate }).ToList(),
|
OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty, Bitrate = a.bitrate }).ToList(),
|
||||||
SkipSubMux = options.SkipSubMux,
|
SkipSubMux = options.SkipSubMux,
|
||||||
OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty, Bitrate = a.bitrate }).ToList(),
|
OnlyAudio = data.Where(a => a.Type is DownloadMediaType.Audio or DownloadMediaType.AudioRoleDescription).Select(a => new MergerInput
|
||||||
|
{ Language = a.Lang, Path = a.Path ?? string.Empty, Bitrate = a.bitrate, IsAudioRoleDescription = (a.Type is DownloadMediaType.AudioRoleDescription) }).ToList(),
|
||||||
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
|
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
|
||||||
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
|
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
|
||||||
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
|
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
|
||||||
|
|
@ -878,8 +903,46 @@ public class CrunchyrollManager{
|
||||||
if (data.Data is{ Count: > 0 }){
|
if (data.Data is{ Count: > 0 }){
|
||||||
options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
|
options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
|
||||||
|
|
||||||
|
if (options.DownloadDescriptionAudio){
|
||||||
|
|
||||||
|
var alreadyAdr = new HashSet<string>(
|
||||||
|
data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err")
|
||||||
|
);
|
||||||
|
|
||||||
|
bool HasDescriptionRole(IEnumerable<string>? roles) =>
|
||||||
|
roles?.Any(r => string.Equals(r, "description", StringComparison.OrdinalIgnoreCase)) == true;
|
||||||
|
|
||||||
|
var toDuplicate = data.Data
|
||||||
|
.Where(m => !m.IsAudioRoleDescription)
|
||||||
|
.Where(m => !alreadyAdr.Contains(m.Lang?.CrLocale ?? "err"))
|
||||||
|
.Where(m => m.Versions?.Any(v => (v.AudioLocale == (m.Lang?.CrLocale ?? "err"))
|
||||||
|
&& HasDescriptionRole(v.roles)) == true)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var additions = toDuplicate.Select(m => new CrunchyEpMetaData{
|
||||||
|
MediaId = m.MediaId,
|
||||||
|
Lang = m.Lang,
|
||||||
|
Playback = m.Playback,
|
||||||
|
Versions = m.Versions,
|
||||||
|
IsSubbed = m.IsSubbed,
|
||||||
|
IsDubbed = m.IsDubbed,
|
||||||
|
IsAudioRoleDescription = true
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
data.Data.AddRange(additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var rank = options.DubLang
|
||||||
|
.Select((val, i) => new{ val, i })
|
||||||
|
.ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var sortedMetaData = data.Data
|
var sortedMetaData = data.Data
|
||||||
.OrderBy(metaData => options.DubLang.IndexOf(metaData.Lang?.CrLocale ?? string.Empty) != -1 ? options.DubLang.IndexOf(metaData.Lang?.CrLocale ?? string.Empty) : int.MaxValue)
|
.OrderBy(m => {
|
||||||
|
var key = m.Lang?.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();
|
.ToList();
|
||||||
|
|
||||||
data.Data = sortedMetaData;
|
data.Data = sortedMetaData;
|
||||||
|
|
@ -953,18 +1016,18 @@ public class CrunchyrollManager{
|
||||||
if (options.Chapters && !data.OnlySubs){
|
if (options.Chapters && !data.OnlySubs){
|
||||||
await ParseChapters(mediaGuid, compiledChapters);
|
await ParseChapters(mediaGuid, compiledChapters);
|
||||||
|
|
||||||
if (compiledChapters.Count == 0 && primaryVersion.MediaGuid != null && mediaGuid != primaryVersion.MediaGuid){
|
if (compiledChapters.Count == 0 && !string.IsNullOrEmpty(primaryVersion.Guid) && mediaGuid != primaryVersion.Guid){
|
||||||
Console.Error.WriteLine("Chapters empty trying to get original version chapters - might not match with video");
|
Console.Error.WriteLine("Chapters empty trying to get original version chapters - might not match with video");
|
||||||
await ParseChapters(primaryVersion.MediaGuid, compiledChapters);
|
await ParseChapters(primaryVersion.Guid, compiledChapters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music);
|
var fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
|
||||||
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
|
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
|
||||||
if (CrAuthEndpoint2.Profile.Username != "???"){
|
if (CrAuthEndpoint2.Profile.Username != "???"){
|
||||||
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music);
|
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchPlaybackData.IsOk){
|
if (!fetchPlaybackData.IsOk){
|
||||||
|
|
@ -1058,7 +1121,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
if (pbStreams?.Keys != null){
|
if (pbStreams?.Keys != null){
|
||||||
var pb = pbStreams.Select(v => {
|
var pb = pbStreams.Select(v => {
|
||||||
if (v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
|
if (v.Key != "none" && v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
|
||||||
hsLangs.Add(v.Value.HardsubLang.CrLocale);
|
hsLangs.Add(v.Value.HardsubLang.CrLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1309,22 +1372,27 @@ public class CrunchyrollManager{
|
||||||
language = item.language,
|
language = item.language,
|
||||||
bandwidth = item.bandwidth,
|
bandwidth = item.bandwidth,
|
||||||
audioSamplingRate = item.audioSamplingRate,
|
audioSamplingRate = item.audioSamplingRate,
|
||||||
resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s"
|
resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s",
|
||||||
|
resolutionTextSnap = $"{Helpers.SnapToAudioBucket(Helpers.ToKbps(item.bandwidth))}kB/s",
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Video: Remove duplicates by resolution (width, height), keep highest bandwidth, then sort
|
// ---------- VIDEO: dedupe & sort ----------
|
||||||
videos = videos
|
videos = videos
|
||||||
.GroupBy(v => new{ v.quality.width, v.quality.height })
|
.GroupBy(v => new{ v.quality.height, WB = Helpers.WidthBucket(v.quality.width, v.quality.height) })
|
||||||
.Select(g => g.OrderByDescending(v => v.bandwidth).First())
|
.Select(g => g.OrderByDescending(v => v.bandwidth).First())
|
||||||
.OrderBy(v => v.quality.width)
|
.OrderBy(v => v.quality.height)
|
||||||
.ThenBy(v => v.bandwidth)
|
.ThenBy(v => v.bandwidth)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Audio: Remove duplicates, then sort by bandwidth
|
// ---------- AUDIO: dedupe & sort ----------
|
||||||
audios = audios
|
audios = audios
|
||||||
.GroupBy(a => new{ a.bandwidth, a.language })
|
.Select(a => new{ Item = a, Lang = string.IsNullOrWhiteSpace(a.language?.CrLocale) ? "und" : a.language.CrLocale, Bucket = Helpers.SnapToAudioBucket(Helpers.ToKbps(a.bandwidth)) })
|
||||||
.Select(g => g.OrderByDescending(x => x.audioSamplingRate).First())
|
.GroupBy(x => new{ x.Lang, x.Bucket })
|
||||||
.OrderBy(a => a.bandwidth)
|
.Select(g => g.OrderByDescending(x => x.Item.@default)
|
||||||
|
.ThenByDescending(x => x.Item.audioSamplingRate)
|
||||||
|
.ThenByDescending(x => x.Item.bandwidth)
|
||||||
|
.First().Item)
|
||||||
|
.OrderBy(a => Helpers.ToKbps(a.bandwidth))
|
||||||
.ThenBy(a => a.audioSamplingRate)
|
.ThenBy(a => a.audioSamplingRate)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|
@ -1363,7 +1431,7 @@ public class CrunchyrollManager{
|
||||||
} else if (options.QualityAudio == "worst"){
|
} else if (options.QualityAudio == "worst"){
|
||||||
chosenAudioQuality = 1;
|
chosenAudioQuality = 1;
|
||||||
} else{
|
} else{
|
||||||
var tempIndex = audios.FindIndex(a => a.resolutionText == options.QualityAudio);
|
var tempIndex = audios.FindIndex(a => a.resolutionTextSnap == options.QualityAudio);
|
||||||
if (tempIndex < 0){
|
if (tempIndex < 0){
|
||||||
chosenAudioQuality = audios.Count;
|
chosenAudioQuality = audios.Count;
|
||||||
} else{
|
} else{
|
||||||
|
|
@ -1386,7 +1454,7 @@ public class CrunchyrollManager{
|
||||||
foreach (var server in streamServers){
|
foreach (var server in streamServers){
|
||||||
Console.WriteLine($"\t{server}");
|
Console.WriteLine($"\t{server}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("Available Video Qualities:");
|
sb.AppendLine("Available Video Qualities:");
|
||||||
for (int i = 0; i < videos.Count; i++){
|
for (int i = 0; i < videos.Count; i++){
|
||||||
|
|
@ -1401,7 +1469,7 @@ public class CrunchyrollManager{
|
||||||
variables.Add(new Variable("height", chosenVideoSegments.quality.height, false));
|
variables.Add(new Variable("height", chosenVideoSegments.quality.height, false));
|
||||||
variables.Add(new Variable("width", chosenVideoSegments.quality.width, false));
|
variables.Add(new Variable("width", chosenVideoSegments.quality.width, false));
|
||||||
if (string.IsNullOrEmpty(data.Resolution)) data.Resolution = chosenVideoSegments.quality.height + "p";
|
if (string.IsNullOrEmpty(data.Resolution)) data.Resolution = chosenVideoSegments.quality.height + "p";
|
||||||
|
|
||||||
|
|
||||||
LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.CrLocale == curStream.AudioLang.CrLocale);
|
LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.CrLocale == curStream.AudioLang.CrLocale);
|
||||||
if (lang == null){
|
if (lang == null){
|
||||||
|
|
@ -1423,10 +1491,8 @@ public class CrunchyrollManager{
|
||||||
string qualityConsoleLog = sb.ToString();
|
string qualityConsoleLog = sb.ToString();
|
||||||
Console.WriteLine(qualityConsoleLog);
|
Console.WriteLine(qualityConsoleLog);
|
||||||
data.AvailableQualities = qualityConsoleLog;
|
data.AvailableQualities = qualityConsoleLog;
|
||||||
|
|
||||||
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
|
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.FileNameWhitespaceSubstitute, options.Override).ToArray());
|
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.FileNameWhitespaceSubstitute, options.Override).ToArray());
|
||||||
|
|
@ -1460,7 +1526,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
//string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
|
//string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
|
||||||
string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale);
|
string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale) + (epMeta.IsAudioRoleDescription ? ".AD" : "");
|
||||||
|
|
||||||
string tempFile = Path.Combine(FileNameManager
|
string tempFile = Path.Combine(FileNameManager
|
||||||
.ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.FileNameWhitespaceSubstitute,
|
.ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.FileNameWhitespaceSubstitute,
|
||||||
|
|
@ -1647,8 +1713,8 @@ public class CrunchyrollManager{
|
||||||
commandVideo, tempTsFileWorkDir);
|
commandVideo, tempTsFileWorkDir);
|
||||||
|
|
||||||
if (!decryptVideo.IsOk){
|
if (!decryptVideo.IsOk){
|
||||||
Console.Error.WriteLine($"Decryption failed with exit code {decryptVideo.ErrorCode}");
|
Console.Error.WriteLine($"Decryption failed with exit code {decryptVideo.ErrorCode}\n" + (shaka ? "Downgrade to Shaka-Packager v2.6.1 or use mp4decrypt" : ""));
|
||||||
MainWindow.Instance.ShowError($"Decryption failed with exit code {decryptVideo.ErrorCode}");
|
MainWindow.Instance.ShowError($"Decryption failed with exit code {decryptVideo.ErrorCode}\n" + (shaka ? "Downgrade to Shaka-Packager v2.6.1 or use mp4decrypt" : ""));
|
||||||
try{
|
try{
|
||||||
File.Move($"{tempTsFile}.video.enc.m4s", $"{tsFile}.video.enc.m4s");
|
File.Move($"{tempTsFile}.video.enc.m4s", $"{tsFile}.video.enc.m4s");
|
||||||
} catch (IOException ex){
|
} catch (IOException ex){
|
||||||
|
|
@ -1660,7 +1726,7 @@ public class CrunchyrollManager{
|
||||||
Data = files,
|
Data = files,
|
||||||
Error = dlFailed,
|
Error = dlFailed,
|
||||||
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
|
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
|
||||||
ErrorText = "Decryption failed"
|
ErrorText = (shaka ? "[SHAKA]" : "[MP4Decrypt]") + " Decryption failed"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1718,7 +1784,8 @@ public class CrunchyrollManager{
|
||||||
commandAudio, tempTsFileWorkDir);
|
commandAudio, tempTsFileWorkDir);
|
||||||
|
|
||||||
if (!decryptAudio.IsOk){
|
if (!decryptAudio.IsOk){
|
||||||
Console.Error.WriteLine($"Decryption failed with exit code {decryptAudio.ErrorCode}");
|
Console.Error.WriteLine($"Decryption failed with exit code {decryptAudio.ErrorCode}\n" + (shaka ? "Downgrade to Shaka-Packager v2.6.1 or use mp4decrypt" : ""));
|
||||||
|
MainWindow.Instance.ShowError($"Decryption failed with exit code {decryptAudio.ErrorCode}\n" + (shaka ? "Downgrade to Shaka-Packager v2.6.1 or use mp4decrypt" : ""));
|
||||||
try{
|
try{
|
||||||
File.Move($"{tempTsFile}.audio.enc.m4s", $"{tsFile}.audio.enc.m4s");
|
File.Move($"{tempTsFile}.audio.enc.m4s", $"{tsFile}.audio.enc.m4s");
|
||||||
} catch (IOException ex){
|
} catch (IOException ex){
|
||||||
|
|
@ -1730,7 +1797,7 @@ public class CrunchyrollManager{
|
||||||
Data = files,
|
Data = files,
|
||||||
Error = dlFailed,
|
Error = dlFailed,
|
||||||
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
|
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
|
||||||
ErrorText = "Decryption failed"
|
ErrorText = (shaka ? "[SHAKA]" : "[MP4Decrypt]") + " Decryption failed"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1761,7 +1828,7 @@ public class CrunchyrollManager{
|
||||||
}
|
}
|
||||||
|
|
||||||
files.Add(new DownloadedMedia{
|
files.Add(new DownloadedMedia{
|
||||||
Type = DownloadMediaType.Audio,
|
Type = epMeta.IsAudioRoleDescription ? DownloadMediaType.AudioRoleDescription : DownloadMediaType.Audio,
|
||||||
Path = $"{tsFile}.audio.m4s",
|
Path = $"{tsFile}.audio.m4s",
|
||||||
Lang = lang,
|
Lang = lang,
|
||||||
IsPrimary = isPrimary,
|
IsPrimary = isPrimary,
|
||||||
|
|
@ -1791,7 +1858,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
if (audioDownloaded){
|
if (audioDownloaded){
|
||||||
files.Add(new DownloadedMedia{
|
files.Add(new DownloadedMedia{
|
||||||
Type = DownloadMediaType.Audio,
|
Type = epMeta.IsAudioRoleDescription ? DownloadMediaType.AudioRoleDescription : DownloadMediaType.Audio,
|
||||||
Path = $"{tsFile}.audio.m4s",
|
Path = $"{tsFile}.audio.m4s",
|
||||||
Lang = lang,
|
Lang = lang,
|
||||||
IsPrimary = isPrimary,
|
IsPrimary = isPrimary,
|
||||||
|
|
@ -2045,25 +2112,13 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
if (subsAssReqResponse.IsOk){
|
if (subsAssReqResponse.IsOk){
|
||||||
if (subsItem.format == "ass"){
|
if (subsItem.format == "ass"){
|
||||||
var sBodySplit = subsAssReqResponse.ResponseContent.Split(new[]{ "\r\n" }, StringSplitOptions.None).ToList();
|
subsAssReqResponse.ResponseContent =
|
||||||
|
SubtitleUtils.CleanAssAndEnsureScriptInfo(subsAssReqResponse.ResponseContent, options, langItem);
|
||||||
|
|
||||||
if (sBodySplit.Count > 2){
|
sxData.Title = $"{langItem.Name}";
|
||||||
if (options.SubsAddScaledBorder == ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes){
|
var keysList = FontsManager.ExtractFontsFromAss(subsAssReqResponse.ResponseContent);
|
||||||
sBodySplit.Insert(2, "ScaledBorderAndShadow: yes");
|
sxData.Fonts = FontsManager.Instance.GetDictFromKeyList(keysList);
|
||||||
} else if (options.SubsAddScaledBorder == ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo){
|
} else if (subsItem.format == "vtt" && options.ConvertVtt2Ass){
|
||||||
sBodySplit.Insert(2, "ScaledBorderAndShadow: no");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subsAssReqResponse.ResponseContent = string.Join("\r\n", sBodySplit);
|
|
||||||
|
|
||||||
if (sBodySplit.Count > 1){
|
|
||||||
sxData.Title = sBodySplit[1].Replace("Title: ", "");
|
|
||||||
sxData.Title = $"{langItem.Language} / {sxData.Title}";
|
|
||||||
var keysList = FontsManager.ExtractFontsFromAss(subsAssReqResponse.ResponseContent);
|
|
||||||
sxData.Fonts = FontsManager.Instance.GetDictFromKeyList(keysList);
|
|
||||||
}
|
|
||||||
} else if (subsItem.format == "vtt"){
|
|
||||||
var assBuilder = new StringBuilder();
|
var assBuilder = new StringBuilder();
|
||||||
|
|
||||||
assBuilder.AppendLine("[Script Info]");
|
assBuilder.AppendLine("[Script Info]");
|
||||||
|
|
@ -2090,30 +2145,50 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
// Parse the VTT content
|
// Parse the VTT content
|
||||||
string normalizedContent = subsAssReqResponse.ResponseContent.Replace("\r\n", "\n").Replace("\r", "\n");
|
string normalizedContent = subsAssReqResponse.ResponseContent.Replace("\r\n", "\n").Replace("\r", "\n");
|
||||||
var blocks = normalizedContent.Split(new[]{ "\n\n" }, StringSplitOptions.RemoveEmptyEntries);
|
var timePattern = new Regex(
|
||||||
Regex timePattern = new Regex(@"(?<start>\d{2}:\d{2}:\d{2}\.\d{3})\s-->\s(?<end>\d{2}:\d{2}:\d{2}\.\d{3})");
|
@"^(?<start>(?:\d{2}:)?\d{2}:\d{2}\.\d{3})\s*-->\s*(?<end>(?:\d{2}:)?\d{2}:\d{2}\.\d{3})(?:\s+.+)?$",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
foreach (var block in blocks){
|
var lines = normalizedContent.Split('\n');
|
||||||
// Split each block into lines
|
int i = 0;
|
||||||
var lines = block.Split(new[]{ '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
if (lines.Length < 3) continue; // Skip blocks that don't have enough lines
|
while (i < lines.Length){
|
||||||
|
var line = lines[i].TrimEnd();
|
||||||
|
if (string.IsNullOrWhiteSpace(line) || line.Equals("WEBVTT", StringComparison.OrdinalIgnoreCase) || line.Equals("STYLE", StringComparison.OrdinalIgnoreCase)){
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Match the first line to get the time codes
|
int timeLineIndex = -1;
|
||||||
Match match = timePattern.Match(lines[1]);
|
|
||||||
|
|
||||||
if (match.Success){
|
if (timePattern.IsMatch(line)){
|
||||||
string startTime = Helpers.ConvertTimeFormat(match.Groups["start"].Value);
|
timeLineIndex = i;
|
||||||
string endTime = Helpers.ConvertTimeFormat(match.Groups["end"].Value);
|
} else if (i + 1 < lines.Length && timePattern.IsMatch(lines[i + 1].TrimEnd())){
|
||||||
|
timeLineIndex = i + 1;
|
||||||
|
} else{
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Join the remaining lines for dialogue, using \N for line breaks
|
var match = timePattern.Match(lines[timeLineIndex].TrimEnd());
|
||||||
string dialogue = string.Join("\\N", lines.Skip(2));
|
string startAss = Helpers.ConvertTimeFormat(match.Groups["start"].Value);
|
||||||
|
string endAss = Helpers.ConvertTimeFormat(match.Groups["end"].Value);
|
||||||
|
|
||||||
|
int textStart = timeLineIndex + 1;
|
||||||
|
var textLines = new List<string>();
|
||||||
|
while (textStart < lines.Length && !string.IsNullOrWhiteSpace(lines[textStart])){
|
||||||
|
textLines.Add(lines[textStart].TrimEnd());
|
||||||
|
textStart++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textLines.Count > 0){
|
||||||
|
string dialogue = string.Join("\\N", textLines);
|
||||||
dialogue = Helpers.ConvertVTTStylesToASS(dialogue);
|
dialogue = Helpers.ConvertVTTStylesToASS(dialogue);
|
||||||
|
|
||||||
// Append dialogue to ASS
|
assBuilder.AppendLine($"Dialogue: 0,{startAss},{endAss},Default,,0000,0000,0000,,{dialogue}");
|
||||||
assBuilder.AppendLine($"Dialogue: 0,{startTime},{endTime},Default,,0000,0000,0000,,{dialogue}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i = textStart + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
subsAssReqResponse.ResponseContent = assBuilder.ToString();
|
subsAssReqResponse.ResponseContent = assBuilder.ToString();
|
||||||
|
|
@ -2273,7 +2348,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
#region Fetch Playback Data
|
#region Fetch Playback Data
|
||||||
|
|
||||||
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music){
|
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music, bool auioRoleDesc){
|
||||||
var temppbData = new PlaybackData{
|
var temppbData = new PlaybackData{
|
||||||
Total = 0,
|
Total = 0,
|
||||||
Data = new Dictionary<string, StreamDetails>()
|
Data = new Dictionary<string, StreamDetails>()
|
||||||
|
|
@ -2281,7 +2356,7 @@ public class CrunchyrollManager{
|
||||||
|
|
||||||
await authEndpoint.RefreshToken(true);
|
await authEndpoint.RefreshToken(true);
|
||||||
|
|
||||||
var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{authEndpoint.AuthSettings.Endpoint}/play?queue=false";
|
var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{authEndpoint.AuthSettings.Endpoint}/play{(auioRoleDesc ? "?audioRole=description" : "")}";
|
||||||
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint);
|
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint);
|
||||||
|
|
||||||
if (!playbackRequestResponse.IsOk){
|
if (!playbackRequestResponse.IsOk){
|
||||||
|
|
@ -2292,7 +2367,7 @@ public class CrunchyrollManager{
|
||||||
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
|
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
|
||||||
} else{
|
} else{
|
||||||
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
|
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
|
||||||
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play";
|
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play{(auioRoleDesc ? "?audioRole=description" : "")}";
|
||||||
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint);
|
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint);
|
||||||
|
|
||||||
if (!playbackRequestResponse.IsOk){
|
if (!playbackRequestResponse.IsOk){
|
||||||
|
|
|
||||||
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]
|
[ObservableProperty]
|
||||||
private bool _downloadAudio = true;
|
private bool _downloadAudio = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _downloadDescriptionAudio = true;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _downloadChapters = true;
|
private bool _downloadChapters = true;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _addScaledBorderAndShadow;
|
private bool _addScaledBorderAndShadow;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _fixCccSubtitles;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _subsDownloadDuplicate;
|
private bool _subsDownloadDuplicate;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _includeSignSubs;
|
private bool _includeSignSubs;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _includeCcSubs;
|
private bool _includeCcSubs;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _convertVtt2Ass;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _showVtt2AssSettings;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedScaledBorderAndShadow;
|
private ComboBoxItem _selectedScaledBorderAndShadow;
|
||||||
|
|
||||||
|
|
@ -63,13 +75,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _muxToMp4;
|
private bool _muxToMp4;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _muxToMp3;
|
private bool _muxToMp3;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _muxFonts;
|
private bool _muxFonts;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _muxCover;
|
private bool _muxCover;
|
||||||
|
|
||||||
|
|
@ -102,7 +114,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _fileName = "";
|
private string _fileName = "";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _fileNameWhitespaceSubstitute = "";
|
private string _fileNameWhitespaceSubstitute = "";
|
||||||
|
|
||||||
|
|
@ -138,22 +150,28 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _selectedStreamEndpoint;
|
private ComboBoxItem _selectedStreamEndpoint;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ComboBoxItem _SelectedStreamEndpointSecondary;
|
private ComboBoxItem _SelectedStreamEndpointSecondary;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointAuthorization = "";
|
private string _endpointAuthorization = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _endpointClientId = "";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointUserAgent = "";
|
private string _endpointUserAgent = "";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointDeviceName = "";
|
private string _endpointDeviceName = "";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _endpointDeviceType = "";
|
private string _endpointDeviceType = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isLoggingIn;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _endpointNotSignedWarning;
|
private bool _endpointNotSignedWarning;
|
||||||
|
|
||||||
|
|
@ -187,6 +205,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } =[
|
public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } =[
|
||||||
new(){ Content = "best" },
|
new(){ Content = "best" },
|
||||||
|
new(){ Content = "192kB/s" },
|
||||||
new(){ Content = "128kB/s" },
|
new(){ Content = "128kB/s" },
|
||||||
new(){ Content = "96kB/s" },
|
new(){ Content = "96kB/s" },
|
||||||
new(){ Content = "64kB/s" },
|
new(){ Content = "64kB/s" },
|
||||||
|
|
@ -240,7 +259,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
new(){ Content = "tv/vidaa" },
|
new(){ Content = "tv/vidaa" },
|
||||||
new(){ Content = "tv/android_tv" },
|
new(){ Content = "tv/android_tv" },
|
||||||
];
|
];
|
||||||
|
|
||||||
public ObservableCollection<ComboBoxItem> StreamEndpointsSecondary{ get; } =[
|
public ObservableCollection<ComboBoxItem> StreamEndpointsSecondary{ get; } =[
|
||||||
new(){ Content = "" },
|
new(){ Content = "" },
|
||||||
// new(){ Content = "web/firefox" },
|
// new(){ Content = "web/firefox" },
|
||||||
|
|
@ -331,16 +350,17 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
|
|
||||||
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
|
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
|
||||||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||||
|
|
||||||
EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty;
|
EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty;
|
||||||
|
EndpointClientId = options.StreamEndpointSecondSettings?.Client_ID ?? string.Empty;
|
||||||
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
|
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
|
||||||
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
|
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
|
||||||
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty;
|
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty;
|
||||||
|
|
||||||
if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
|
if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
|
||||||
EndpointNotSignedWarning = true;
|
EndpointNotSignedWarning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions());
|
||||||
|
|
||||||
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
StringItemWithDisplayName? hwAccellFlag = FFmpegHWAccel.FirstOrDefault(a => a.value == options.FfmpegHwAccelFlag) ?? null;
|
||||||
|
|
@ -351,7 +371,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
.Where(a => options.DlSubs.Contains(a.Content))
|
.Where(a => options.DlSubs.Contains(a.Content))
|
||||||
.OrderBy(a => options.DlSubs.IndexOf(a.Content))
|
.OrderBy(a => options.DlSubs.IndexOf(a.Content))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
SelectedSubLang.Clear();
|
SelectedSubLang.Clear();
|
||||||
foreach (var listBoxItem in softSubLang){
|
foreach (var listBoxItem in softSubLang){
|
||||||
SelectedSubLang.Add(listBoxItem);
|
SelectedSubLang.Add(listBoxItem);
|
||||||
|
|
@ -361,7 +381,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
.Where(a => options.DubLang.Contains(a.Content))
|
.Where(a => options.DubLang.Contains(a.Content))
|
||||||
.OrderBy(a => options.DubLang.IndexOf(a.Content))
|
.OrderBy(a => options.DubLang.IndexOf(a.Content))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
SelectedDubLang.Clear();
|
SelectedDubLang.Clear();
|
||||||
foreach (var listBoxItem in dubLang){
|
foreach (var listBoxItem in dubLang){
|
||||||
SelectedDubLang.Add(listBoxItem);
|
SelectedDubLang.Add(listBoxItem);
|
||||||
|
|
@ -370,6 +390,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
|
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
|
||||||
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
|
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
|
||||||
|
|
||||||
|
FixCccSubtitles = options.FixCccSubtitles;
|
||||||
|
ConvertVtt2Ass = options.ConvertVtt2Ass;
|
||||||
SubsDownloadDuplicate = options.SubsDownloadDuplicate;
|
SubsDownloadDuplicate = options.SubsDownloadDuplicate;
|
||||||
MarkAsWatched = options.MarkAsWatched;
|
MarkAsWatched = options.MarkAsWatched;
|
||||||
DownloadFirstAvailableDub = options.DownloadFirstAvailableDub;
|
DownloadFirstAvailableDub = options.DownloadFirstAvailableDub;
|
||||||
|
|
@ -388,6 +410,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
IncludeCcSubs = options.IncludeCcSubs;
|
IncludeCcSubs = options.IncludeCcSubs;
|
||||||
DownloadVideo = !options.Novids;
|
DownloadVideo = !options.Novids;
|
||||||
DownloadAudio = !options.Noaudio;
|
DownloadAudio = !options.Noaudio;
|
||||||
|
DownloadDescriptionAudio = options.DownloadDescriptionAudio;
|
||||||
DownloadVideoForEveryDub = !options.DlVideoOnce;
|
DownloadVideoForEveryDub = !options.DlVideoOnce;
|
||||||
KeepDubsSeparate = options.KeepDubsSeperate;
|
KeepDubsSeparate = options.KeepDubsSeperate;
|
||||||
DownloadChapters = options.Chapters;
|
DownloadChapters = options.Chapters;
|
||||||
|
|
@ -428,6 +451,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
|
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
|
||||||
SelectedSubs = string.Join(", ", subs) ?? "";
|
SelectedSubs = string.Join(", ", subs) ?? "";
|
||||||
|
|
||||||
|
ShowVtt2AssSettings = IncludeCcSubs && ConvertVtt2Ass;
|
||||||
|
|
||||||
SelectedSubLang.CollectionChanged += Changes;
|
SelectedSubLang.CollectionChanged += Changes;
|
||||||
SelectedDubLang.CollectionChanged += Changes;
|
SelectedDubLang.CollectionChanged += Changes;
|
||||||
|
|
||||||
|
|
@ -443,6 +468,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.SubsDownloadDuplicate = SubsDownloadDuplicate;
|
CrunchyrollManager.Instance.CrunOptions.SubsDownloadDuplicate = SubsDownloadDuplicate;
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.ConvertVtt2Ass = ConvertVtt2Ass;
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.FixCccSubtitles = FixCccSubtitles;
|
||||||
CrunchyrollManager.Instance.CrunOptions.MarkAsWatched = MarkAsWatched;
|
CrunchyrollManager.Instance.CrunOptions.MarkAsWatched = MarkAsWatched;
|
||||||
CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub = DownloadFirstAvailableDub;
|
CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub = DownloadFirstAvailableDub;
|
||||||
CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi = UseCrBetaApi;
|
CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi = UseCrBetaApi;
|
||||||
|
|
@ -457,6 +484,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
CrunchyrollManager.Instance.CrunOptions.VideoTitle = FileTitle;
|
CrunchyrollManager.Instance.CrunOptions.VideoTitle = FileTitle;
|
||||||
CrunchyrollManager.Instance.CrunOptions.Novids = !DownloadVideo;
|
CrunchyrollManager.Instance.CrunOptions.Novids = !DownloadVideo;
|
||||||
CrunchyrollManager.Instance.CrunOptions.Noaudio = !DownloadAudio;
|
CrunchyrollManager.Instance.CrunOptions.Noaudio = !DownloadAudio;
|
||||||
|
CrunchyrollManager.Instance.CrunOptions.DownloadDescriptionAudio = DownloadDescriptionAudio;
|
||||||
CrunchyrollManager.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub;
|
CrunchyrollManager.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub;
|
||||||
CrunchyrollManager.Instance.CrunOptions.KeepDubsSeperate = KeepDubsSeparate;
|
CrunchyrollManager.Instance.CrunOptions.KeepDubsSeperate = KeepDubsSeparate;
|
||||||
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
|
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
|
||||||
|
|
@ -489,7 +517,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
string descLang = SelectedDescriptionLang.Content + "";
|
string descLang = SelectedDescriptionLang.Content + "";
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
|
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
|
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
|
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
|
||||||
|
|
@ -501,12 +529,15 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
var endpointSettings = new CrAuthSettings();
|
var endpointSettings = new CrAuthSettings();
|
||||||
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
|
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
|
||||||
endpointSettings.Authorization = EndpointAuthorization;
|
endpointSettings.Authorization = EndpointAuthorization;
|
||||||
|
endpointSettings.Client_ID = EndpointClientId;
|
||||||
endpointSettings.UserAgent = EndpointUserAgent;
|
endpointSettings.UserAgent = EndpointUserAgent;
|
||||||
endpointSettings.Device_name = EndpointDeviceName;
|
endpointSettings.Device_name = EndpointDeviceName;
|
||||||
endpointSettings.Device_type = EndpointDeviceType;
|
endpointSettings.Device_type = EndpointDeviceType;
|
||||||
|
|
||||||
|
|
||||||
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
|
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
|
||||||
|
CrunchyrollManager.Instance.CrAuthEndpoint2.AuthSettings = endpointSettings;
|
||||||
|
|
||||||
|
|
||||||
List<string> dubLangs = new List<string>();
|
List<string> dubLangs = new List<string>();
|
||||||
foreach (var listBoxItem in SelectedDubLang){
|
foreach (var listBoxItem in SelectedDubLang){
|
||||||
|
|
@ -609,6 +640,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSettings();
|
UpdateSettings();
|
||||||
|
ShowVtt2AssSettings = IncludeCcSubs && ConvertVtt2Ass;
|
||||||
|
|
||||||
if (e.PropertyName is nameof(History)){
|
if (e.PropertyName is nameof(History)){
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.History){
|
if (CrunchyrollManager.Instance.CrunOptions.History){
|
||||||
|
|
@ -669,13 +701,14 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
public void ResetEndpointSettings(){
|
public void ResetEndpointSettings(){
|
||||||
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == ("android/phone")) ?? null;
|
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == ("android/phone")) ?? null;
|
||||||
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0];
|
||||||
|
|
||||||
EndpointAuthorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=";
|
EndpointAuthorization = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Authorization;
|
||||||
EndpointUserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0";
|
EndpointClientId = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Client_ID;
|
||||||
EndpointDeviceName = "CPH2449";
|
EndpointUserAgent = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.UserAgent;
|
||||||
EndpointDeviceType = "OnePlus CPH2449";
|
EndpointDeviceName = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_name;
|
||||||
|
EndpointDeviceType = CrunchyrollManager.Instance.DefaultAndroidAuthSettings.Device_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task Login(){
|
public async Task Login(){
|
||||||
var dialog = new ContentDialog(){
|
var dialog = new ContentDialog(){
|
||||||
|
|
@ -690,9 +723,10 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
};
|
};
|
||||||
|
|
||||||
_ = await dialog.ShowAsync();
|
_ = await dialog.ShowAsync();
|
||||||
|
IsLoggingIn = true;
|
||||||
|
await viewModel.LoginCompleted;
|
||||||
|
IsLoggingIn = false;
|
||||||
EndpointNotSignedWarning = CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???";
|
EndpointNotSignedWarning = CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<StringItemWithDisplayName> GetAvailableHWAccelOptions(){
|
private List<StringItemWithDisplayName> GetAvailableHWAccelOptions(){
|
||||||
|
|
@ -706,7 +740,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
process.StartInfo.CreateNoWindow = true;
|
process.StartInfo.CreateNoWindow = true;
|
||||||
|
|
||||||
string output = string.Empty;
|
string output = string.Empty;
|
||||||
|
|
||||||
process.OutputDataReceived += (sender, e) => {
|
process.OutputDataReceived += (sender, e) => {
|
||||||
if (!string.IsNullOrEmpty(e.Data)){
|
if (!string.IsNullOrEmpty(e.Data)){
|
||||||
output += e.Data + Environment.NewLine;
|
output += e.Data + Environment.NewLine;
|
||||||
|
|
@ -714,7 +748,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
||||||
};
|
};
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
process.BeginOutputReadLine();
|
process.BeginOutputReadLine();
|
||||||
// process.BeginErrorReadLine();
|
// process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,12 @@
|
||||||
|
|
||||||
</controls:SettingsExpander.Footer>
|
</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>
|
</controls:SettingsExpander>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -105,6 +111,12 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsExpander.Footer>
|
</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 Content="Download Duplicate" Description="Download subtitles from all dubs where they're available">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<CheckBox IsChecked="{Binding SubsDownloadDuplicate}"> </CheckBox>
|
<CheckBox IsChecked="{Binding SubsDownloadDuplicate}"> </CheckBox>
|
||||||
|
|
@ -187,7 +199,13 @@
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</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>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||||
Text="{Binding CCSubsFont}" />
|
Text="{Binding CCSubsFont}" />
|
||||||
|
|
@ -254,6 +272,12 @@
|
||||||
Text="{Binding EndpointAuthorization}" />
|
Text="{Binding EndpointAuthorization}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Margin="0,5">
|
||||||
|
<TextBlock Text="Client Id" />
|
||||||
|
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250"
|
||||||
|
Text="{Binding EndpointClientId}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Margin="0,5">
|
<StackPanel Margin="0,5">
|
||||||
<TextBlock Text="User Agent" />
|
<TextBlock Text="User Agent" />
|
||||||
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250"
|
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250"
|
||||||
|
|
@ -286,6 +310,12 @@
|
||||||
<TextBlock Text="Login" HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="12" />
|
<TextBlock Text="Login" HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="12" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<controls:ProgressRing Width="24" Height="24"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
IsActive="{Binding IsLoggingIn}"
|
||||||
|
IsVisible="{Binding IsLoggingIn}" />
|
||||||
|
|
||||||
<controls:SymbolIcon Symbol="CloudOff"
|
<controls:SymbolIcon Symbol="CloudOff"
|
||||||
IsVisible="{Binding EndpointNotSignedWarning}"
|
IsVisible="{Binding EndpointNotSignedWarning}"
|
||||||
Foreground="OrangeRed"
|
Foreground="OrangeRed"
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ public partial class QueueManager : ObservableObject{
|
||||||
|
|
||||||
public int ActiveDownloads => Volatile.Read(ref activeDownloads);
|
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
|
#endregion
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
|
@ -60,6 +64,10 @@ public partial class QueueManager : ObservableObject{
|
||||||
Interlocked.Increment(ref activeDownloads);
|
Interlocked.Increment(ref activeDownloads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetDownloads(){
|
||||||
|
Interlocked.Exchange(ref activeDownloads, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public void DecrementDownloads(){
|
public void DecrementDownloads(){
|
||||||
while (true){
|
while (true){
|
||||||
int current = Volatile.Read(ref activeDownloads);
|
int current = Volatile.Read(ref activeDownloads);
|
||||||
|
|
@ -69,7 +77,6 @@ public partial class QueueManager : ObservableObject{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){
|
private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){
|
||||||
if (e.Action == NotifyCollectionChangedAction.Remove){
|
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));
|
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);
|
await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
|
||||||
var musicVideo = await CrunchyrollManager.Instance.CrMusic.ParseMusicVideoByIdAsync(epId, "");
|
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);
|
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;
|
musicVideoMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
|
||||||
|
|
||||||
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
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);
|
await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true);
|
||||||
|
|
||||||
var concert = await CrunchyrollManager.Instance.CrMusic.ParseConcertByIdAsync(epId, "");
|
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);
|
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;
|
concertMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
|
||||||
|
|
||||||
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
|
||||||
|
|
@ -438,7 +447,7 @@ public partial class QueueManager : ObservableObject{
|
||||||
crunchyEpMeta.HighlightAllAvailable = true;
|
crunchyEpMeta.HighlightAllAvailable = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Queue.Add(crunchyEpMeta);
|
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));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +173,9 @@ public enum DownloadMediaType{
|
||||||
|
|
||||||
[EnumMember(Value = "Audio")]
|
[EnumMember(Value = "Audio")]
|
||||||
Audio,
|
Audio,
|
||||||
|
|
||||||
|
[EnumMember(Value = "AudioRoleDescription")]
|
||||||
|
AudioRoleDescription,
|
||||||
|
|
||||||
[EnumMember(Value = "Chapters")]
|
[EnumMember(Value = "Chapters")]
|
||||||
Chapters,
|
Chapters,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
|
@ -11,7 +16,7 @@ namespace CRD.Utils.Files;
|
||||||
|
|
||||||
public class CfgManager{
|
public class CfgManager{
|
||||||
private static string workingDirectory = AppContext.BaseDirectory;
|
private static string workingDirectory = AppContext.BaseDirectory;
|
||||||
|
|
||||||
public static readonly string PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json");
|
public static readonly string PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json");
|
||||||
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
|
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
|
||||||
|
|
||||||
|
|
@ -182,30 +187,105 @@ public class CfgManager{
|
||||||
WriteJsonToFileCompressed(PathCrHistory, CrunchyrollManager.Instance.HistoryList);
|
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){
|
public static void WriteJsonToFileCompressed(string pathToFile, object obj, int keepBackups = 5){
|
||||||
try{
|
string? directoryPath = Path.GetDirectoryName(pathToFile);
|
||||||
// Check if the directory exists; if not, create it.
|
if (string.IsNullOrEmpty(directoryPath))
|
||||||
string directoryPath = Path.GetDirectoryName(pathToFile);
|
directoryPath = Environment.CurrentDirectory;
|
||||||
if (!Directory.Exists(directoryPath)){
|
|
||||||
Directory.CreateDirectory(directoryPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (fileLock){
|
Directory.CreateDirectory(directoryPath);
|
||||||
using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write))
|
|
||||||
using (var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal))
|
string key = Path.GetFullPath(pathToFile);
|
||||||
using (var streamWriter = new StreamWriter(gzipStream))
|
object gate = _pathLocks.GetOrAdd(key, _ => new object());
|
||||||
using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){
|
|
||||||
|
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();
|
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){
|
public static void WriteJsonToFile(string pathToFile, object obj){
|
||||||
try{
|
try{
|
||||||
// Check if the directory exists; if not, create it.
|
// 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{
|
try{
|
||||||
using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read)){
|
var fso = new FileStreamOptions{
|
||||||
// Check if the file is compressed
|
Mode = FileMode.Open,
|
||||||
if (IsFileCompressed(fileStream)){
|
Access = FileAccess.Read,
|
||||||
// Reset the stream position to the beginning
|
Share = FileShare.ReadWrite | FileShare.Delete,
|
||||||
fileStream.Position = 0;
|
Options = FileOptions.SequentialScan
|
||||||
using (var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress))
|
};
|
||||||
using (var streamReader = new StreamReader(gzipStream)){
|
|
||||||
return streamReader.ReadToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not compressed, read the file as is
|
using var fs = new FileStream(pathToFile, fso);
|
||||||
fileStream.Position = 0;
|
|
||||||
using (var streamReader = new StreamReader(fileStream)){
|
Span<byte> hdr = stackalloc byte[2];
|
||||||
return streamReader.ReadToEnd();
|
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){
|
} catch (Exception ex){
|
||||||
Console.Error.WriteLine($"An error occurred: {ex.Message}");
|
Console.Error.WriteLine($"Read failed for {pathToFile}: {ex.Message}");
|
||||||
return null;
|
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){
|
public static bool CheckIfFileExists(string filePath){
|
||||||
string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;
|
string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;
|
||||||
|
|
||||||
return Directory.Exists(dirPath) && File.Exists(filePath);
|
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{
|
public static T? ReadJsonFromFile<T>(string pathToFile) where T : class{
|
||||||
try{
|
try{
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,9 @@ public class Helpers{
|
||||||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var property in originalRequest.Properties){
|
foreach (var kvp in originalRequest.Options){
|
||||||
clone.Properties.Add(property);
|
var key = new HttpRequestOptionsKey<object?>(kvp.Key);
|
||||||
|
clone.Options.Set(key, kvp.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
|
|
@ -71,15 +72,31 @@ public class Helpers{
|
||||||
return JsonConvert.DeserializeObject<T>(json);
|
return JsonConvert.DeserializeObject<T>(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int ToKbps(int bps) => (int)Math.Round(bps / 1000.0);
|
||||||
|
public static int SnapToAudioBucket(int kbps){
|
||||||
|
int[] buckets ={ 64, 96, 128, 192,256 };
|
||||||
|
return buckets.OrderBy(b => Math.Abs(b - kbps)).First();
|
||||||
|
}
|
||||||
|
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 time){
|
public static string ConvertTimeFormat(string vttTime){
|
||||||
var timeParts = time.Split(':', '.');
|
if (TimeSpan.TryParseExact(vttTime, @"hh\:mm\:ss\.fff", null, out var ts) ||
|
||||||
int hours = int.Parse(timeParts[0]);
|
TimeSpan.TryParseExact(vttTime, @"mm\:ss\.fff", null, out ts)){
|
||||||
int minutes = int.Parse(timeParts[1]);
|
var totalCentiseconds = (int)Math.Round(ts.TotalMilliseconds / 10.0, MidpointRounding.AwayFromZero);
|
||||||
int seconds = int.Parse(timeParts[2]);
|
var hours = totalCentiseconds / 360000; // 100 cs * 60 * 60
|
||||||
int milliseconds = int.Parse(timeParts[3]);
|
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 $"{hours}:{minutes:D2}:{seconds:D2}.{milliseconds / 10:D2}";
|
return "0:00:00.00";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ConvertVTTStylesToASS(string dialogue){
|
public static string ConvertVTTStylesToASS(string dialogue){
|
||||||
|
|
@ -391,6 +408,7 @@ public class Helpers{
|
||||||
process.StartInfo.RedirectStandardError = true;
|
process.StartInfo.RedirectStandardError = true;
|
||||||
process.StartInfo.UseShellExecute = false;
|
process.StartInfo.UseShellExecute = false;
|
||||||
process.StartInfo.CreateNoWindow = true;
|
process.StartInfo.CreateNoWindow = true;
|
||||||
|
process.EnableRaisingEvents = true;
|
||||||
|
|
||||||
process.OutputDataReceived += (sender, e) => {
|
process.OutputDataReceived += (sender, e) => {
|
||||||
if (!string.IsNullOrEmpty(e.Data)){
|
if (!string.IsNullOrEmpty(e.Data)){
|
||||||
|
|
@ -411,7 +429,28 @@ public class Helpers{
|
||||||
process.BeginOutputReadLine();
|
process.BeginOutputReadLine();
|
||||||
process.BeginErrorReadLine();
|
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;
|
bool isSuccess = process.ExitCode == 0;
|
||||||
|
|
||||||
|
|
@ -423,7 +462,14 @@ public class Helpers{
|
||||||
File.Move(tempOutputFilePath, inputFilePath);
|
File.Move(tempOutputFilePath, inputFilePath);
|
||||||
} else{
|
} else{
|
||||||
// If something went wrong, delete the temporary output file
|
// 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("FFmpeg processing failed.");
|
||||||
Console.Error.WriteLine($"Command: {ffmpegCommand}");
|
Console.Error.WriteLine($"Command: {ffmpegCommand}");
|
||||||
}
|
}
|
||||||
|
|
@ -572,6 +618,12 @@ public class Helpers{
|
||||||
string cNumber = match.Groups[2].Value; // Extract the C number if present
|
string cNumber = match.Groups[2].Value; // Extract the C number if present
|
||||||
string pNumber = match.Groups[3].Value; // Extract the P 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)){
|
if (!string.IsNullOrEmpty(cNumber)){
|
||||||
// Case for C: Return S + . + C
|
// Case for C: Return S + . + C
|
||||||
return $"{sNumber}.{cNumber}";
|
return $"{sNumber}.{cNumber}";
|
||||||
|
|
@ -645,10 +697,10 @@ public class Helpers{
|
||||||
group.Add(descriptionMedia[0]);
|
group.Add(descriptionMedia[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Find and add Cover media to each group
|
//Find and add Cover media to each group
|
||||||
var coverMedia = allMedia.Where(media => media.Type == DownloadMediaType.Cover).ToList();
|
var coverMedia = allMedia.Where(media => media.Type == DownloadMediaType.Cover).ToList();
|
||||||
|
|
||||||
if (coverMedia.Count > 0){
|
if (coverMedia.Count > 0){
|
||||||
foreach (var group in languageGroups.Values){
|
foreach (var group in languageGroups.Values){
|
||||||
group.Add(coverMedia[0]);
|
group.Add(coverMedia[0]);
|
||||||
|
|
@ -860,7 +912,7 @@ public class Helpers{
|
||||||
} else{
|
} else{
|
||||||
throw new PlatformNotSupportedException();
|
throw new PlatformNotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
try{
|
||||||
using (var process = new Process()){
|
using (var process = new Process()){
|
||||||
process.StartInfo.FileName = shutdownCmd;
|
process.StartInfo.FileName = shutdownCmd;
|
||||||
|
|
@ -875,13 +927,13 @@ public class Helpers{
|
||||||
Console.Error.WriteLine($"{e.Data}");
|
Console.Error.WriteLine($"{e.Data}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.OutputDataReceived += (sender, e) => {
|
process.OutputDataReceived += (sender, e) => {
|
||||||
if (!string.IsNullOrEmpty(e.Data)){
|
if (!string.IsNullOrEmpty(e.Data)){
|
||||||
Console.Error.WriteLine(e.Data);
|
Console.Error.WriteLine(e.Data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
process.BeginOutputReadLine();
|
process.BeginOutputReadLine();
|
||||||
|
|
@ -892,11 +944,9 @@ public class Helpers{
|
||||||
if (process.ExitCode != 0){
|
if (process.ExitCode != 0){
|
||||||
Console.Error.WriteLine($"Shutdown failed with exit code {process.ExitCode}");
|
Console.Error.WriteLine($"Shutdown failed with exit code {process.ExitCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (Exception ex){
|
} catch (Exception ex){
|
||||||
Console.Error.WriteLine($"Failed to start shutdown process: {ex.Message}");
|
Console.Error.WriteLine($"Failed to start shutdown process: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -33,9 +33,9 @@ public class HttpClientReq{
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private HttpClient client;
|
private HttpClient client;
|
||||||
|
|
||||||
public HttpClientReq(){
|
public HttpClientReq(){
|
||||||
IWebProxy systemProxy = WebRequest.DefaultWebProxy;
|
IWebProxy systemProxy = WebRequest.DefaultWebProxy;
|
||||||
|
|
||||||
|
|
@ -137,6 +137,8 @@ public class HttpClientReq{
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
CaptureResponseCookies(response, request.RequestUri!, cookieStore);
|
||||||
|
|
||||||
return (IsOk: true, ResponseContent: content, error: "");
|
return (IsOk: true, ResponseContent: content, error: "");
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
|
||||||
|
|
@ -148,6 +150,30 @@ public class HttpClientReq{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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){
|
private void AttachCookies(HttpRequestMessage request, Dictionary<string, CookieCollection>? cookieStore){
|
||||||
if (cookieStore == null){
|
if (cookieStore == null){
|
||||||
return;
|
return;
|
||||||
|
|
@ -177,6 +203,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){
|
public void AddCookie(string domain, Cookie cookie, Dictionary<string, CookieCollection>? cookieStore){
|
||||||
if (cookieStore == null){
|
if (cookieStore == null){
|
||||||
return;
|
return;
|
||||||
|
|
@ -237,7 +276,7 @@ public static class ApiUrls{
|
||||||
public static string Cms => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/cms";
|
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 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://www.crunchyroll.com/playback/v2
|
||||||
//https://cr-play-service.prd.crunchyrollsvc.com/v2
|
//https://cr-play-service.prd.crunchyrollsvc.com/v2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,19 +167,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
|
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();
|
.ToList();
|
||||||
|
|
||||||
foreach (var aud in sortedAudio){
|
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("--audio-tracks 0");
|
||||||
args.Add("--no-video");
|
args.Add("--no-video");
|
||||||
args.Add($"--track-name 0:\"{trackName}\"");
|
args.Add($"--track-name 0:\"{trackName}\"");
|
||||||
args.Add($"--language 0:{aud.Language.Code}");
|
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");
|
args.Add("--default-track 0");
|
||||||
} else{
|
} else{
|
||||||
args.Add("--default-track 0:0");
|
args.Add("--default-track 0:0");
|
||||||
|
|
@ -450,7 +462,7 @@ public class MergerInput{
|
||||||
public LanguageItem Language{ get; set; }
|
public LanguageItem Language{ get; set; }
|
||||||
public int? Duration{ get; set; }
|
public int? Duration{ get; set; }
|
||||||
public int? Delay{ get; set; }
|
public int? Delay{ get; set; }
|
||||||
public bool? IsPrimary{ get; set; }
|
public bool IsAudioRoleDescription{ get; set; }
|
||||||
public int? Bitrate{ get; set; }
|
public int? Bitrate{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ public class VideoItem: VideoPlaylist{
|
||||||
|
|
||||||
public class AudioItem: AudioPlaylist{
|
public class AudioItem: AudioPlaylist{
|
||||||
public string resolutionText{ get; set; }
|
public string resolutionText{ get; set; }
|
||||||
|
public string resolutionTextSnap{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Quality{
|
public class Quality{
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public partial class AnilistSeries : ObservableObject{
|
||||||
public string BannerImage{ get; set; }
|
public string BannerImage{ get; set; }
|
||||||
public bool IsAdult{ get; set; }
|
public bool IsAdult{ get; set; }
|
||||||
public CoverImage CoverImage{ 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<ExternalLink>? ExternalLinks{ get; set; }
|
||||||
public List<Ranking> Rankings{ get; set; }
|
public List<Ranking> Rankings{ get; set; }
|
||||||
public Studios Studios{ get; set; }
|
public Studios Studios{ get; set; }
|
||||||
|
|
@ -53,6 +53,9 @@ public partial class AnilistSeries : ObservableObject{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[ObservableProperty]
|
||||||
|
public bool _fetchedFromCR;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string? CrunchyrollID;
|
public string? CrunchyrollID;
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,13 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
|
||||||
public string? SeasonName{ get; set; }
|
public string? SeasonName{ get; set; }
|
||||||
|
|
||||||
public string? CrSeriesID{ get; set; }
|
public string? CrSeriesID{ get; set; }
|
||||||
|
|
||||||
public bool AnilistEpisode{ get; set; }
|
public bool AnilistEpisode{ get; set; }
|
||||||
|
|
||||||
|
public bool FilteredOut{ get; set; }
|
||||||
|
|
||||||
|
public Locale? AudioLocale{ get; set; }
|
||||||
|
|
||||||
public List<CalendarEpisode> CalendarEpisodes{ get; set; } =[];
|
public List<CalendarEpisode> CalendarEpisodes{ get; set; } =[];
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
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);
|
await QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CalendarEpisodes.Count > 0){
|
if (CalendarEpisodes.Count > 0){
|
||||||
foreach (var calendarEpisode in CalendarEpisodes){
|
foreach (var calendarEpisode in CalendarEpisodes){
|
||||||
calendarEpisode.AddEpisodeToQue();
|
calendarEpisode.AddEpisodeToQue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadImage(int width = 0, int height = 0){
|
public async Task LoadImage(int width = 0, int height = 0){
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("shutdown_when_queue_empty")]
|
[JsonProperty("shutdown_when_queue_empty")]
|
||||||
public bool ShutdownWhenQueueEmpty{ get; set; }
|
public bool ShutdownWhenQueueEmpty{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("auto_download")]
|
[JsonProperty("auto_download")]
|
||||||
public bool AutoDownload{ get; set; }
|
public bool AutoDownload{ get; set; }
|
||||||
|
|
||||||
|
|
@ -22,22 +22,25 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("retry_delay")]
|
[JsonProperty("retry_delay")]
|
||||||
public int RetryDelay{ get; set; }
|
public int RetryDelay{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("retry_attempts")]
|
[JsonProperty("retry_attempts")]
|
||||||
public int RetryAttempts{ get; set; }
|
public int RetryAttempts{ get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string Force{ get; set; } = "";
|
public string Force{ get; set; } = "";
|
||||||
|
|
||||||
[JsonProperty("download_methode_new")]
|
[JsonProperty("download_methode_new")]
|
||||||
public bool DownloadMethodeNew{ get; set; }
|
public bool DownloadMethodeNew{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("download_allow_early_start")]
|
[JsonProperty("download_allow_early_start")]
|
||||||
public bool DownloadAllowEarlyStart{ get; set; }
|
public bool DownloadAllowEarlyStart{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("simultaneous_downloads")]
|
[JsonProperty("simultaneous_downloads")]
|
||||||
public int SimultaneousDownloads{ get; set; }
|
public int SimultaneousDownloads{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("simultaneous_processing_jobs")]
|
||||||
|
public int SimultaneousProcessingJobs{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("theme")]
|
[JsonProperty("theme")]
|
||||||
public string Theme{ get; set; } = "";
|
public string Theme{ get; set; } = "";
|
||||||
|
|
||||||
|
|
@ -49,11 +52,11 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("download_finished_play_sound")]
|
[JsonProperty("download_finished_play_sound")]
|
||||||
public bool DownloadFinishedPlaySound{ get; set; }
|
public bool DownloadFinishedPlaySound{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("download_finished_sound_path")]
|
[JsonProperty("download_finished_sound_path")]
|
||||||
public string? DownloadFinishedSoundPath{ get; set; }
|
public string? DownloadFinishedSoundPath{ get; set; }
|
||||||
|
|
||||||
|
|
||||||
[JsonProperty("background_image_opacity")]
|
[JsonProperty("background_image_opacity")]
|
||||||
public double BackgroundImageOpacity{ get; set; }
|
public double BackgroundImageOpacity{ get; set; }
|
||||||
|
|
||||||
|
|
@ -71,9 +74,9 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("history")]
|
[JsonProperty("history")]
|
||||||
public bool History{ get; set; }
|
public bool History{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("history_count_missing")]
|
[JsonProperty("history_count_missing")]
|
||||||
public bool HistoryCountMissing { get; set; }
|
public bool HistoryCountMissing{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("history_include_cr_artists")]
|
[JsonProperty("history_include_cr_artists")]
|
||||||
public bool HistoryIncludeCrArtists{ get; set; }
|
public bool HistoryIncludeCrArtists{ get; set; }
|
||||||
|
|
@ -136,6 +139,9 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
#region Crunchyroll Settings
|
#region Crunchyroll Settings
|
||||||
|
|
||||||
|
[JsonProperty("cr_download_description_audio")]
|
||||||
|
public bool DownloadDescriptionAudio{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("cr_mark_as_watched")]
|
[JsonProperty("cr_mark_as_watched")]
|
||||||
public bool MarkAsWatched{ get; set; }
|
public bool MarkAsWatched{ get; set; }
|
||||||
|
|
||||||
|
|
@ -165,7 +171,7 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("file_name_whitespace_substitute")]
|
[JsonProperty("file_name_whitespace_substitute")]
|
||||||
public string FileNameWhitespaceSubstitute{ get; set; } = "";
|
public string FileNameWhitespaceSubstitute{ get; set; } = "";
|
||||||
|
|
||||||
[JsonProperty("file_name")]
|
[JsonProperty("file_name")]
|
||||||
public string FileName{ get; set; } = "";
|
public string FileName{ get; set; } = "";
|
||||||
|
|
||||||
|
|
@ -181,6 +187,9 @@ public class CrDownloadOptions{
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool SkipSubs{ get; set; }
|
public bool SkipSubs{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("subs_fix_ccc_subs")]
|
||||||
|
public bool FixCccSubtitles{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("mux_skip_subs")]
|
[JsonProperty("mux_skip_subs")]
|
||||||
public bool SkipSubsMux{ get; set; }
|
public bool SkipSubsMux{ get; set; }
|
||||||
|
|
||||||
|
|
@ -189,7 +198,7 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("subs_download_duplicate")]
|
[JsonProperty("subs_download_duplicate")]
|
||||||
public bool SubsDownloadDuplicate{ get; set; }
|
public bool SubsDownloadDuplicate{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("include_signs_subs")]
|
[JsonProperty("include_signs_subs")]
|
||||||
public bool IncludeSignsSubs{ get; set; }
|
public bool IncludeSignsSubs{ get; set; }
|
||||||
|
|
||||||
|
|
@ -199,6 +208,9 @@ public class CrDownloadOptions{
|
||||||
[JsonProperty("include_cc_subs")]
|
[JsonProperty("include_cc_subs")]
|
||||||
public bool IncludeCcSubs{ get; set; }
|
public bool IncludeCcSubs{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("convert_cc_vtt_subs_to_ass")]
|
||||||
|
public bool ConvertVtt2Ass{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("cc_subs_font")]
|
[JsonProperty("cc_subs_font")]
|
||||||
public string? CcSubsFont{ get; set; }
|
public string? CcSubsFont{ get; set; }
|
||||||
|
|
||||||
|
|
@ -207,13 +219,13 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("mux_mp4")]
|
[JsonProperty("mux_mp4")]
|
||||||
public bool Mp4{ get; set; }
|
public bool Mp4{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("mux_audio_only_to_mp3")]
|
[JsonProperty("mux_audio_only_to_mp3")]
|
||||||
public bool AudioOnlyToMp3 { get; set; }
|
public bool AudioOnlyToMp3{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("mux_fonts")]
|
[JsonProperty("mux_fonts")]
|
||||||
public bool MuxFonts{ get; set; }
|
public bool MuxFonts{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("mux_cover")]
|
[JsonProperty("mux_cover")]
|
||||||
public bool MuxCover{ get; set; }
|
public bool MuxCover{ get; set; }
|
||||||
|
|
||||||
|
|
@ -258,7 +270,7 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("mux_sync_dubs")]
|
[JsonProperty("mux_sync_dubs")]
|
||||||
public bool SyncTiming{ get; set; }
|
public bool SyncTiming{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("mux_sync_hwaccel")]
|
[JsonProperty("mux_sync_hwaccel")]
|
||||||
public string? FfmpegHwAccelFlag{ get; set; }
|
public string? FfmpegHwAccelFlag{ get; set; }
|
||||||
|
|
||||||
|
|
@ -294,9 +306,9 @@ public class CrDownloadOptions{
|
||||||
|
|
||||||
[JsonProperty("stream_endpoint")]
|
[JsonProperty("stream_endpoint")]
|
||||||
public string? StreamEndpoint{ get; set; }
|
public string? StreamEndpoint{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("stream_endpoint_secondary_settings")]
|
[JsonProperty("stream_endpoint_secondary_settings")]
|
||||||
public CrAuthSettings? StreamEndpointSecondSettings { get; set; }
|
public CrAuthSettings? StreamEndpointSecondSettings{ get; set; }
|
||||||
|
|
||||||
[JsonProperty("search_fetch_featured_music")]
|
[JsonProperty("search_fetch_featured_music")]
|
||||||
public bool SearchFetchFeaturedMusic{ get; set; }
|
public bool SearchFetchFeaturedMusic{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace CRD.Utils.Structs.Crunchyroll;
|
||||||
public class CrunchyStreamData{
|
public class CrunchyStreamData{
|
||||||
public string? AssetId{ get; set; }
|
public string? AssetId{ get; set; }
|
||||||
public Locale? AudioLocale{ get; set; }
|
public Locale? AudioLocale{ get; set; }
|
||||||
|
public string? AudioRole{ get; set; }
|
||||||
public string? Bifs{ get; set; }
|
public string? Bifs{ get; set; }
|
||||||
public string? BurnedInLocale{ get; set; }
|
public string? BurnedInLocale{ get; set; }
|
||||||
public Dictionary<string, Caption>? Captions{ get; set; }
|
public Dictionary<string, Caption>? Captions{ get; set; }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using CRD.Utils.Structs.Crunchyroll;
|
using CRD.Utils.Structs.Crunchyroll;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
|
|
@ -338,6 +339,8 @@ public class EpisodeVersion{
|
||||||
[JsonProperty("season_guid")]
|
[JsonProperty("season_guid")]
|
||||||
public string SeasonGuid{ get; set; }
|
public string SeasonGuid{ get; set; }
|
||||||
|
|
||||||
|
public string[] roles{ get; set; } =[];
|
||||||
|
|
||||||
public string Variant{ get; set; }
|
public string Variant{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -394,6 +397,8 @@ public class CrunchyEpMeta{
|
||||||
public CrDownloadOptions? DownloadSettings;
|
public CrDownloadOptions? DownloadSettings;
|
||||||
|
|
||||||
public bool HighlightAllAvailable{ get; set; }
|
public bool HighlightAllAvailable{ get; set; }
|
||||||
|
|
||||||
|
public CancellationTokenSource Cts { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DownloadProgress{
|
public class DownloadProgress{
|
||||||
|
|
@ -415,6 +420,8 @@ public class CrunchyEpMetaData{
|
||||||
public bool IsSubbed{ get; set; }
|
public bool IsSubbed{ get; set; }
|
||||||
public bool IsDubbed{ get; set; }
|
public bool IsDubbed{ get; set; }
|
||||||
|
|
||||||
|
public bool IsAudioRoleDescription{ get; set; }
|
||||||
|
|
||||||
public (string? seasonID, string? guid) GetOriginalIds(){
|
public (string? seasonID, string? guid) GetOriginalIds(){
|
||||||
var version = Versions?.FirstOrDefault(a => a.Original);
|
var version = Versions?.FirstOrDefault(a => a.Original);
|
||||||
if (version != null && !string.IsNullOrEmpty(version.Guid) && !string.IsNullOrEmpty(version.SeasonGuid)){
|
if (version != null && !string.IsNullOrEmpty(version.Guid) && !string.IsNullOrEmpty(version.SeasonGuid)){
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class StreamError{
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsTooManyActiveStreamsError(){
|
public bool IsTooManyActiveStreamsError(){
|
||||||
return Error == "TOO_MANY_ACTIVE_STREAMS";
|
return Error is "TOO_MANY_ACTIVE_STREAMS" or "TOO_MANY_CONCURRENT_STREAMS";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@ public class AuthData{
|
||||||
|
|
||||||
public class CrAuthSettings{
|
public class CrAuthSettings{
|
||||||
public string Endpoint{ get; set; }
|
public string Endpoint{ get; set; }
|
||||||
|
public string Client_ID{ get; set; }
|
||||||
public string Authorization{ get; set; }
|
public string Authorization{ get; set; }
|
||||||
public string UserAgent{ get; set; }
|
public string UserAgent{ get; set; }
|
||||||
public string Device_type{ get; set; }
|
public string Device_type{ get; set; }
|
||||||
public string Device_name{ get; set; }
|
public string Device_name{ get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DrmAuthData{
|
public class DrmAuthData{
|
||||||
|
|
|
||||||
|
|
@ -143,13 +143,13 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
||||||
await DownloadEpisode();
|
await DownloadEpisode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){
|
public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default, string overrideDownloadPath = ""){
|
||||||
switch (EpisodeType){
|
switch (EpisodeType){
|
||||||
case EpisodeType.MusicVideo:
|
case EpisodeType.MusicVideo:
|
||||||
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty);
|
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty, overrideDownloadPath);
|
||||||
break;
|
break;
|
||||||
case EpisodeType.Concert:
|
case EpisodeType.Concert:
|
||||||
await QueueManager.Instance.CrAddConcertToQueue(EpisodeId ?? string.Empty);
|
await QueueManager.Instance.CrAddConcertToQueue(EpisodeId ?? string.Empty, overrideDownloadPath);
|
||||||
break;
|
break;
|
||||||
case EpisodeType.Episode:
|
case EpisodeType.Episode:
|
||||||
case EpisodeType.Unknown:
|
case EpisodeType.Unknown:
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
|
||||||
CrunchyEpMeta? downloadItem = QueueManager.Instance.Queue.FirstOrDefault(e => e.Equals(epMeta)) ?? null;
|
CrunchyEpMeta? downloadItem = QueueManager.Instance.Queue.FirstOrDefault(e => e.Equals(epMeta)) ?? null;
|
||||||
if (downloadItem != null){
|
if (downloadItem != null){
|
||||||
QueueManager.Instance.Queue.Remove(downloadItem);
|
QueueManager.Instance.Queue.Remove(downloadItem);
|
||||||
|
epMeta.Cts.Cancel();
|
||||||
if (!Done){
|
if (!Done){
|
||||||
foreach (var downloadItemDownloadedFile in downloadItem.downloadedFiles){
|
foreach (var downloadItemDownloadedFile in downloadItem.downloadedFiles){
|
||||||
try{
|
try{
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
FullSizeDesired = true
|
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(){
|
dialog.Content = new ContentDialogFeaturedMusicView(){
|
||||||
DataContext = viewModel
|
DataContext = viewModel
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Collections;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
@ -17,6 +18,7 @@ using CRD.Utils.Files;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
using CRD.Views;
|
using CRD.Views;
|
||||||
|
using FluentAvalonia.UI.Data;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
|
@ -149,6 +151,9 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _quickAddMode;
|
private bool _quickAddMode;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private static bool _showCrFetches;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
|
|
||||||
|
|
@ -168,7 +173,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
public ObservableCollection<SeasonViewModel> Seasons{ get; set; } =[];
|
public ObservableCollection<SeasonViewModel> Seasons{ get; set; } =[];
|
||||||
|
|
||||||
public ObservableCollection<AnilistSeries> SelectedSeason{ get; set; } =[];
|
public ObservableCollection<AnilistSeries> SelectedSeason{ get; set; } =[];
|
||||||
|
|
||||||
private SeasonViewModel currentSelection;
|
private SeasonViewModel currentSelection;
|
||||||
|
|
||||||
public UpcomingPageViewModel(){
|
public UpcomingPageViewModel(){
|
||||||
|
|
@ -198,11 +203,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
currentSelection = Seasons.Last();
|
currentSelection = Seasons.Last();
|
||||||
currentSelection.IsSelected = true;
|
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();
|
SelectedSeason.Clear();
|
||||||
|
|
||||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
|
||||||
|
|
||||||
foreach (var anilistSeries in list){
|
foreach (var anilistSeries in list){
|
||||||
SelectedSeason.Add(anilistSeries);
|
SelectedSeason.Add(anilistSeries);
|
||||||
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
||||||
|
|
@ -214,6 +219,8 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilterItems();
|
||||||
|
|
||||||
SortItems();
|
SortItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,11 +230,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
currentSelection = selectedSeason;
|
currentSelection = selectedSeason;
|
||||||
currentSelection.IsSelected = true;
|
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();
|
SelectedSeason.Clear();
|
||||||
|
|
||||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
|
||||||
|
|
||||||
foreach (var anilistSeries in list){
|
foreach (var anilistSeries in list){
|
||||||
SelectedSeason.Add(anilistSeries);
|
SelectedSeason.Add(anilistSeries);
|
||||||
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
||||||
|
|
@ -238,17 +245,24 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilterItems();
|
||||||
|
|
||||||
SortItems();
|
SortItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public void OpenTrailer(AnilistSeries series){
|
public void OpenTrailer(AnilistSeries series){
|
||||||
if (series.Trailer.Site.Equals("youtube")){
|
if (series.Trailer != null){
|
||||||
var url = "https://www.youtube.com/watch?v=" + series.Trailer.Id; // Replace with your video URL
|
if (series.Trailer.Site.Equals("youtube")){
|
||||||
Process.Start(new ProcessStartInfo{
|
var url = "https://www.youtube.com/watch?v=" + series.Trailer.Id;
|
||||||
FileName = url,
|
Process.Start(new ProcessStartInfo{
|
||||||
UseShellExecute = true
|
FileName = url,
|
||||||
});
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,7 +272,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
MessageBus.Current.SendMessage(new ToastMessage($"History still loading", ToastType.Warning, 3));
|
MessageBus.Current.SendMessage(new ToastMessage($"History still loading", ToastType.Warning, 3));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(series.CrunchyrollID)){
|
if (!string.IsNullOrEmpty(series.CrunchyrollID)){
|
||||||
if (CrunchyrollManager.Instance.CrunOptions.History){
|
if (CrunchyrollManager.Instance.CrunOptions.History){
|
||||||
series.IsInHistory = true;
|
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){
|
if (ProgramManager.Instance.AnilistSeasons.ContainsKey(season + year) && !forceRefresh){
|
||||||
return ProgramManager.Instance.AnilistSeasons[season + year];
|
return ProgramManager.Instance.AnilistSeasons[season + year];
|
||||||
}
|
}
|
||||||
|
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
|
|
||||||
var variables = new{
|
var allMedia = new List<AnilistSeries>();
|
||||||
season,
|
var page = 1;
|
||||||
year,
|
var maxPage = 10;
|
||||||
format = "TV",
|
bool hasNext;
|
||||||
page = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
var payload = new{
|
do{
|
||||||
query,
|
var payload = new{
|
||||||
variables
|
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);
|
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||||
request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
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){
|
var pageNode = ani.Data?.Page;
|
||||||
Console.Error.WriteLine($"Anilist Request Failed for {season} {year}");
|
var media = pageNode?.Media ?? new List<AnilistSeries>();
|
||||||
return[];
|
allMedia.AddRange(media);
|
||||||
}
|
|
||||||
|
|
||||||
AniListResponse aniListResponse = Helpers.Deserialize<AniListResponse>(response.ResponseContent, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ?? new AniListResponse();
|
hasNext = pageNode?.PageInfo?.HasNextPage ?? false;
|
||||||
|
page++;
|
||||||
var list = aniListResponse.Data?.Page?.Media ??[];
|
} while (hasNext || page <= maxPage);
|
||||||
|
|
||||||
list = list.Where(ele => ele.ExternalLinks != null && ele.ExternalLinks.Any(external =>
|
var list = allMedia.Where(ele => ele.ExternalLinks != null && ele.ExternalLinks.Any(external =>
|
||||||
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
|
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -324,6 +344,8 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
anilistEle.Description = anilistEle.Description
|
anilistEle.Description = anilistEle.Description
|
||||||
.Replace("<i>", "")
|
.Replace("<i>", "")
|
||||||
.Replace("</i>", "")
|
.Replace("</i>", "")
|
||||||
|
.Replace("<b>", "")
|
||||||
|
.Replace("</b>", "")
|
||||||
.Replace("<BR>", "")
|
.Replace("<BR>", "")
|
||||||
.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;
|
ProgramManager.Instance.AnilistSeasons[season + year] = list;
|
||||||
|
|
||||||
|
|
@ -447,6 +512,11 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
partial void OnSelectedSeriesChanged(AnilistSeries? value){
|
partial void OnSelectedSeriesChanged(AnilistSeries? value){
|
||||||
SelectionChangedOfSeries(value);
|
SelectionChangedOfSeries(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnShowCrFetchesChanged(bool value){
|
||||||
|
FilterItems();
|
||||||
|
SortItems();
|
||||||
|
}
|
||||||
|
|
||||||
#region Sorting
|
#region Sorting
|
||||||
|
|
||||||
|
|
@ -512,6 +582,24 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
||||||
SelectedSeason.Add(item);
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
|
using CRD.Utils;
|
||||||
using CRD.Utils.Structs.Crunchyroll.Music;
|
using CRD.Utils.Structs.Crunchyroll.Music;
|
||||||
using CRD.Utils.Structs.History;
|
using CRD.Utils.Structs.History;
|
||||||
using CRD.Utils.UI;
|
using CRD.Utils.UI;
|
||||||
|
|
@ -23,11 +24,13 @@ public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{
|
||||||
private bool _musicInHistory;
|
private bool _musicInHistory;
|
||||||
|
|
||||||
private CrunchyMusicVideoList featuredMusic;
|
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);
|
ArgumentNullException.ThrowIfNull(contentDialog);
|
||||||
|
|
||||||
this.featuredMusic = featuredMusic;
|
this.featuredMusic = featuredMusic;
|
||||||
|
this.FolderPath = overrideDownloadPath + "/OST";
|
||||||
|
|
||||||
dialog = contentDialog;
|
dialog = contentDialog;
|
||||||
dialog.Closed += DialogOnClosed;
|
dialog.Closed += DialogOnClosed;
|
||||||
|
|
@ -78,7 +81,7 @@ public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public void DownloadEpisode(HistoryEpisode episode){
|
public void DownloadEpisode(HistoryEpisode episode){
|
||||||
episode.DownloadEpisode();
|
episode.DownloadEpisode(EpisodeDownloadMode.Default, FolderPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CRD.Downloader.Crunchyroll;
|
using CRD.Downloader.Crunchyroll;
|
||||||
using CRD.Utils.Structs;
|
using CRD.Utils.Structs;
|
||||||
|
|
@ -9,15 +10,19 @@ namespace CRD.ViewModels.Utils;
|
||||||
public partial class ContentDialogInputLoginViewModel : ViewModelBase{
|
public partial class ContentDialogInputLoginViewModel : ViewModelBase{
|
||||||
private readonly ContentDialog dialog;
|
private readonly ContentDialog dialog;
|
||||||
|
|
||||||
|
private readonly TaskCompletionSource<bool> _loginTcs = new();
|
||||||
|
|
||||||
|
public Task LoginCompleted => _loginTcs.Task;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _email;
|
private string _email;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _password;
|
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){
|
if (dialog is null){
|
||||||
throw new ArgumentNullException(nameof(dialog));
|
throw new ArgumentNullException(nameof(dialog));
|
||||||
}
|
}
|
||||||
|
|
@ -30,15 +35,19 @@ public partial class ContentDialogInputLoginViewModel : ViewModelBase{
|
||||||
|
|
||||||
private async void LoginButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
private async void LoginButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
|
||||||
dialog.PrimaryButtonClick -= LoginButton;
|
dialog.PrimaryButtonClick -= LoginButton;
|
||||||
await CrunchyrollManager.Instance.CrAuthEndpoint1.Auth(new AuthData{Password = Password,Username = Email});
|
try{
|
||||||
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrAuthEndpoint2.AuthSettings.Endpoint)){
|
await CrunchyrollManager.Instance.CrAuthEndpoint1.Auth(new AuthData{ Password = Password, Username = Email });
|
||||||
await CrunchyrollManager.Instance.CrAuthEndpoint2.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){
|
_loginTcs.TrySetResult(true);
|
||||||
accountPageViewModel.UpdatetProfile();
|
} catch (Exception ex){
|
||||||
|
_loginTcs.TrySetException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
|
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double? _simultaneousDownloads;
|
private double? _simultaneousDownloads;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private double? _simultaneousProcessingJobs;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _downloadMethodeNew;
|
private bool _downloadMethodeNew;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _downloadAllowEarlyStart;
|
private bool _downloadAllowEarlyStart;
|
||||||
|
|
||||||
|
|
@ -280,6 +283,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
RetryDelay = Math.Clamp((options.RetryDelay), 1, 30);
|
RetryDelay = Math.Clamp((options.RetryDelay), 1, 30);
|
||||||
DownloadToTempFolder = options.DownloadToTempFolder;
|
DownloadToTempFolder = options.DownloadToTempFolder;
|
||||||
SimultaneousDownloads = options.SimultaneousDownloads;
|
SimultaneousDownloads = options.SimultaneousDownloads;
|
||||||
|
SimultaneousProcessingJobs = options.SimultaneousProcessingJobs;
|
||||||
LogMode = options.LogMode;
|
LogMode = options.LogMode;
|
||||||
|
|
||||||
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
|
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
|
||||||
|
|
@ -320,6 +324,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
||||||
settings.HistoryCountSonarr = HistoryCountSonarr;
|
settings.HistoryCountSonarr = HistoryCountSonarr;
|
||||||
settings.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
|
settings.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
|
||||||
settings.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
|
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.ProxyEnabled = ProxyEnabled;
|
||||||
settings.ProxySocks = ProxySocks;
|
settings.ProxySocks = ProxySocks;
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,19 @@
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
|
<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"
|
<ToggleButton Width="50" Height="50" BorderThickness="0" Margin="5 0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsChecked="{Binding QuickAddMode}"
|
IsChecked="{Binding QuickAddMode}"
|
||||||
|
|
@ -149,7 +162,7 @@
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
|
|
||||||
<Grid>
|
<Grid >
|
||||||
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="185" />
|
<ColumnDefinition Width="185" />
|
||||||
|
|
@ -268,7 +281,7 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom">
|
<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}"
|
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).OpenTrailer}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
<controls:SettingsExpanderItem Content="Simultaneous Downloads">
|
<controls:SettingsExpanderItem Content="Parallel Downloads">
|
||||||
<controls:SettingsExpanderItem.Footer>
|
<controls:SettingsExpanderItem.Footer>
|
||||||
<controls:NumberBox Minimum="0" Maximum="10"
|
<controls:NumberBox Minimum="0" Maximum="10"
|
||||||
Value="{Binding SimultaneousDownloads}"
|
Value="{Binding SimultaneousDownloads}"
|
||||||
|
|
@ -186,6 +186,15 @@
|
||||||
HorizontalAlignment="Stretch" />
|
HorizontalAlignment="Stretch" />
|
||||||
</controls:SettingsExpanderItem.Footer>
|
</controls:SettingsExpanderItem.Footer>
|
||||||
</controls:SettingsExpanderItem>
|
</controls:SettingsExpanderItem>
|
||||||
|
|
||||||
|
<controls:SettingsExpanderItem IsVisible="{Binding DownloadAllowEarlyStart}" Content="Parallel Processing Jobs" Description="The maximum number of completed downloads that can be processed simultaneously (encoding, muxing, moving)">
|
||||||
|
<controls:SettingsExpanderItem.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 Content="Play completion sound" Description="Enables a notification sound to be played when all downloads have finished">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue