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