From 272d59a03b0aa5d1d278e23dc0acb19dc3db4292 Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Wed, 19 Jun 2024 02:16:02 +0200 Subject: [PATCH] Add - Added fields to add mkvmerge & ffmpeg parameters to settings Add - Added a new way to check the premium status of an account Add - Added default audio and sub to muxing settings Add - Added option to keep subtitles as files and not merge them Add - Added option to download videos for all dubs selected Chg - FFmpeg now also adds chapters to the files Chg - Shows error if decryption files are missing --- CRD/Downloader/CRAuth.cs | 30 ++- CRD/Downloader/CrEpisode.cs | 6 +- CRD/Downloader/CrSeries.cs | 29 +-- CRD/Downloader/Crunchyroll.cs | 215 +++++++++++++-------- CRD/Downloader/History.cs | 6 +- CRD/Utils/DRM/Widevine.cs | 8 +- CRD/Utils/Files/CfgManager.cs | 81 +++++--- CRD/Utils/Files/FileNameManager.cs | 8 +- CRD/Utils/HLS/HLSDownloader.cs | 6 +- CRD/Utils/Helpers.cs | 38 +++- CRD/Utils/Http/HttpClientReq.cs | 11 +- CRD/Utils/Muxing/Merger.cs | 147 +++----------- CRD/Utils/Sonarr/SonarrClient.cs | 6 +- CRD/Utils/Structs/CalendarStructs.cs | 2 +- CRD/Utils/Structs/CrDownloadOptions.cs | 16 +- CRD/Utils/Structs/CrProfile.cs | 44 ++++- CRD/Utils/Structs/StreamLimits.cs | 95 +++++++++ CRD/Utils/Updater/Updater.cs | 6 +- CRD/ViewModels/AccountPageViewModel.cs | 50 ++++- CRD/ViewModels/AddDownloadPageViewModel.cs | 6 +- CRD/ViewModels/DownloadsPageViewModel.cs | 4 +- CRD/ViewModels/MainWindowViewModel.cs | 2 +- CRD/ViewModels/SeriesPageViewModel.cs | 2 +- CRD/ViewModels/SettingsPageViewModel.cs | 126 +++++++++++- CRD/Views/AccountPageView.axaml | 7 +- CRD/Views/SettingsPageView.axaml | 100 +++++++++- CRD/Views/SettingsPageView.axaml.cs | 2 + 27 files changed, 735 insertions(+), 318 deletions(-) create mode 100644 CRD/Utils/Structs/StreamLimits.cs diff --git a/CRD/Downloader/CRAuth.cs b/CRD/Downloader/CRAuth.cs index b9aedf6..97888e5 100644 --- a/CRD/Downloader/CRAuth.cs +++ b/CRD/Downloader/CRAuth.cs @@ -35,7 +35,7 @@ public class CrAuth{ if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent); } else{ - Console.WriteLine("Anonymous login failed"); + Console.Error.WriteLine("Anonymous login failed"); } crunInstance.Profile = new CrProfile{ @@ -104,13 +104,27 @@ public class CrAuth{ if (profileTemp != null){ crunInstance.Profile = profileTemp; + + var requestSubs = HttpClientReq.CreateRequestMessage(Api.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null); + + var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs); + + if (responseSubs.IsOk){ + var subsc = Helpers.Deserialize(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + crunInstance.Profile.Subscription = subsc; + crunInstance.Profile.HasPremium = subsc.IsActive; + } else{ + crunInstance.Profile.HasPremium = false; + Console.Error.WriteLine("Failed to check premium subscription status"); + } + } } } public async void LoginWithToken(){ if (crunInstance.Token?.refresh_token == null){ - Console.WriteLine("Missing Refresh Token"); + Console.Error.WriteLine("Missing Refresh Token"); return; } @@ -133,7 +147,7 @@ public class CrAuth{ if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent); } else{ - Console.WriteLine("Token Auth Failed"); + Console.Error.WriteLine("Token Auth Failed"); } if (crunInstance.Token?.refresh_token != null){ @@ -176,7 +190,7 @@ public class CrAuth{ if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent); } else{ - Console.WriteLine("Refresh Token Auth Failed"); + Console.Error.WriteLine("Refresh Token Auth Failed"); } await GetCmsToken(); @@ -185,7 +199,7 @@ public class CrAuth{ public async Task GetCmsToken(){ if (crunInstance.Token?.access_token == null){ - Console.WriteLine($"Missing Access Token: {crunInstance.Token?.access_token}"); + Console.Error.WriteLine($"Missing Access Token: {crunInstance.Token?.access_token}"); return; } @@ -196,13 +210,13 @@ public class CrAuth{ if (response.IsOk){ crunInstance.CmsToken = JsonConvert.DeserializeObject(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); } else{ - Console.WriteLine("CMS Token Auth Failed"); + Console.Error.WriteLine("CMS Token Auth Failed"); } } public async Task GetCmsData(){ if (crunInstance.CmsToken?.Cms == null){ - Console.WriteLine("Missing CMS Token"); + Console.Error.WriteLine("Missing CMS Token"); return; } @@ -224,7 +238,7 @@ public class CrAuth{ if (response.IsOk){ Console.WriteLine(response.ResponseContent); } else{ - Console.WriteLine("Failed to Get CMS Index"); + Console.Error.WriteLine("Failed to Get CMS Index"); } } } \ No newline at end of file diff --git a/CRD/Downloader/CrEpisode.cs b/CRD/Downloader/CrEpisode.cs index 9306e0b..8b0c94b 100644 --- a/CRD/Downloader/CrEpisode.cs +++ b/CRD/Downloader/CrEpisode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; @@ -19,7 +19,7 @@ public class CrEpisode(){ public async Task ParseEpisodeById(string id,string locale){ if (crunInstance.CmsToken?.Cms == null){ - Console.WriteLine("Missing CMS Access Token"); + Console.Error.WriteLine("Missing CMS Access Token"); return null; } @@ -33,7 +33,7 @@ public class CrEpisode(){ var response = await HttpClientReq.Instance.SendHttpRequest(request); if (!response.IsOk){ - Console.WriteLine("Series Request Failed"); + Console.Error.WriteLine("Series Request Failed"); return null; } diff --git a/CRD/Downloader/CrSeries.cs b/CRD/Downloader/CrSeries.cs index b1a4e64..255e3ef 100644 --- a/CRD/Downloader/CrSeries.cs +++ b/CRD/Downloader/CrSeries.cs @@ -10,7 +10,9 @@ using System.Threading.Tasks; using System.Web; using CRD.Utils; using CRD.Utils.Structs; +using CRD.Views; using Newtonsoft.Json; +using ReactiveUI; namespace CRD.Downloader; @@ -45,10 +47,15 @@ public class CrSeries(){ foreach (var kvp in eps){ var key = kvp.Key; var episode = kvp.Value; - + for (int index = 0; index < episode.Items.Count; index++){ var item = episode.Items[index]; + if (item.IsPremiumOnly && !crunInstance.Profile.HasPremium){ + MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode – make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3)); + continue; + } + if (!dubLang.Contains(episode.Langs[index].CrLocale)) continue; @@ -133,7 +140,7 @@ public class CrSeries(){ CrSeriesSearch? parsedSeries = await ParseSeriesById(id,Locale); // one piece - GRMG8ZQZR if (parsedSeries == null){ - Console.WriteLine("Parse Data Invalid"); + Console.Error.WriteLine("Parse Data Invalid"); return null; } @@ -281,7 +288,7 @@ public class CrSeries(){ CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List(), Total = 0, Meta = new Meta() }; if (crunInstance.CmsToken?.Cms == null){ - Console.WriteLine("Missing CMS Token"); + Console.Error.WriteLine("Missing CMS Token"); return episodeList; } @@ -291,14 +298,12 @@ public class CrSeries(){ var response = await HttpClientReq.Instance.SendHttpRequest(showRequest); if (!response.IsOk){ - Console.WriteLine("Show Request FAILED!"); + Console.Error.WriteLine("Show Request FAILED!"); } else{ Console.WriteLine(response.ResponseContent); } } - //TODO - var episodeRequest = new HttpRequestMessage(HttpMethod.Get, $"{Api.Cms}/seasons/{seasonID}/episodes?preferred_audio_language=ja-JP"); episodeRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", crunInstance.Token?.access_token); @@ -306,13 +311,13 @@ public class CrSeries(){ var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest); if (!episodeRequestResponse.IsOk){ - Console.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}"); + Console.Error.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}"); } else{ episodeList = Helpers.Deserialize(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings); } if (episodeList.Total < 1){ - Console.WriteLine("Season is empty!"); + Console.Error.WriteLine("Season is empty!"); } return episodeList; @@ -352,7 +357,7 @@ public class CrSeries(){ public async Task ParseSeriesById(string id,string? locale){ if (crunInstance.CmsToken?.Cms == null){ - Console.WriteLine("Missing CMS Access Token"); + Console.Error.WriteLine("Missing CMS Access Token"); return null; } @@ -369,7 +374,7 @@ public class CrSeries(){ var response = await HttpClientReq.Instance.SendHttpRequest(request); if (!response.IsOk){ - Console.WriteLine("Series Request Failed"); + Console.Error.WriteLine("Series Request Failed"); return null; } @@ -385,7 +390,7 @@ public class CrSeries(){ public async Task SeriesById(string id){ if (crunInstance.CmsToken?.Cms == null){ - Console.WriteLine("Missing CMS Access Token"); + Console.Error.WriteLine("Missing CMS Access Token"); return null; } @@ -398,7 +403,7 @@ public class CrSeries(){ var response = await HttpClientReq.Instance.SendHttpRequest(request); if (!response.IsOk){ - Console.WriteLine("Series Request Failed"); + Console.Error.WriteLine("Series Request Failed"); return null; } diff --git a/CRD/Downloader/Crunchyroll.cs b/CRD/Downloader/Crunchyroll.cs index 5b18c06..ba27576 100644 --- a/CRD/Downloader/Crunchyroll.cs +++ b/CRD/Downloader/Crunchyroll.cs @@ -114,7 +114,8 @@ public class Crunchyroll{ Username = "???", Avatar = "003-cr-hime-excited.png", PreferredContentAudioLanguage = "ja-JP", - PreferredContentSubtitleLanguage = "de-DE" + PreferredContentSubtitleLanguage = "de-DE", + HasPremium = false, }; @@ -132,14 +133,14 @@ public class Crunchyroll{ CrunOptions.Chapters = true; CrunOptions.Hslang = "none"; CrunOptions.Force = "Y"; - CrunOptions.FileName = "${showTitle} - S${season}E${episode} [${height}p]"; + CrunOptions.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]"; CrunOptions.Partsize = 10; - CrunOptions.NoSubs = false; CrunOptions.DlSubs = new List{ "de-DE" }; CrunOptions.Skipmux = false; CrunOptions.MkvmergeOptions = new List{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" }; - CrunOptions.DefaultAudio = Languages.FindLang("ja-JP"); - CrunOptions.DefaultSub = Languages.FindLang("de-DE"); + CrunOptions.FfmpegOptions = new(); + CrunOptions.DefaultAudio = "ja-JP"; + CrunOptions.DefaultSub = "de-DE"; CrunOptions.CcTag = "cc"; CrunOptions.FsRetryTime = 5; CrunOptions.Numbers = 2; @@ -163,7 +164,7 @@ public class Crunchyroll{ RefreshSonarr(); } - + if (CrunOptions.LogMode){ CfgManager.EnableLogMode(); } else{ @@ -269,7 +270,7 @@ public class Crunchyroll{ week.CalendarDays.Add(calDay); } } else{ - Console.WriteLine("No days found in the HTML document."); + Console.Error.WriteLine("No days found in the HTML document."); } calendar[weeksMondayDate] = week; @@ -285,7 +286,7 @@ public class Crunchyroll{ if (episodeL != null){ - if (episodeL.Value.Data != null && episodeL.Value.Data.First().IsPremiumOnly && (episodeL.Value.Data.First().StreamsLink == null || Profile.Username == "???")){ + if (episodeL.Value.Data != null && episodeL.Value.Data.First().IsPremiumOnly && !Profile.HasPremium){ MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode – make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3)); return; } @@ -298,8 +299,13 @@ public class Crunchyroll{ if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId); if (historyEpisode != null){ - crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber; - crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber; + if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){ + crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber; + } + + if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){ + crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber; + } } } @@ -321,8 +327,13 @@ public class Crunchyroll{ if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId); if (historyEpisode != null){ - crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber; - crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber; + if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){ + crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber; + } + + if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){ + crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber; + } } } @@ -382,19 +393,19 @@ public class Crunchyroll{ await MuxStreams(res.Data, new CrunchyMuxOptions{ FfmpegOptions = options.FfmpegOptions, - SkipSubMux = options.Skipmux, + SkipSubMux = options.SkipSubsMux, Output = res.FileName, Mp4 = options.Mp4, VideoTitle = options.VideoTitle, Novids = options.Novids, NoCleanup = options.Nocleanup, - DefaultAudio = options.DefaultAudio, - DefaultSub = options.DefaultSub, + DefaultAudio = Languages.FindLang(options.DefaultAudio), + DefaultSub = Languages.FindLang(options.DefaultSub), MkvmergeOptions = options.MkvmergeOptions, ForceMuxer = options.Force, SyncTiming = options.SyncTiming, CcTag = options.CcTag, - KeepAllVideos = false + KeepAllVideos = true }, res.FileName); @@ -423,8 +434,6 @@ public class Crunchyroll{ } private async Task MuxStreams(List data, CrunchyMuxOptions options, string filename){ - var hasAudioStreams = false; - var muxToMp3 = false; if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ @@ -437,10 +446,6 @@ public class Crunchyroll{ } } - if (data.Any(a => a.Type == DownloadMediaType.Audio)){ - hasAudioStreams = true; - } - var subs = data.Where(a => a.Type == DownloadMediaType.Subtitle).ToList(); var subsList = new List(); @@ -465,15 +470,13 @@ public class Crunchyroll{ var merger = new Merger(new MergerOptions{ - OnlyVid = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List(), + OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), SkipSubMux = options.SkipSubMux, - OnlyAudio = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List(), + OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).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, Signs = a.Signs }).ToList(), - Simul = false, KeepAllVideos = options.KeepAllVideos, Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined - VideoAndAudio = hasAudioStreams ? new List() : data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), VideoTitle = options.VideoTitle, Options = new MuxOptions(){ @@ -489,11 +492,11 @@ public class Crunchyroll{ }); if (!File.Exists(CfgManager.PathFFMPEG)){ - Console.WriteLine("FFmpeg not found"); + Console.Error.WriteLine("FFmpeg not found"); } if (!File.Exists(CfgManager.PathMKVMERGE)){ - Console.WriteLine("MKVmerge not found"); + Console.Error.WriteLine("MKVmerge not found"); } bool isMuxed; @@ -645,6 +648,20 @@ public class Crunchyroll{ var fetchPlaybackData = await FetchPlaybackData(mediaId, mediaGuid, epMeta); if (!fetchPlaybackData.IsOk){ + if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){ + var s = fetchPlaybackData.error; + var error = StreamError.FromJson(s); + if (error != null && error.IsTooManyActiveStreamsError()){ + MainWindow.Instance.ShowError("Too many active streams that couldn't be stopped"); + return new DownloadResponse{ + Data = new List(), + Error = true, + FileName = "./unknown", + ErrorText = "Too many active streams that couldn't be stopped" + }; + } + } + MainWindow.Instance.ShowError("Couldn't get Playback Data"); return new DownloadResponse{ Data = new List(), @@ -1001,6 +1018,17 @@ public class Crunchyroll{ Console.WriteLine("Decryption Needed, attempting to decrypt"); + if (!_widevine.canDecrypt){ + dlFailed = true; + return new DownloadResponse{ + Data = files, + Error = dlFailed, + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown", + ErrorText = "Decryption Needed but couldn't find CDM files" + }; + } + + var reqBodyData = new{ accounting_id = "crunchyroll", asset_id = assetId, @@ -1169,9 +1197,6 @@ public class Crunchyroll{ }); } } - } else if (!options.Novids){ - //TODO - MainWindow.Instance.ShowError("Requested Video with the current settings not implemented"); } else if (options.Novids){ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); Console.WriteLine("Downloading skipped!"); @@ -1234,11 +1259,6 @@ public class Crunchyroll{ options.SkipSubs = true; } - if (options.NoSubs){ - Console.WriteLine("Subtitles downloading disabled from nosubs flag."); - options.SkipSubs = true; - } - if (!options.SkipSubs && options.DlSubs.IndexOf("none") == -1){ await DownloadSubtitles(options, pbData, audDub, fileName, files); } else{ @@ -1486,12 +1506,25 @@ public class Crunchyroll{ var playbackRequestNonDrmResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequestNonDrm); + if (!playbackRequestNonDrmResponse.IsOk && playbackRequestNonDrmResponse.ResponseContent != string.Empty){ + var s = playbackRequestNonDrmResponse.ResponseContent; + var error = StreamError.FromJson(s); + if (error != null && error.IsTooManyActiveStreamsError()){ + foreach (var errorActiveStream in error.ActiveStreams){ + await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token); + } + + playbackRequestNonDrm = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{currentMediaId}/console/switch/play", HttpMethod.Get, true, true, null); + playbackRequestNonDrm.Headers.UserAgent.ParseAdd("Crunchyroll/1.8.0 Nintendo Switch/12.3.12.0 UE4/4.27"); + playbackRequestNonDrmResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequestNonDrm); + } + } + if (playbackRequestNonDrmResponse.IsOk && playbackRequestNonDrmResponse.ResponseContent != string.Empty){ CrunchyStreamData? playStream = JsonConvert.DeserializeObject(playbackRequestNonDrmResponse.ResponseContent, SettingsJsonSerializerSettings); CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams(); if (playStream != null){ - var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{currentMediaId}/{playStream.Token}/inactive", HttpMethod.Patch, true, false, null); - var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken); + if (playStream.Token != null) await HttpClientReq.DeAuthVideo(currentMediaId, playStream.Token); if (playStream.HardSubs != null) foreach (var hardsub in playStream.HardSubs){ @@ -1514,7 +1547,7 @@ public class Crunchyroll{ } } - private async Task<(bool IsOk, PlaybackData pbData)> FetchPlaybackData(string mediaId, string mediaGuidId, CrunchyEpMetaData epMeta){ + private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string mediaId, string mediaGuidId, CrunchyEpMetaData epMeta){ PlaybackData temppbData = new PlaybackData{ Total = 0, Data = new List>>() }; bool ok = true; @@ -1569,64 +1602,86 @@ public class Crunchyroll{ // // var playbackRequestResponse22 = await HttpClientReq.Instance.SendHttpRequest(playbackRequest22); - playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null); + playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/videos/{mediaId}/streams", HttpMethod.Get, true, false, null); playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); - if (playbackRequestResponse.IsOk){ - // var temppbData2 = Helpers.Deserialize(playbackRequestResponse22.ResponseContent, SettingsJsonSerializerSettings) ?? - // new PlaybackData{ Total = 0, Data = new List>>() }; - - temppbData = new PlaybackData{ Total = 0, Data = new List>>() }; - temppbData.Data.Add(new Dictionary>()); - - CrunchyStreamData? playStream = JsonConvert.DeserializeObject(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings); - CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams(); - if (playStream != null){ - var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{mediaGuidId}/{playStream.Token}/inactive", HttpMethod.Patch, true, false, null); - var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken); - - if (playStream.HardSubs != null) - foreach (var hardsub in playStream.HardSubs){ - var stream = hardsub.Value; - derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ - Url = stream.Url, - HardsubLocale = stream.Hlang - }; - } - - derivedPlayCrunchyStreams[""] = new StreamDetails{ - Url = playStream.Url, - HardsubLocale = Locale.DefaulT - }; - - if (temppbData.Data != null) temppbData.Data[0]["drm_adaptive_switch_dash"] = derivedPlayCrunchyStreams; - - temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List{ playStream.Bifs }, MediaId = mediaId }; - temppbData.Meta.Subtitles = new Subtitles(); - foreach (var playStreamSubtitle in playStream.Subtitles){ - Subtitle sub = playStreamSubtitle.Value; - temppbData.Meta.Subtitles.Add(playStreamSubtitle.Key, new SubtitleInfo(){ Format = sub.Format, Locale = sub.Locale, Url = sub.Url }); + if (!playbackRequestResponse.IsOk && playbackRequestResponse.ResponseContent != string.Empty){ + var s = playbackRequestResponse.ResponseContent; + var error = StreamError.FromJson(s); + if (error != null && error.IsTooManyActiveStreamsError()){ + foreach (var errorActiveStream in error.ActiveStreams){ + await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token); } + + playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/videos/{mediaId}/streams", HttpMethod.Get, true, false, null); + playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); } + } + + if (!playbackRequestResponse.IsOk){ + temppbData = Helpers.Deserialize(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? + new PlaybackData{ Total = 0, Data = new List>>() }; } else{ Console.WriteLine("Request Stream URLs FAILED! Attempting fallback"); - - playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.ApiBeta}{epMeta.Playback}", HttpMethod.Get, true, true, null); + + playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null); playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); + if (!playbackRequestResponse.IsOk && playbackRequestResponse.ResponseContent != string.Empty){ + var s = playbackRequestResponse.ResponseContent; + var error = StreamError.FromJson(s); + if (error != null && error.IsTooManyActiveStreamsError()){ + foreach (var errorActiveStream in error.ActiveStreams){ + await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token); + } + + playbackRequest = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{mediaGuidId}/web/firefox/play", HttpMethod.Get, true, false, null); + playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); + } + } + if (playbackRequestResponse.IsOk){ - temppbData = Helpers.Deserialize(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? - new PlaybackData{ Total = 0, Data = new List>>() }; + temppbData = new PlaybackData{ Total = 0, Data = new List>>() }; + temppbData.Data.Add(new Dictionary>()); + + CrunchyStreamData? playStream = JsonConvert.DeserializeObject(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings); + CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams(); + if (playStream != null){ + if (playStream.Token != null) await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token); + + if (playStream.HardSubs != null) + foreach (var hardsub in playStream.HardSubs){ + var stream = hardsub.Value; + derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ + Url = stream.Url, + HardsubLocale = stream.Hlang + }; + } + + derivedPlayCrunchyStreams[""] = new StreamDetails{ + Url = playStream.Url, + HardsubLocale = Locale.DefaulT + }; + + if (temppbData.Data != null) temppbData.Data[0]["drm_adaptive_switch_dash"] = derivedPlayCrunchyStreams; + + temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List{ playStream.Bifs }, MediaId = mediaId }; + temppbData.Meta.Subtitles = new Subtitles(); + foreach (var playStreamSubtitle in playStream.Subtitles){ + Subtitle sub = playStreamSubtitle.Value; + temppbData.Meta.Subtitles.Add(playStreamSubtitle.Key, new SubtitleInfo(){ Format = sub.Format, Locale = sub.Locale, Url = sub.Url }); + } + } } else{ - Console.WriteLine("'Fallback Request Stream URLs FAILED!'"); + Console.Error.WriteLine("'Fallback Request Stream URLs FAILED!'"); ok = playbackRequestResponse.IsOk; } } } - return (IsOk: ok, pbData: temppbData); + return (IsOk: ok, pbData: temppbData, error: ok ? "" : playbackRequestResponse.ResponseContent); } private async Task ParseChapters(string currentMediaId, List compiledChapters){ diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index e7d1321..c9bef51 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -25,7 +25,7 @@ public class History(){ CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja"); if (parsedSeries == null){ - Console.WriteLine("Parse Data Invalid"); + Console.Error.WriteLine("Parse Data Invalid"); return; } @@ -420,7 +420,7 @@ public class History(){ historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + ""; episodes.Remove(episode2); } else{ - Console.WriteLine($"Could not match episode {historyEpisode.EpisodeTitle} to sonarr episode"); + Console.Error.WriteLine($"Could not match episode {historyEpisode.EpisodeTitle} to sonarr episode"); } } } @@ -568,7 +568,7 @@ public class HistorySeries : INotifyPropertyChanged{ } } catch (Exception ex){ // Handle exceptions - Console.WriteLine("Failed to load image: " + ex.Message); + Console.Error.WriteLine("Failed to load image: " + ex.Message); } } diff --git a/CRD/Utils/DRM/Widevine.cs b/CRD/Utils/DRM/Widevine.cs index d952b4d..44d7811 100644 --- a/CRD/Utils/DRM/Widevine.cs +++ b/CRD/Utils/DRM/Widevine.cs @@ -60,14 +60,14 @@ public class Widevine{ if (privateKey.Length != 0 && identifierBlob.Length != 0){ canDecrypt = true; } else if (privateKey.Length == 0){ - Console.WriteLine("Private key missing"); + Console.Error.WriteLine("Private key missing"); canDecrypt = false; } else if (identifierBlob.Length == 0){ - Console.WriteLine("Identifier blob missing"); + Console.Error.WriteLine("Identifier blob missing"); canDecrypt = false; } } catch (Exception e){ - Console.WriteLine("Widevine: " + e); + Console.Error.WriteLine("Widevine: " + e); canDecrypt = false; } } @@ -90,7 +90,7 @@ public class Widevine{ var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2); if (!response.IsOk){ - Console.WriteLine("Fallback Request Stream URLs FAILED!"); + Console.Error.WriteLine("Failed to get Keys!"); return new List(); } diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index 987951d..5a1e761 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Reflection; using CRD.Downloader; using CRD.Utils.Structs; using Newtonsoft.Json; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -23,9 +28,9 @@ public class CfgManager{ public static readonly string PathVIDEOS_DIR = WorkingDirectory + "/video/"; public static readonly string PathFONTS_DIR = WorkingDirectory + "/video/"; - + public static readonly string PathLogFile = WorkingDirectory + "/logfile.txt"; - + private static StreamWriter logFile; private static bool isLogModeEnabled = false; @@ -33,23 +38,23 @@ public class CfgManager{ if (!isLogModeEnabled){ logFile = new StreamWriter(PathLogFile); logFile.AutoFlush = true; - Console.SetOut(logFile); + Console.SetError(logFile); isLogModeEnabled = true; - Console.WriteLine("Log mode enabled."); + Console.Error.WriteLine("Log mode enabled."); } } public static void DisableLogMode(){ if (isLogModeEnabled){ logFile.Close(); - StreamWriter standardOutput = new StreamWriter(Console.OpenStandardOutput()); - standardOutput.AutoFlush = true; - Console.SetOut(standardOutput); + StreamWriter standardError = new StreamWriter(Console.OpenStandardError()); + standardError.AutoFlush = true; + Console.SetError(standardError); isLogModeEnabled = false; - Console.WriteLine("Log mode disabled."); + Console.Error.WriteLine("Log mode disabled."); } } - + public static void WriteJsonResponseToYamlFile(string jsonResponse, string filePath){ // Convert JSON to an object var deserializer = new DeserializerBuilder() @@ -122,6 +127,7 @@ public class CfgManager{ File.WriteAllText(PathCrDownloadOptions, yaml); } + public static void UpdateSettingsFromFile(){ string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty; @@ -144,35 +150,46 @@ public class CfgManager{ var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() // Important to ignore properties not present in YAML + .IgnoreUnmatchedProperties() .Build(); + var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input); var loadedOptions = deserializer.Deserialize(new StringReader(input)); + var instanceOptions = Crunchyroll.Instance.CrunOptions; - Crunchyroll.Instance.CrunOptions.Hslang = loadedOptions.Hslang; - Crunchyroll.Instance.CrunOptions.Novids = loadedOptions.Novids; - Crunchyroll.Instance.CrunOptions.Noaudio = loadedOptions.Noaudio; - Crunchyroll.Instance.CrunOptions.FileName = loadedOptions.FileName; - Crunchyroll.Instance.CrunOptions.Numbers = loadedOptions.Numbers; - Crunchyroll.Instance.CrunOptions.DlSubs = loadedOptions.DlSubs; - Crunchyroll.Instance.CrunOptions.Mp4 = loadedOptions.Mp4; - Crunchyroll.Instance.CrunOptions.FfmpegOptions = loadedOptions.FfmpegOptions; - Crunchyroll.Instance.CrunOptions.MkvmergeOptions = loadedOptions.MkvmergeOptions; - Crunchyroll.Instance.CrunOptions.Chapters = loadedOptions.Chapters; - Crunchyroll.Instance.CrunOptions.SimultaneousDownloads = loadedOptions.SimultaneousDownloads; - Crunchyroll.Instance.CrunOptions.QualityAudio = loadedOptions.QualityAudio; - Crunchyroll.Instance.CrunOptions.QualityVideo = loadedOptions.QualityVideo; - Crunchyroll.Instance.CrunOptions.DubLang = loadedOptions.DubLang; - Crunchyroll.Instance.CrunOptions.Theme = loadedOptions.Theme; - Crunchyroll.Instance.CrunOptions.AccentColor = loadedOptions.AccentColor; - Crunchyroll.Instance.CrunOptions.History = loadedOptions.History; - Crunchyroll.Instance.CrunOptions.UseNonDrmStreams = loadedOptions.UseNonDrmStreams; - Crunchyroll.Instance.CrunOptions.SonarrProperties = loadedOptions.SonarrProperties; - Crunchyroll.Instance.CrunOptions.LogMode = loadedOptions.LogMode; + foreach (PropertyInfo property in typeof(CrDownloadOptions).GetProperties()){ + var yamlMemberAttribute = property.GetCustomAttribute(); + string yamlPropertyName = yamlMemberAttribute?.Alias ?? property.Name; + + if (propertiesPresentInYaml.Contains(yamlPropertyName)){ + PropertyInfo instanceProperty = instanceOptions.GetType().GetProperty(property.Name); + if (instanceProperty != null && instanceProperty.CanWrite){ + instanceProperty.SetValue(instanceOptions, property.GetValue(loadedOptions)); + } + } + } + } + + private static HashSet GetTopLevelPropertiesInYaml(string yamlContent){ + var reader = new StringReader(yamlContent); + var yamlStream = new YamlStream(); + yamlStream.Load(reader); + + var properties = new HashSet(); + + if (yamlStream.Documents.Count > 0 && yamlStream.Documents[0].RootNode is YamlMappingNode rootNode){ + foreach (var entry in rootNode.Children){ + if (entry.Key is YamlScalarNode scalarKey){ + properties.Add(scalarKey.Value); + } + } + } + + return properties; } private static object fileLock = new object(); - + public static void WriteJsonToFile(string pathToFile, object obj){ try{ // Check if the directory exists; if not, create it. @@ -191,7 +208,7 @@ public class CfgManager{ } } } catch (Exception ex){ - Console.WriteLine($"An error occurred: {ex.Message}"); + Console.Error.WriteLine($"An error occurred: {ex.Message}"); } } diff --git a/CRD/Utils/Files/FileNameManager.cs b/CRD/Utils/Files/FileNameManager.cs index 5d50abb..afe76bc 100644 --- a/CRD/Utils/Files/FileNameManager.cs +++ b/CRD/Utils/Files/FileNameManager.cs @@ -22,7 +22,7 @@ public class FileNameManager{ var variable = overriddenVars.FirstOrDefault(v => v.Name == varName); if (variable == null){ - Console.WriteLine($"[ERROR] Found variable '{match}' in fileName but no values was internally found!"); + Console.Error.WriteLine($"[ERROR] Found variable '{match}' in fileName but no values was internally found!"); continue; } @@ -51,13 +51,13 @@ public class FileNameManager{ foreach (var item in overrides){ int index = item.IndexOf('='); if (index == -1){ - Console.WriteLine($"Error: Invalid override format '{item}'"); + Console.Error.WriteLine($"Error: Invalid override format '{item}'"); continue; } string[] parts ={ item.Substring(0, index), item.Substring(index + 1) }; if (!(parts[1].StartsWith("'") && parts[1].EndsWith("'") && parts[1].Length >= 2)){ - Console.WriteLine($"Error: Invalid value format for '{item}'"); + Console.Error.WriteLine($"Error: Invalid value format for '{item}'"); continue; } @@ -67,7 +67,7 @@ public class FileNameManager{ if (alreadyIndex > -1){ if (variables[alreadyIndex].Type == "number"){ if (!float.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float numberValue)){ - Console.WriteLine($"Error: Wrong type for '{item}'"); + Console.Error.WriteLine($"Error: Wrong type for '{item}'"); continue; } diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index 7f0d66e..379ec31 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -81,8 +81,8 @@ public class HlsDownloader{ $"Current: {{ total: {_data.M3U8Json?.Segments.Count} }}"); } } catch (Exception e){ - Console.WriteLine("Resume failed, downloading will not be resumed!"); - Console.WriteLine(e.Message); + Console.Error.WriteLine("Resume failed, downloading will not be resumed!"); + Console.Error.WriteLine(e.Message); } } @@ -169,7 +169,7 @@ public class HlsDownloader{ try{ await Task.WhenAll(keyTasks.Values); } catch (Exception ex){ - Console.WriteLine($"Error downloading keys: {ex.Message}"); + Console.Error.WriteLine($"Error downloading keys: {ex.Message}"); throw; } diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 0cfe291..a35d01e 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Text.RegularExpressions; @@ -21,7 +22,7 @@ public class Helpers{ try{ return JsonConvert.DeserializeObject(json, serializerSettings); } catch (JsonException ex){ - Console.WriteLine($"Error deserializing JSON: {ex.Message}"); + Console.Error.WriteLine($"Error deserializing JSON: {ex.Message}"); throw; } } @@ -41,7 +42,7 @@ public class Helpers{ if (string.IsNullOrEmpty(value)){ return Locale.DefaulT; } - + return Locale.Unknown; // Return default if not found } @@ -58,6 +59,35 @@ public class Helpers{ return milliseconds + highResTimestamp; } + public static void ConvertChapterFileForFFMPEG(string chapterFilePath) + { + var chapterLines = File.ReadAllLines(chapterFilePath); + var ffmpegChapterLines = new List { ";FFMETADATA1" }; + + for (int i = 0; i < chapterLines.Length; i += 2) + { + var timeLine = chapterLines[i]; + var nameLine = chapterLines[i + 1]; + + var timeParts = timeLine.Split('='); + var nameParts = nameLine.Split('='); + + if (timeParts.Length == 2 && nameParts.Length == 2) + { + var startTime = TimeSpan.Parse(timeParts[1]).TotalMilliseconds; + var endTime = i + 2 < chapterLines.Length ? TimeSpan.Parse(chapterLines[i + 2].Split('=')[1]).TotalMilliseconds : startTime + 10000; + + ffmpegChapterLines.Add("[CHAPTER]"); + ffmpegChapterLines.Add("TIMEBASE=1/1000"); + ffmpegChapterLines.Add($"START={startTime}"); + ffmpegChapterLines.Add($"END={endTime}"); + ffmpegChapterLines.Add($"title={nameParts[1]}"); + } + } + + File.WriteAllLines(chapterFilePath, ffmpegChapterLines); + } + public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsync(string type, string bin, string command){ using (var process = new Process()){ process.StartInfo.FileName = bin; @@ -75,7 +105,7 @@ public class Helpers{ process.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)){ - Console.WriteLine($"ERROR: {e.Data}"); + Console.WriteLine($"{e.Data}"); } }; @@ -137,7 +167,7 @@ public class Helpers{ return words; } - + private static double CosineSimilarity(Dictionary vector1, Dictionary vector2){ var intersection = vector1.Keys.Intersect(vector2.Keys); diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index 4ab83fa..8b67e64 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -97,7 +97,7 @@ public class HttpClientReq{ return (IsOk: true, ResponseContent: content); } catch (Exception e){ - Console.WriteLine($"Error: {e} \n Response: {content}"); + Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}"); return (IsOk: false, ResponseContent: content); } } @@ -124,6 +124,11 @@ public class HttpClientReq{ return request; } + public static async Task DeAuthVideo(string currentMediaId, string token){ + var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{currentMediaId}/{token}/inactive", HttpMethod.Patch, true, false, null); + var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken); + } + public HttpClient GetHttpClient(){ return client; } @@ -132,7 +137,7 @@ public class HttpClientReq{ public static class Api{ public static readonly string ApiBeta = "https://beta-api.crunchyroll.com"; - public static readonly string ApiN = "https://crunchyroll.com"; + public static readonly string ApiN = "https://www.crunchyroll.com"; public static readonly string BetaAuth = ApiBeta + "/auth/v1/token"; public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile"; @@ -143,7 +148,7 @@ public static class Api{ public static readonly string BetaCms = ApiBeta + "/cms/v2"; public static readonly string DRM = ApiBeta + "/drm/v1/auth"; - + public static readonly string Subscription = ApiBeta + "/subs/v3/subscriptions/"; public static readonly string CmsN = ApiN + "/content/v2/cms"; diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 386114b..1c370ea 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -14,7 +14,7 @@ public class Merger{ public Merger(MergerOptions options){ this.options = options; if (this.options.SkipSubMux != null && this.options.SkipSubMux == true){ - this.options.Subtitles = new List(); + this.options.Subtitles = new(); } if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){ @@ -32,31 +32,11 @@ public class Merger{ var hasVideo = false; if (!options.mp3){ - foreach (var vid in options.VideoAndAudio){ - if (vid.Delay != null && hasVideo){ - args.Add($"-itsoffset -{Math.Ceiling((double)vid.Delay * 1000)}ms"); - } - - args.Add($"-i \"{vid.Path}\""); - if (!hasVideo || options.KeepAllVideos == true){ - metaData.Add($"-map {index}:a -map {index}:v"); - metaData.Add($"-metadata:s:a:{audioIndex} language={vid.Language.Code}"); - metaData.Add($"-metadata:s:v:{index} title=\"{options.VideoTitle}\""); - hasVideo = true; - } else{ - metaData.Add($"-map {index}:a"); - metaData.Add($"-metadata:s:a:{audioIndex} language={vid.Language.Code}"); - } - - audioIndex++; - index++; - } - foreach (var vid in options.OnlyVid){ if (!hasVideo || options.KeepAllVideos == true){ args.Add($"-i \"{vid.Path}\""); - metaData.Add($"-map {index} -map -{index}:a"); - metaData.Add($"-metadata:s:v:{index} title=\"{options.VideoTitle}\""); + metaData.Add($"-map {index}:v"); + metaData.Add($"-metadata:s:v:{index} title=\"{(options.VideoTitle ?? vid.Language.Name)}\""); hasVideo = true; index++; } @@ -64,12 +44,21 @@ public class Merger{ foreach (var aud in options.OnlyAudio){ args.Add($"-i \"{aud.Path}\""); - metaData.Add($"-map {index}"); + metaData.Add($"-map {index}:a"); metaData.Add($"-metadata:s:a:{audioIndex} language={aud.Language.Code}"); index++; audioIndex++; } + if (options.Chapters != null && options.Chapters.Count > 0){ + + Helpers.ConvertChapterFileForFFMPEG(options.Chapters[0].Path); + + args.Add($"-i \"{options.Chapters[0].Path}\""); + metaData.Add($"-map_metadata {index}"); + index++; + } + foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){ if (sub.value.Delay != null){ args.Add($"-itsoffset -{Math.Ceiling((double)sub.value.Delay * 1000)}ms"); @@ -77,7 +66,7 @@ public class Merger{ args.Add($"-i \"{sub.value.File}\""); } - + if (options.Output.EndsWith(".mkv", StringComparison.OrdinalIgnoreCase)){ if (options.Fonts != null){ int fontIndex = 0; @@ -87,7 +76,7 @@ public class Merger{ } } } - + args.AddRange(metaData); args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}")); args.Add("-c:v copy"); @@ -95,6 +84,9 @@ public class Merger{ args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass"); args.AddRange(options.Subtitles.Select((sub, subindex) => $"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}")); + + + if (options.Options.ffmpeg?.Count > 0){ args.AddRange(options.Options.ffmpeg); } @@ -112,13 +104,15 @@ public class Merger{ index++; audioIndex++; } - + args.Add("-acodec libmp3lame"); args.Add("-ab 192k"); args.Add($"\"{options.Output}\""); return string.Join(" ", args); } + + public string MkvMerge(){ List args = new List(); @@ -135,7 +129,7 @@ public class Merger{ args.Add("--video-tracks 0"); args.Add("--no-audio"); - string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}{(options.Simul == true ? " [Simulcast]" : " [Uncut]")}"; + string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}"; args.Add($"--track-name 0:\"{trackName}\""); args.Add($"--language 0:{vid.Language.Code}"); @@ -144,47 +138,6 @@ public class Merger{ } } - foreach (var vid in options.VideoAndAudio){ - string audioTrackNum = options.InverseTrackOrder == true ? "0" : "1"; - string videoTrackNum = options.InverseTrackOrder == true ? "1" : "0"; - - if (vid.Delay.HasValue){ - double delay = vid.Delay ?? 0; - args.Add($"--sync {audioTrackNum}:-{Math.Ceiling(delay * 1000)}"); - } - - if (!hasVideo || options.KeepAllVideos == true){ - args.Add($"--video-tracks {videoTrackNum}"); - args.Add($"--audio-tracks {audioTrackNum}"); - - string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}{(options.Simul == true ? " [Simulcast]" : " [Uncut]")}"; - args.Add($"--track-name 0:\"{trackName}\""); // Assuming trackName applies to video if present - args.Add($"--language {audioTrackNum}:{vid.Language.Code}"); - - if (options.Defaults.Audio.Code == vid.Language.Code){ - args.Add($"--default-track {audioTrackNum}"); - } else{ - args.Add($"--default-track {audioTrackNum}:0"); - } - - hasVideo = true; - } else{ - args.Add("--no-video"); - args.Add($"--audio-tracks {audioTrackNum}"); - - if (options.Defaults.Audio.Code == vid.Language.Code){ - args.Add($"--default-track {audioTrackNum}"); - } else{ - args.Add($"--default-track {audioTrackNum}:0"); - } - - args.Add($"--track-name {audioTrackNum}:\"{vid.Language.Name}\""); - args.Add($"--language {audioTrackNum}:{vid.Language.Code}"); - } - - args.Add($"\"{vid.Path}\""); - } - foreach (var aud in options.OnlyAudio){ string trackName = aud.Language.Name; args.Add($"--track-name 0:\"{trackName}\""); @@ -245,51 +198,6 @@ public class Merger{ return string.Join(" ", args); } - // public async Task CreateDelays(){ - // // Don't bother scanning if there is only 1 vna stream - // if (options.VideoAndAudio.Count > 1){ - // var bin = await YamlCfg.LoadBinCfg(); - // var vnas = this.options.VideoAndAudio; - // - // // Get and set durations on each videoAndAudio Stream - // foreach (var vna in vnas){ - // var streamInfo = await FFProbe(vna.Path, bin.FFProbe); - // var videoInfo = streamInfo.Streams.Where(stream => stream.CodecType == "video").FirstOrDefault(); - // vna.Duration = int.Parse(videoInfo.Duration); - // } - // - // // Sort videoAndAudio streams by duration (shortest first) - // vnas.Sort((a, b) => { - // if (a.Duration == 0 || b.Duration == 0) return -1; - // return a.Duration.CompareTo(b.Duration); - // }); - // - // // Set Delays - // var shortestDuration = vnas[0].Duration; - // foreach (var (vna, index) in vnas.Select((vna, index) => (vna, index))){ - // // Don't calculate the shortestDuration track - // if (index == 0){ - // if (!vna.IsPrimary) - // Console.WriteLine("Shortest video isn't primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues."); - // continue; - // } - // - // if (vna.Duration > 0 && shortestDuration > 0){ - // // Calculate the tracks delay - // vna.Delay = Math.Ceiling((vna.Duration - shortestDuration) * 1000) / 1000; - // - // var subtitles = this.options.Subtitles.Where(sub => sub.Language.Code == vna.Lang.Code).ToList(); - // foreach (var (sub, subIndex) in subtitles.Select((sub, subIndex) => (sub, subIndex))){ - // if (vna.IsPrimary) - // subtitles[subIndex].Delay = vna.Delay; - // else if (sub.ClosedCaption) - // subtitles[subIndex].Delay = vna.Delay; - // } - // } - // } - // } - // } - public async Task Merge(string type, string bin){ string command = type switch{ @@ -299,7 +207,7 @@ public class Merger{ }; if (string.IsNullOrEmpty(command)){ - Console.WriteLine("Unable to merge files."); + Console.Error.WriteLine("Unable to merge files."); return; } @@ -309,7 +217,7 @@ public class Merger{ if (!result.IsOk && type == "mkvmerge" && result.ErrorCode == 1){ Console.WriteLine($"[{type}] Mkvmerge finished with at least one warning"); } else if (!result.IsOk){ - Console.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}"); + Console.Error.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}"); } else{ Console.WriteLine($"[{type} Done]"); } @@ -319,7 +227,7 @@ public class Merger{ public void CleanUp(){ // Combine all media file lists and iterate through them var allMediaFiles = options.OnlyAudio.Concat(options.OnlyVid) - .Concat(options.VideoAndAudio).ToList(); + .ToList(); allMediaFiles.ForEach(file => DeleteFile(file.Path)); allMediaFiles.ForEach(file => DeleteFile(file.Path + ".resume")); @@ -336,7 +244,7 @@ public class Merger{ File.Delete(filePath); } } catch (Exception ex){ - Console.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}"); + Console.Error.WriteLine($"Failed to delete file {filePath}. Error: {ex.Message}"); // Handle exceptions if you need to log them or throw } } @@ -382,7 +290,6 @@ public class CrunchyMuxOptions{ } public class MergerOptions{ - public List VideoAndAudio{ get; set; } = new List(); public List OnlyVid{ get; set; } = new List(); public List OnlyAudio{ get; set; } = new List(); public List Subtitles{ get; set; } = new List(); @@ -390,8 +297,6 @@ public class MergerOptions{ public string CcTag{ get; set; } public string Output{ get; set; } public string VideoTitle{ get; set; } - public bool? Simul{ get; set; } - public bool? InverseTrackOrder{ get; set; } public bool? KeepAllVideos{ get; set; } public List Fonts{ get; set; } = new List(); public bool? SkipSubMux{ get; set; } diff --git a/CRD/Utils/Sonarr/SonarrClient.cs b/CRD/Utils/Sonarr/SonarrClient.cs index 7ed7ed0..913faca 100644 --- a/CRD/Utils/Sonarr/SonarrClient.cs +++ b/CRD/Utils/Sonarr/SonarrClient.cs @@ -92,7 +92,7 @@ public class SonarrClient{ series = JsonConvert.DeserializeObject>(json) ?? []; } catch (Exception e){ MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e); - Console.WriteLine("Sonarr GetSeries error \n" + e); + Console.Error.WriteLine("Sonarr GetSeries error \n" + e); } return series; @@ -107,7 +107,7 @@ public class SonarrClient{ episodes = JsonConvert.DeserializeObject>(json) ?? []; } catch (Exception e){ MainWindow.Instance.ShowError("Sonarr GetEpisodes error \n" + e); - Console.WriteLine("Sonarr GetEpisodes error \n" + e); + Console.Error.WriteLine("Sonarr GetEpisodes error \n" + e); } return episodes; @@ -121,7 +121,7 @@ public class SonarrClient{ episode = JsonConvert.DeserializeObject(json) ?? new SonarrEpisode(); } catch (Exception e){ MainWindow.Instance.ShowError("Sonarr GetEpisode error \n" + e); - Console.WriteLine("Sonarr GetEpisode error \n" + e); + Console.Error.WriteLine("Sonarr GetEpisode error \n" + e); } return episode; diff --git a/CRD/Utils/Structs/CalendarStructs.cs b/CRD/Utils/Structs/CalendarStructs.cs index a6178bc..997f11c 100644 --- a/CRD/Utils/Structs/CalendarStructs.cs +++ b/CRD/Utils/Structs/CalendarStructs.cs @@ -63,7 +63,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{ } } catch (Exception ex){ // Handle exceptions - Console.WriteLine("Failed to load image: " + ex.Message); + Console.Error.WriteLine("Failed to load image: " + ex.Message); } } } \ No newline at end of file diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/CrDownloadOptions.cs index f7f68e7..d819233 100644 --- a/CRD/Utils/Structs/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/CrDownloadOptions.cs @@ -50,9 +50,9 @@ public class CrDownloadOptions{ [YamlIgnore] public bool SkipSubs{ get; set; } - [YamlIgnore] - public bool NoSubs{ get; set; } - + [YamlMember(Alias = "mux_skip_subs", ApplyNamingConventions = false)] + public bool SkipSubsMux{ get; set; } + [YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)] public bool Mp4{ get; set; } @@ -71,16 +71,16 @@ public class CrDownloadOptions{ [YamlMember(Alias = "mux_mkvmerge", ApplyNamingConventions = false)] public List MkvmergeOptions{ get; set; } - [YamlIgnore] - public LanguageItem DefaultSub{ get; set; } + [YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)] + public string DefaultSub{ get; set; } - [YamlIgnore] - public LanguageItem DefaultAudio{ get; set; } + [YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)] + public string DefaultAudio{ get; set; } [YamlIgnore] public string CcTag{ get; set; } - [YamlIgnore] + [YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)] public bool DlVideoOnce{ get; set; } [YamlIgnore] diff --git a/CRD/Utils/Structs/CrProfile.cs b/CRD/Utils/Structs/CrProfile.cs index 17f1cb3..dbc221e 100644 --- a/CRD/Utils/Structs/CrProfile.cs +++ b/CRD/Utils/Structs/CrProfile.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using Newtonsoft.Json; namespace CRD.Utils.Structs; @@ -12,4 +14,44 @@ public class CrProfile{ [JsonProperty("preferred_content_subtitle_language")] public string? PreferredContentSubtitleLanguage{ get; set; } + + [JsonIgnore] + public Subscription? Subscription{ get; set; } + + [JsonIgnore] + public bool HasPremium{ get; set; } +} + + +public class Subscription{ + [JsonProperty("account_id")] + public int AccountId{ get; set; } + [JsonProperty("ctp_account_id")] + public string? CtpAccountId{ get; set; } + [JsonProperty("cycle_duration")] + public string? CycleDuration{ get; set; } + [JsonProperty("next_renewal_date")] + public DateTime NextRenewalDate{ get; set; } + [JsonProperty("currency_code")] + public string? CurrencyCode{ get; set; } + [JsonProperty("is_active")] + public bool IsActive{ get; set; } + [JsonProperty("tax_included")] + public bool TaxIncluded{ get; set; } + [JsonProperty("subscription_products")] + public List? SubscriptionProducts{ get; set; } +} + +public class SubscriptionProduct{ + [JsonProperty("currency_code")] + public string? CurrencyCode{ get; set; } + public string? Amount{ get; set; } + [JsonProperty("is_cancelled")] + public bool IsCancelled{ get; set; } + [JsonProperty("effective_date")] + public DateTime EffectiveDate{ get; set; } + public string? Sku{ get; set; } + public string? Tier{ get; set; } + [JsonProperty("active_free_trial")] + public bool ActiveFreeTrial{ get; set; } } \ No newline at end of file diff --git a/CRD/Utils/Structs/StreamLimits.cs b/CRD/Utils/Structs/StreamLimits.cs new file mode 100644 index 0000000..0da9a8e --- /dev/null +++ b/CRD/Utils/Structs/StreamLimits.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs; + +public class StreamError{ + [JsonPropertyName("error")] + public string Error{ get; set; } + + [JsonPropertyName("activeStreams")] + public List ActiveStreams{ get; set; } + + public static StreamError? FromJson(string json){ + try{ + return JsonConvert.DeserializeObject(json); + } catch (Exception e){ + Console.Error.WriteLine(e); + return null; + } + } + + public bool IsTooManyActiveStreamsError(){ + return Error == "TOO_MANY_ACTIVE_STREAMS"; + } +} + +public class ActiveStream{ + [JsonPropertyName("deviceSubtype")] + public string DeviceSubtype{ get; set; } + + [JsonPropertyName("accountId")] + public string AccountId{ get; set; } + + [JsonPropertyName("deviceType")] + public string DeviceType{ get; set; } + + [JsonPropertyName("subscription")] + public string Subscription{ get; set; } + + [JsonPropertyName("maxKeepAliveSeconds")] + public int MaxKeepAliveSeconds{ get; set; } + + [JsonPropertyName("ttl")] + public int Ttl{ get; set; } + + [JsonPropertyName("episodeIdentity")] + public string EpisodeIdentity{ get; set; } + + [JsonPropertyName("tabId")] + public string TabId{ get; set; } + + [JsonPropertyName("country")] + public string Country{ get; set; } + + [JsonPropertyName("clientId")] + public string ClientId{ get; set; } + + [JsonPropertyName("active")] + public bool Active{ get; set; } + + [JsonPropertyName("deviceId")] + public string DeviceId{ get; set; } + + [JsonPropertyName("token")] + public string Token{ get; set; } + + [JsonPropertyName("assetId")] + public string AssetId{ get; set; } + + [JsonPropertyName("sessionType")] + public string SessionType{ get; set; } + + [JsonPropertyName("contentId")] + public string ContentId{ get; set; } + + [JsonPropertyName("usesStreamLimits")] + public bool UsesStreamLimits{ get; set; } + + [JsonPropertyName("playbackType")] + public string PlaybackType{ get; set; } + + [JsonPropertyName("pk")] + public string Pk{ get; set; } + + [JsonPropertyName("id")] + public string Id{ get; set; } + + [JsonPropertyName("createdTimestamp")] + public long CreatedTimestamp{ get; set; } + + [JsonPropertyName("lastKeepAliveTimestamp")] + public long LastKeepAliveTimestamp{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Updater/Updater.cs b/CRD/Utils/Updater/Updater.cs index de33052..a396576 100644 --- a/CRD/Utils/Updater/Updater.cs +++ b/CRD/Utils/Updater/Updater.cs @@ -73,7 +73,7 @@ public class Updater : INotifyPropertyChanged{ } } } catch (Exception e){ - Console.WriteLine("Failed to get Update information"); + Console.Error.WriteLine("Failed to get Update information"); return false; } } @@ -119,11 +119,11 @@ public class Updater : INotifyPropertyChanged{ ApplyUpdate(extractPath); } else{ - Console.WriteLine("Failed to get Update"); + Console.Error.WriteLine("Failed to get Update"); } } } catch (Exception e){ - Console.WriteLine($"Failed to get Update: {e.Message}"); + Console.Error.WriteLine($"Failed to get Update: {e.Message}"); } } diff --git a/CRD/ViewModels/AccountPageViewModel.cs b/CRD/ViewModels/AccountPageViewModel.cs index c69b3f5..774ae07 100644 --- a/CRD/ViewModels/AccountPageViewModel.cs +++ b/CRD/ViewModels/AccountPageViewModel.cs @@ -1,31 +1,73 @@ using System; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Avalonia.Media.Imaging; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; +using CRD.Utils.Structs; using CRD.Views.Utils; using FluentAvalonia.UI.Controls; namespace CRD.ViewModels; public partial class AccountPageViewModel : ViewModelBase{ - [ObservableProperty] private Bitmap? _profileImage; + [ObservableProperty] + private Bitmap? _profileImage; - [ObservableProperty] private string _profileName = ""; + [ObservableProperty] + private string _profileName = ""; - [ObservableProperty] private string _loginLogoutText = ""; + [ObservableProperty] + private string _loginLogoutText = ""; + [ObservableProperty] + private string _remainingTime = ""; + + private static DispatcherTimer? _timer; + private DateTime _targetTime; + private bool IsCancelled = false; public AccountPageViewModel(){ UpdatetProfile(); + + + + } + + private void Timer_Tick(object sender, EventArgs e){ + var remaining = _targetTime - DateTime.UtcNow; + if (remaining <= TimeSpan.Zero){ + RemainingTime = "No active Subscription"; + _timer.Stop(); + } else{ + RemainingTime = $"{(IsCancelled ? "Subscription ending in: ":"Subscription refreshing in: ")}{remaining:dd\\:hh\\:mm\\:ss}"; + } } public void UpdatetProfile(){ ProfileName = Crunchyroll.Instance.Profile.Username; // Default or fetched user name LoginLogoutText = Crunchyroll.Instance.Profile.Username == "???" ? "Login" : "Logout"; // Default state LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" + Crunchyroll.Instance.Profile.Avatar); + + if (Crunchyroll.Instance.Profile.Subscription != null && Crunchyroll.Instance.Profile.Subscription?.SubscriptionProducts != null){ + var sub = Crunchyroll.Instance.Profile.Subscription?.SubscriptionProducts.First(); + _targetTime = Crunchyroll.Instance.Profile.Subscription.NextRenewalDate; + if (sub != null){ + IsCancelled = sub.IsCancelled; + } + + _timer = new DispatcherTimer{ + Interval = TimeSpan.FromSeconds(1) + }; + _timer.Tick += Timer_Tick; + _timer.Start(); + } else{ + RemainingTime = "No active Subscription"; + } + } [RelayCommand] @@ -60,7 +102,7 @@ public partial class AccountPageViewModel : ViewModelBase{ } } catch (Exception ex){ // Handle exceptions - Console.WriteLine("Failed to load image: " + ex.Message); + Console.Error.WriteLine("Failed to load image: " + ex.Message); } } } \ No newline at end of file diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index 8594960..479bf30 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -156,7 +156,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } } else{ - Console.WriteLine("Unnkown input"); + Console.Error.WriteLine("Unnkown input"); } } @@ -248,7 +248,7 @@ public class ItemModel(string imageUrl, string description, string time, string } } catch (Exception ex){ // Handle exceptions - Console.WriteLine("Failed to load image: " + ex.Message); + Console.Error.WriteLine("Failed to load image: " + ex.Message); } } } \ No newline at end of file diff --git a/CRD/ViewModels/DownloadsPageViewModel.cs b/CRD/ViewModels/DownloadsPageViewModel.cs index 65b730d..a7d1cb9 100644 --- a/CRD/ViewModels/DownloadsPageViewModel.cs +++ b/CRD/ViewModels/DownloadsPageViewModel.cs @@ -42,7 +42,7 @@ public partial class DownloadsPageViewModel : ViewModelBase{ if (downloadItem != null){ Crunchyroll.Instance.DownloadItemModels.Remove(downloadItem); } else{ - Console.WriteLine("Failed to Remove Episode from list"); + Console.Error.WriteLine("Failed to Remove Episode from list"); } } } @@ -252,7 +252,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{ } } catch (Exception ex){ // Handle exceptions - Console.WriteLine("Failed to load image: " + ex.Message); + Console.Error.WriteLine("Failed to load image: " + ex.Message); } } } \ No newline at end of file diff --git a/CRD/ViewModels/MainWindowViewModel.cs b/CRD/ViewModels/MainWindowViewModel.cs index d3ba259..fd08265 100644 --- a/CRD/ViewModels/MainWindowViewModel.cs +++ b/CRD/ViewModels/MainWindowViewModel.cs @@ -37,7 +37,7 @@ public partial class MainWindowViewModel : ViewModelBase{ File.Delete(backupFilePath); Console.WriteLine($"Deleted old updater file: {backupFilePath}"); } catch (Exception ex) { - Console.WriteLine($"Failed to delete old updater file: {ex.Message}"); + Console.Error.WriteLine($"Failed to delete old updater file: {ex.Message}"); } } else { Console.WriteLine("No old updater file found to delete."); diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index 3f93e9c..05f18ae 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -87,7 +87,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ UseShellExecute = true }); } catch (Exception e){ - Console.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}"); + Console.Error.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}"); } } } \ No newline at end of file diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index c322b0b..1d6a643 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using System.Net.Mime; using System.Reflection; @@ -34,6 +35,12 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _muxToMp4; + + [ObservableProperty] + private bool _downloadVideoForEveryDub; + + [ObservableProperty] + private bool _skipSubMux; [ObservableProperty] private bool _history; @@ -51,10 +58,16 @@ public partial class SettingsPageViewModel : ViewModelBase{ private string _fileName = ""; [ObservableProperty] - private string _mkvMergeOptions = ""; + private ObservableCollection _mkvMergeOptions = new(); [ObservableProperty] - private string _ffmpegOptions = ""; + private string _mkvMergeOption = ""; + + [ObservableProperty] + private string _ffmpegOption = ""; + + [ObservableProperty] + private ObservableCollection _ffmpegOptions = new(); [ObservableProperty] private string _selectedSubs = "all"; @@ -68,6 +81,12 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private ObservableCollection _selectedDubLang = new(); + [ObservableProperty] + private ComboBoxItem _selectedDefaultDubLang; + + [ObservableProperty] + private ComboBoxItem _selectedDefaultSubLang; + [ObservableProperty] private ComboBoxItem? _selectedVideoQuality; @@ -103,7 +122,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _sonarrUseSonarrNumbering = false; - + [ObservableProperty] private bool _logMode = false; @@ -190,6 +209,14 @@ public partial class SettingsPageViewModel : ViewModelBase{ public ObservableCollection DubLangList{ get; } = new(){ }; + + public ObservableCollection DefaultDubLangList{ get; } = new(){ + }; + + public ObservableCollection DefaultSubLangList{ get; } = new(){ + }; + + public ObservableCollection SubLangList{ get; } = new(){ new ListBoxItem(){ Content = "all" }, new ListBoxItem(){ Content = "none" }, @@ -210,6 +237,8 @@ public partial class SettingsPageViewModel : ViewModelBase{ HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale }); DubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); + DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); + DefaultSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); } CrDownloadOptions options = Crunchyroll.Instance.CrunOptions; @@ -217,6 +246,12 @@ public partial class SettingsPageViewModel : ViewModelBase{ ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null; SelectedHSLang = hsLang ?? HardSubLangList[0]; + ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null; + SelectedDefaultDubLang = defaultDubLang ?? DefaultDubLangList[0]; + + ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null; + SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0]; + var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList(); SelectedSubLang.Clear(); @@ -246,8 +281,10 @@ public partial class SettingsPageViewModel : ViewModelBase{ UseNonDrmEndpoint = options.UseNonDrmStreams; DownloadVideo = !options.Novids; DownloadAudio = !options.Noaudio; + DownloadVideoForEveryDub = !options.DlVideoOnce; DownloadChapters = options.Chapters; MuxToMp4 = options.Mp4; + SkipSubMux = options.SkipSubsMux; LeadingNumbers = options.Numbers; FileName = options.FileName; SimultaneousDownloads = options.SimultaneousDownloads; @@ -268,11 +305,26 @@ public partial class SettingsPageViewModel : ViewModelBase{ History = options.History; - //TODO - Mux Options + MkvMergeOptions.Clear(); + if (options.MkvmergeOptions != null){ + foreach (var mkvmergeParam in options.MkvmergeOptions){ + MkvMergeOptions.Add(new MuxingParam(){ ParamValue = mkvmergeParam }); + } + } + + FfmpegOptions.Clear(); + if (options.FfmpegOptions != null){ + foreach (var ffmpegParam in options.FfmpegOptions){ + FfmpegOptions.Add(new MuxingParam(){ ParamValue = ffmpegParam }); + } + } SelectedSubLang.CollectionChanged += Changes; SelectedDubLang.CollectionChanged += Changes; + MkvMergeOptions.CollectionChanged += Changes; + FfmpegOptions.CollectionChanged += Changes; + settingsLoaded = true; } @@ -285,8 +337,10 @@ public partial class SettingsPageViewModel : ViewModelBase{ Crunchyroll.Instance.CrunOptions.Novids = !DownloadVideo; Crunchyroll.Instance.CrunOptions.Noaudio = !DownloadAudio; + Crunchyroll.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub; Crunchyroll.Instance.CrunOptions.Chapters = DownloadChapters; Crunchyroll.Instance.CrunOptions.Mp4 = MuxToMp4; + Crunchyroll.Instance.CrunOptions.SkipSubsMux = SkipSubMux; Crunchyroll.Instance.CrunOptions.Numbers = LeadingNumbers; Crunchyroll.Instance.CrunOptions.FileName = FileName; @@ -302,6 +356,8 @@ public partial class SettingsPageViewModel : ViewModelBase{ Crunchyroll.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang; + Crunchyroll.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + ""; + Crunchyroll.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + ""; List dubLangs = new List(); foreach (var listBoxItem in SelectedDubLang){ @@ -340,9 +396,20 @@ public partial class SettingsPageViewModel : ViewModelBase{ Crunchyroll.Instance.CrunOptions.SonarrProperties = props; Crunchyroll.Instance.CrunOptions.LogMode = LogMode; - - //TODO - Mux Options + List mkvmergeParams = new List(); + foreach (var mkvmergeParam in MkvMergeOptions){ + mkvmergeParams.Add(mkvmergeParam.ParamValue); + } + + Crunchyroll.Instance.CrunOptions.MkvmergeOptions = mkvmergeParams; + + List ffmpegParams = new List(); + foreach (var ffmpegParam in FfmpegOptions){ + ffmpegParams.Add(ffmpegParam.ParamValue); + } + + Crunchyroll.Instance.CrunOptions.FfmpegOptions = ffmpegParams; CfgManager.WriteSettingsToFile(); @@ -369,6 +436,32 @@ public partial class SettingsPageViewModel : ViewModelBase{ } } + [RelayCommand] + public void AddMkvMergeParam(){ + MkvMergeOptions.Add(new MuxingParam(){ ParamValue = MkvMergeOption }); + MkvMergeOption = ""; + RaisePropertyChanged(nameof(MkvMergeOptions)); + } + + [RelayCommand] + public void RemoveMkvMergeParam(MuxingParam param){ + MkvMergeOptions.Remove(param); + RaisePropertyChanged(nameof(MkvMergeOptions)); + } + + [RelayCommand] + public void AddFfmpegParam(){ + FfmpegOptions.Add(new MuxingParam(){ ParamValue = FfmpegOption }); + FfmpegOption = ""; + RaisePropertyChanged(nameof(FfmpegOptions)); + } + + [RelayCommand] + public void RemoveFfmpegParam(MuxingParam param){ + FfmpegOptions.Remove(param); + RaisePropertyChanged(nameof(FfmpegOptions)); + } + partial void OnCurrentAppThemeChanged(ComboBoxItem? value){ if (value?.Content?.ToString() == "System"){ _faTheme.PreferSystemTheme = true; @@ -419,7 +512,6 @@ public partial class SettingsPageViewModel : ViewModelBase{ UpdateSettings(); } - private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ UpdateSettings(); } @@ -500,4 +592,24 @@ public partial class SettingsPageViewModel : ViewModelBase{ CfgManager.DisableLogMode(); } } + + partial void OnSelectedDefaultDubLangChanged(ComboBoxItem value){ + UpdateSettings(); + } + + partial void OnSelectedDefaultSubLangChanged(ComboBoxItem value){ + UpdateSettings(); + } + + partial void OnSkipSubMuxChanged(bool value){ + UpdateSettings(); + } + + partial void OnDownloadVideoForEveryDubChanged(bool value){ + UpdateSettings(); + } +} + +public class MuxingParam{ + public string ParamValue{ get; set; } } \ No newline at end of file diff --git a/CRD/Views/AccountPageView.axaml b/CRD/Views/AccountPageView.axaml index 26e1ead..7e9320f 100644 --- a/CRD/Views/AccountPageView.axaml +++ b/CRD/Views/AccountPageView.axaml @@ -22,10 +22,13 @@ - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + +