diff --git a/CRD/Downloader/Crunchyroll/CRAuth.cs b/CRD/Downloader/Crunchyroll/CRAuth.cs index 8a23f62..ee29344 100644 --- a/CRD/Downloader/Crunchyroll/CRAuth.cs +++ b/CRD/Downloader/Crunchyroll/CRAuth.cs @@ -81,8 +81,8 @@ public class CrAuth{ string uuid = Guid.NewGuid().ToString(); var formData = new Dictionary{ - { "username", data.Username }, - { "password", data.Password }, + { "username", data.Username }, + { "password", data.Password }, { "grant_type", "password" }, { "scope", "offline_access" }, { "device_id", uuid }, @@ -115,9 +115,16 @@ public class CrAuth{ } else{ if (response.ResponseContent.Contains("invalid_credentials")){ MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 10)); + } else if (response.ResponseContent.Contains("Just a moment...") || + response.ResponseContent.Contains("Access denied") || + response.ResponseContent.Contains("Attention Required! | Cloudflare") || + response.ResponseContent.Trim().Equals("error code: 1020") || + response.ResponseContent.IndexOf("DDOS-GUARD", StringComparison.OrdinalIgnoreCase) > -1){ + MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 10)); } else{ MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {response.ResponseContent.Substring(0, response.ResponseContent.Length < 200 ? response.ResponseContent.Length : 200)}", ToastType.Error, 10)); + await Console.Error.WriteLineAsync("Full Response: " + response.ResponseContent); } } @@ -191,7 +198,7 @@ public class CrAuth{ return; } - string uuid = Guid.NewGuid().ToString(); + string uuid = string.IsNullOrEmpty(crunInstance.Token.device_id) ? Guid.NewGuid().ToString() : crunInstance.Token.device_id; var formData = new Dictionary{ { "refresh_token", crunInstance.Token.refresh_token }, @@ -252,7 +259,7 @@ public class CrAuth{ return; } - string uuid = Guid.NewGuid().ToString(); + string uuid = string.IsNullOrEmpty(crunInstance.Token?.device_id) ? Guid.NewGuid().ToString() : crunInstance.Token.device_id; var formData = new Dictionary{ { "refresh_token", crunInstance.Token?.refresh_token ?? "" }, diff --git a/CRD/Downloader/Crunchyroll/CrMovies.cs b/CRD/Downloader/Crunchyroll/CrMovies.cs index 60bda91..8db5912 100644 --- a/CRD/Downloader/Crunchyroll/CrMovies.cs +++ b/CRD/Downloader/Crunchyroll/CrMovies.cs @@ -25,7 +25,7 @@ public class CrMovies{ } - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/movies/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/objects/{id}", HttpMethod.Get, true, true, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index c1f89fb..1015eeb 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -454,7 +454,7 @@ public class CrSeries{ query["q"] = searchString; query["n"] = "6"; - query["type"] = "top_results"; + query["type"] = "series"; var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Search}", HttpMethod.Get, true, false, query); @@ -525,4 +525,29 @@ public class CrSeries{ return complete; } + + public async Task GetSeasonalSeries(string season, string year, string? crLocale){ + NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); + + if (!string.IsNullOrEmpty(crLocale)){ + query["locale"] = crLocale; + } + + query["seasonal_tag"] = season.ToLower() + "-" + year; + query["n"] = "100"; + + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query); + + var response = await HttpClientReq.Instance.SendHttpRequest(request); + + if (!response.IsOk){ + Console.Error.WriteLine("Series Request Failed"); + return null; + } + + CrBrowseSeriesBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); + + return series; + } + } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index cb6ce76..cad76bd 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -126,7 +126,7 @@ public class CrunchyrollManager{ options.CustomCalendar = true; options.DlVideoOnce = true; options.StreamEndpoint = "web/firefox"; - options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; + options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd; options.HistoryLang = DefaultLocale; options.BackgroundImageOpacity = 0.5; @@ -177,7 +177,7 @@ public class CrunchyrollManager{ optionsYaml.CustomCalendar = true; optionsYaml.DlVideoOnce = true; optionsYaml.StreamEndpoint = "web/firefox"; - optionsYaml.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; + optionsYaml.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd; optionsYaml.HistoryLang = DefaultLocale; optionsYaml.BackgroundImageOpacity = 0.5; @@ -230,6 +230,29 @@ public class CrunchyrollManager{ } } + public static async Task GetBase64EncodedTokenAsync(){ + string url = "https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js"; + + try{ + string jsContent = await HttpClientReq.Instance.GetHttpClient().GetStringAsync(url); + + Match match = Regex.Match(jsContent, @"prod=""([\w-]+:[\w-]+)"""); + + if (!match.Success) + throw new Exception("Token not found in JS file."); + + string token = match.Groups[1].Value; + + byte[] tokenBytes = Encoding.UTF8.GetBytes(token); + string base64Token = Convert.ToBase64String(tokenBytes); + + return base64Token; + } catch (Exception ex){ + Console.Error.WriteLine($"Auth Token Fetch Error: {ex.Message}"); + return ""; + } + } + public async Task Init(){ if (CrunOptions.LogMode){ CfgManager.EnableLogMode(); @@ -237,6 +260,12 @@ public class CrunchyrollManager{ CfgManager.DisableLogMode(); } + var token = await GetBase64EncodedTokenAsync(); + + if (!string.IsNullOrEmpty(token)){ + ApiUrls.authBasicMob = "Basic " + token; + } + var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") :[]; foreach (var file in jsonFiles){ @@ -331,6 +360,7 @@ public class CrunchyrollManager{ if (options.SkipMuxing == false){ bool syncError = false; + bool muxError = false; data.DownloadProgress = new DownloadProgress(){ IsDownloading = true, @@ -342,6 +372,10 @@ public class CrunchyrollManager{ 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); @@ -357,6 +391,7 @@ public class CrunchyrollManager{ SkipSubMux = options.SkipSubsMux, Output = fileNameAndPath, Mp4 = options.Mp4, + MuxFonts = options.MuxFonts, VideoTitle = res.VideoTitle, Novids = options.Novids, NoCleanup = options.Nocleanup, @@ -380,6 +415,10 @@ public class CrunchyrollManager{ mergers.Add(result.merger); } + if (!result.isMuxed){ + muxError = true; + } + if (result.syncError){ syncError = true; } @@ -417,6 +456,7 @@ public class CrunchyrollManager{ SkipSubMux = options.SkipSubsMux, Output = fileNameAndPath, Mp4 = options.Mp4, + MuxFonts = options.MuxFonts, VideoTitle = res.VideoTitle, Novids = options.Novids, NoCleanup = options.Nocleanup, @@ -437,12 +477,13 @@ public class CrunchyrollManager{ fileNameAndPath); syncError = result.syncError; + muxError = !result.isMuxed; if (result is{ merger: not null, isMuxed: true }){ result.merger.CleanUp(); } - if (options.IsEncodeEnabled){ + if (options.IsEncodeEnabled && !muxError){ data.DownloadProgress = new DownloadProgress(){ IsDownloading = true, Percent = 100, @@ -469,10 +510,10 @@ public class CrunchyrollManager{ Percent = 100, Time = 0, DownloadSpeed = 0, - Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "") + Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? " - Couldn't sync dubs" : "") }; - if (options.RemoveFinishedDownload && !syncError){ + if (CrunOptions.RemoveFinishedDownload && !syncError){ QueueManager.Instance.Queue.Remove(data); } } else{ @@ -498,7 +539,7 @@ public class CrunchyrollManager{ Doing = "Done - Skipped muxing" }; - if (options.RemoveFinishedDownload){ + if (CrunOptions.RemoveFinishedDownload){ QueueManager.Instance.Queue.Remove(data); } } @@ -515,6 +556,18 @@ public class CrunchyrollManager{ _ = CrEpisode.MarkAsWatched(data.Data.First().MediaId); } + if (QueueManager.Instance.Queue.Count == 0){ + try{ + var audioPath = CrunOptions.DownloadFinishedSoundPath; + if (!string.IsNullOrEmpty(audioPath)){ + var player = new AudioPlayer(); + player.Play(audioPath); + } + } catch (Exception exception){ + Console.Error.WriteLine("Failed to play sound: " + exception); + } + } + return true; } @@ -630,7 +683,7 @@ public class CrunchyrollManager{ bool muxDesc = false; if (options.MuxDescription){ - var descriptionPath = data.Where(a => a.Type == DownloadMediaType.Description).First().Path; + var descriptionPath = data.First(a => a.Type == DownloadMediaType.Description).Path; if (File.Exists(descriptionPath)){ muxDesc = true; } else{ @@ -649,7 +702,7 @@ public class CrunchyrollManager{ 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(), KeepAllVideos = options.KeepAllVideos, - Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), + Fonts = options.MuxFonts ? FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList) :[], Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), VideoTitle = options.VideoTitle, Options = new MuxOptions(){ @@ -714,11 +767,9 @@ public class CrunchyrollManager{ } if (!options.Mp4 && !muxToMp3){ - await merger.Merge("mkvmerge", CfgManager.PathMKVMERGE); - isMuxed = true; + isMuxed = await merger.Merge("mkvmerge", CfgManager.PathMKVMERGE); } else{ - await merger.Merge("ffmpeg", CfgManager.PathFFMPEG); - isMuxed = true; + isMuxed = await merger.Merge("ffmpeg", CfgManager.PathFFMPEG); } return (merger, isMuxed, syncError); @@ -916,21 +967,43 @@ public class CrunchyrollManager{ var fetchPlaybackData = await FetchPlaybackData(options, mediaId, mediaGuid, data.Music); if (!fetchPlaybackData.IsOk){ - if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){ - var s = fetchPlaybackData.error; - var error = StreamError.FromJson(s); - if (error != null && error.IsTooManyActiveStreamsError()){ + var errorJson = fetchPlaybackData.error; + if (!string.IsNullOrEmpty(errorJson)){ + var error = StreamError.FromJson(errorJson); + + if (error?.IsTooManyActiveStreamsError() == true){ 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\nClose open cruchyroll tabs in your browser" + ErrorText = "Too many active streams that couldn't be stopped\nClose open Crunchyroll tabs in your browser" + }; + } + + if (error?.Error.Contains("Account maturity rating is lower than video rating") == true || + errorJson.Contains("Account maturity rating is lower than video rating")){ + MainWindow.Instance.ShowError("Account maturity rating is lower than video rating\nChange it in the Crunchyroll account settings"); + return new DownloadResponse{ + Data = new List(), + Error = true, + FileName = "./unknown", + ErrorText = "Account maturity rating is lower than video rating" + }; + } + + if (!string.IsNullOrEmpty(error?.Error)){ + MainWindow.Instance.ShowError($"Couldn't get Playback Data\n{error.Error}"); + return new DownloadResponse{ + Data = new List(), + Error = true, + FileName = "./unknown", + ErrorText = "Playback data not found" }; } } - MainWindow.Instance.ShowError("Couldn't get Playback Data\nTry again later or else check logs and crunchyroll"); + MainWindow.Instance.ShowError("Couldn't get Playback Data\nTry again later or else check logs and Crunchyroll"); return new DownloadResponse{ Data = new List(), Error = true, @@ -939,6 +1012,7 @@ public class CrunchyrollManager{ }; } + var pbData = fetchPlaybackData.pbData; List hsLangs = new List(); @@ -1815,7 +1889,11 @@ public class CrunchyrollManager{ } if (data.DownloadSubs.Contains("all") || data.DownloadSubs.Contains(langItem.CrLocale)){ - var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url ?? string.Empty, HttpMethod.Get, false, false, null); + if (string.IsNullOrEmpty(subsItem.url)){ + continue; + } + + var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url, HttpMethod.Get, false, false, null); var subsAssReqResponse = await HttpClientReq.Instance.SendHttpRequest(subsAssReq); @@ -2025,7 +2103,7 @@ public class CrunchyrollManager{ Data = new Dictionary() }; - var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play"; + var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play"; var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); if (!playbackRequestResponse.IsOk){ @@ -2036,7 +2114,7 @@ public class CrunchyrollManager{ temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId); } else{ Console.WriteLine("Request Stream URLs FAILED! Attempting fallback"); - playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play"; + playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play"; playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); if (!playbackRequestResponse.IsOk){ @@ -2185,13 +2263,11 @@ public class CrunchyrollManager{ foreach (CrunchyChapter chapter in chapterData.Chapters){ if (chapter.start == null || chapter.end == null) continue; - DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + TimeSpan startTime = TimeSpan.FromSeconds(chapter.start.Value); + TimeSpan endTime = TimeSpan.FromSeconds(chapter.end.Value); - DateTime startTime = epoch.AddSeconds(chapter.start.Value); - DateTime endTime = epoch.AddSeconds(chapter.end.Value); - - string startFormatted = startTime.ToString("HH:mm:ss") + ".00"; - string endFormatted = endTime.ToString("HH:mm:ss") + ".00"; + string startFormatted = startTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); + string endFormatted = endTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); int chapterNumber = (compiledChapters.Count / 2) + 1; if (chapter.type == "intro"){ @@ -2227,19 +2303,12 @@ public class CrunchyrollManager{ if (showRequestResponse.IsOk){ CrunchyOldChapter chapterData = Helpers.Deserialize(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new CrunchyOldChapter(); - DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + TimeSpan startTime = TimeSpan.FromSeconds(chapterData.startTime); + TimeSpan endTime = TimeSpan.FromSeconds(chapterData.endTime); - DateTime startTime = epoch.AddSeconds(chapterData.startTime); - DateTime endTime = epoch.AddSeconds(chapterData.endTime); + string startFormatted = startTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); + string endFormatted = endTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); - string[] startTimeParts = startTime.ToString(CultureInfo.CurrentCulture).Split('.'); - string[] endTimeParts = endTime.ToString(CultureInfo.CurrentCulture).Split('.'); - - string startMs = startTimeParts.Length > 1 ? startTimeParts[1] : "00"; - string endMs = endTimeParts.Length > 1 ? endTimeParts[1] : "00"; - - string startFormatted = startTime.ToString("HH:mm:ss") + "." + startMs; - string endFormatted = endTime.ToString("HH:mm:ss") + "." + endMs; int chapterNumber = (compiledChapters.Count / 2) + 1; if (chapterData.startTime > 1){ diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index 6db931d..9147ebb 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -58,6 +58,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _muxToMp4; + + [ObservableProperty] + private bool _muxFonts; [ObservableProperty] private bool _syncTimings; @@ -310,6 +313,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ KeepDubsSeparate = options.KeepDubsSeperate; DownloadChapters = options.Chapters; MuxToMp4 = options.Mp4; + MuxFonts = options.MuxFonts; SyncTimings = options.SyncTiming; SkipSubMux = options.SkipSubsMux; LeadingNumbers = options.Numbers; @@ -375,6 +379,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters; CrunchyrollManager.Instance.CrunOptions.SkipMuxing = SkipMuxing; CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4; + CrunchyrollManager.Instance.CrunOptions.MuxFonts = MuxFonts; CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings; CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux; CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10); diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml index b5edb2b..0de9aa5 100644 --- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -377,6 +377,12 @@ + + + + + + diff --git a/CRD/Downloader/ProgramManager.cs b/CRD/Downloader/ProgramManager.cs index 40cfdca..f49232a 100644 --- a/CRD/Downloader/ProgramManager.cs +++ b/CRD/Downloader/ProgramManager.cs @@ -49,9 +49,12 @@ public partial class ProgramManager : ObservableObject{ [ObservableProperty] private bool _updateAvailable = true; + [ObservableProperty] + private double _opacityButton = 0.4; + [ObservableProperty] private bool _finishedLoading; - + [ObservableProperty] private bool _navigationLock; @@ -122,6 +125,8 @@ public partial class ProgramManager : ObservableObject{ UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync(); + OpacityButton = UpdateAvailable ? 1.0 : 0.4; + if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){ if (_faTheme != null) _faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor); } @@ -176,9 +181,8 @@ public partial class ProgramManager : ObservableObject{ private void CleanUpOldUpdater(){ - var executableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; - + string backupFilePath = Path.Combine(Directory.GetCurrentDirectory(), $"Updater{executableExtension}.bak"); if (File.Exists(backupFilePath)){ diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs index f43bb6e..2b48fcd 100644 --- a/CRD/Downloader/QueueManager.cs +++ b/CRD/Downloader/QueueManager.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.CustomList; @@ -16,7 +17,7 @@ using ReactiveUI; namespace CRD.Downloader; -public class QueueManager{ +public partial class QueueManager : ObservableObject{ #region Download Variables public RefreshableObservableCollection Queue = new RefreshableObservableCollection(); @@ -24,7 +25,9 @@ public class QueueManager{ public int ActiveDownloads; #endregion - + + [ObservableProperty] + private bool _hasFailedItem; #region Singelton @@ -87,8 +90,11 @@ public class QueueManager{ downloadItem.StartDownload(); } } - } + HasFailedItem = Queue.Any(item => item.DownloadProgress.Error); + + } + public async Task CrAddEpisodeToQueue(string epId, string crLocale, List dubLang, bool updateHistory = false, bool onlySubs = false){ if (string.IsNullOrEmpty(epId)){ @@ -230,7 +236,9 @@ public class QueueManager{ newOptions.DubLang = dubLang; movieMeta.DownloadSettings = newOptions; - + + movieMeta.VideoQuality = CrunchyrollManager.Instance.CrunOptions.QualityVideo; + Queue.Add(movieMeta); Console.WriteLine("Added Movie to Queue"); diff --git a/CRD/Utils/AudioPlayer.cs b/CRD/Utils/AudioPlayer.cs new file mode 100644 index 0000000..2554e3c --- /dev/null +++ b/CRD/Utils/AudioPlayer.cs @@ -0,0 +1,29 @@ +using System; +using NetCoreAudio; + +namespace CRD.Utils; + +public class AudioPlayer{ + private readonly Player _player; + private bool _isPlaying = false; + + public AudioPlayer(){ + _player = new Player(); + } + + public async void Play(string path){ + if (_isPlaying){ + Console.WriteLine("Audio is already playing, ignoring duplicate request."); + return; + } + + _isPlaying = true; + await _player.Play(path); + _isPlaying = false; + } + + public async void Stop(){ + await _player.Stop(); + _isPlaying = false; + } +} \ No newline at end of file diff --git a/CRD/Utils/DRM/Widevine.cs b/CRD/Utils/DRM/Widevine.cs index cdec1a8..2e472b4 100644 --- a/CRD/Utils/DRM/Widevine.cs +++ b/CRD/Utils/DRM/Widevine.cs @@ -39,41 +39,57 @@ public class Widevine{ public Widevine(){ try{ if (Directory.Exists(CfgManager.PathWIDEVINE_DIR)){ - var files = Directory.GetFiles(CfgManager.PathWIDEVINE_DIR); - - foreach (var file in files){ + foreach (var file in Directory.EnumerateFiles(CfgManager.PathWIDEVINE_DIR)){ var fileInfo = new FileInfo(file); - if (fileInfo.Length < 1024 * 8 && !fileInfo.Attributes.HasFlag(FileAttributes.Directory)){ - string fileContents = File.ReadAllText(file, Encoding.UTF8); - if (fileContents.Contains("-BEGIN RSA PRIVATE KEY-") || fileContents.Contains("-BEGIN PRIVATE KEY-")){ - privateKey = File.ReadAllBytes(file); - } + + if (fileInfo.Length >= 1024 * 8 || fileInfo.Attributes.HasFlag(FileAttributes.Directory)) + continue; + + string fileContents = File.ReadAllText(file, Encoding.UTF8); - if (fileContents.Contains("widevine_cdm_version")){ - identifierBlob = File.ReadAllBytes(file); - } + if (IsPrivateKey(fileContents)){ + privateKey = File.ReadAllBytes(file); + } else if (IsWidevineIdentifierBlob(fileContents)){ + identifierBlob = File.ReadAllBytes(file); } } } - - if (privateKey.Length != 0 && identifierBlob.Length != 0){ + if (privateKey?.Length > 0 && identifierBlob?.Length > 0){ canDecrypt = true; - } else if (privateKey.Length == 0){ - Console.Error.WriteLine("Private key missing"); - canDecrypt = false; - } else if (identifierBlob.Length == 0){ - Console.Error.WriteLine("Identifier blob missing"); + } else{ canDecrypt = false; + if (privateKey == null || privateKey.Length == 0){ + Console.Error.WriteLine("Private key missing"); + } + + if (identifierBlob == null || identifierBlob.Length == 0){ + Console.Error.WriteLine("Identifier blob missing"); + } } - } catch (Exception e){ - Console.Error.WriteLine("Widevine: " + e); + } catch (IOException ioEx){ + Console.Error.WriteLine("I/O error accessing Widevine files: " + ioEx); + canDecrypt = false; + } catch (UnauthorizedAccessException uaEx){ + Console.Error.WriteLine("Permission error accessing Widevine files: " + uaEx); + canDecrypt = false; + } catch (Exception ex){ + Console.Error.WriteLine("Unexpected Widevine error: " + ex); canDecrypt = false; } Console.WriteLine($"CDM available: {canDecrypt}"); } + private bool IsPrivateKey(string content){ + return content.Contains("-BEGIN RSA PRIVATE KEY-", StringComparison.Ordinal) || + content.Contains("-BEGIN PRIVATE KEY-", StringComparison.Ordinal); + } + + private bool IsWidevineIdentifierBlob(string content){ + return content.Contains("widevine_cdm_version", StringComparison.Ordinal); + } + public async Task> getKeys(string? pssh, string licenseServer, Dictionary authData){ if (pssh == null || !canDecrypt){ Console.Error.WriteLine("Missing pssh or cdm files"); diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index 6afbd55..79450d5 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -15,36 +15,36 @@ using YamlDotNet.Serialization.NamingConventions; namespace CRD.Utils.Files; public class CfgManager{ - private static string WorkingDirectory = AppContext.BaseDirectory; + private static string workingDirectory = AppContext.BaseDirectory; - public static readonly string PathCrTokenOld = Path.Combine(WorkingDirectory, "config", "cr_token.yml"); - public static readonly string PathCrDownloadOptionsOld = Path.Combine(WorkingDirectory, "config", "settings.yml"); + public static readonly string PathCrTokenOld = Path.Combine(workingDirectory, "config", "cr_token.yml"); + public static readonly string PathCrDownloadOptionsOld = Path.Combine(workingDirectory, "config", "settings.yml"); - public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.json"); - public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.json"); + public static readonly string PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json"); + public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json"); - public static readonly string PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json"); - public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json"); + public static readonly string PathCrHistory = Path.Combine(workingDirectory, "config", "history.json"); + public static readonly string PathWindowSettings = Path.Combine(workingDirectory, "config", "windowSettings.json"); private static readonly string ExecutableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; - public static readonly string PathFFMPEG = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(WorkingDirectory, "lib", "ffmpeg.exe") : - File.Exists(Path.Combine(WorkingDirectory, "lib", "ffmpeg")) ? Path.Combine(WorkingDirectory, "lib", "ffmpeg") : "ffmpeg"; + public static readonly string PathFFMPEG = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(workingDirectory, "lib", "ffmpeg.exe") : + File.Exists(Path.Combine(workingDirectory, "lib", "ffmpeg")) ? Path.Combine(workingDirectory, "lib", "ffmpeg") : "ffmpeg"; - public static readonly string PathMKVMERGE = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(WorkingDirectory, "lib", "mkvmerge.exe") : - File.Exists(Path.Combine(WorkingDirectory, "lib", "mkvmerge")) ? Path.Combine(WorkingDirectory, "lib", "mkvmerge") : "mkvmerge"; + public static readonly string PathMKVMERGE = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(workingDirectory, "lib", "mkvmerge.exe") : + File.Exists(Path.Combine(workingDirectory, "lib", "mkvmerge")) ? Path.Combine(workingDirectory, "lib", "mkvmerge") : "mkvmerge"; - public static readonly string PathMP4Decrypt = Path.Combine(WorkingDirectory, "lib", "mp4decrypt" + ExecutableExtension); - public static readonly string PathShakaPackager = Path.Combine(WorkingDirectory, "lib", "shaka-packager" + ExecutableExtension); + public static readonly string PathMP4Decrypt = Path.Combine(workingDirectory, "lib", "mp4decrypt" + ExecutableExtension); + public static readonly string PathShakaPackager = Path.Combine(workingDirectory, "lib", "shaka-packager" + ExecutableExtension); - public static readonly string PathWIDEVINE_DIR = Path.Combine(WorkingDirectory, "widevine"); + public static readonly string PathWIDEVINE_DIR = Path.Combine(workingDirectory, "widevine"); - public static readonly string PathVIDEOS_DIR = Path.Combine(WorkingDirectory, "video"); - public static readonly string PathENCODING_PRESETS_DIR = Path.Combine(WorkingDirectory, "presets"); - public static readonly string PathTEMP_DIR = Path.Combine(WorkingDirectory, "temp"); - public static readonly string PathFONTS_DIR = Path.Combine(WorkingDirectory, "fonts"); + public static readonly string PathVIDEOS_DIR = Path.Combine(workingDirectory, "video"); + public static readonly string PathENCODING_PRESETS_DIR = Path.Combine(workingDirectory, "presets"); + public static readonly string PathTEMP_DIR = Path.Combine(workingDirectory, "temp"); + public static readonly string PathFONTS_DIR = Path.Combine(workingDirectory, "fonts"); - public static readonly string PathLogFile = Path.Combine(WorkingDirectory, "logfile.txt"); + public static readonly string PathLogFile = Path.Combine(workingDirectory, "logfile.txt"); private static StreamWriter logFile; private static bool isLogModeEnabled = false; @@ -290,7 +290,7 @@ public class CfgManager{ } lock (fileLock){ - using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write)) + using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write, FileShare.None)) using (var streamWriter = new StreamWriter(fileStream)) using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){ var serializer = new JsonSerializer(); diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index 6525819..825fd30 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -453,6 +453,8 @@ public class HlsDownloader{ Console.WriteLine($"\tError: {ex.Message}"); if (attempt == retryCount) throw; // rethrow after last retry + + await Task.Delay(_data.WaitTime); } } } diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 8929177..0af9f87 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -237,7 +237,11 @@ public class Helpers{ process.OutputDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)){ - Console.WriteLine(e.Data); + if (e.Data.StartsWith("Error:")){ + Console.Error.WriteLine(e.Data); + } else{ + Console.WriteLine(e.Data); + } } }; diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index 7bb4d10..c7fb370 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -216,6 +216,11 @@ public class HttpClientReq{ public static HttpRequestMessage CreateRequestMessage(string uri, HttpMethod requestMethod, bool authHeader, bool disableDrmHeader, NameValueCollection? query){ + if (string.IsNullOrEmpty(uri)){ + Console.Error.WriteLine($" Request URI is empty"); + return new HttpRequestMessage(HttpMethod.Get, "about:blank"); + } + UriBuilder uriBuilder = new UriBuilder(uri); if (query != null){ @@ -263,12 +268,8 @@ public static class ApiUrls{ public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse"; public static readonly string BetaCms = ApiBeta + "/cms/v2"; public static readonly string DRM = ApiBeta + "/drm/v1/auth"; + + public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE="; - public static readonly string authBasic = "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6"; - - public static readonly string authBasicMob = "Basic ZG1yeWZlc2NkYm90dWJldW56NXo6NU45aThPV2cyVmtNcm1oekNfNUNXekRLOG55SXo0QU0="; - public static readonly string authBasicSwitch = "Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4="; - - public static readonly string ChromeUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"; - public static readonly string MobileUserAgent = "Crunchyroll/3.74.2 Android/14 okhttp/4.12.0"; + public static readonly string MobileUserAgent = "Crunchyroll/3.78.3 Android/15 okhttp/4.12.0"; } \ No newline at end of file diff --git a/CRD/Utils/Muxing/FontsManager.cs b/CRD/Utils/Muxing/FontsManager.cs index 9bc67aa..7ae82b8 100644 --- a/CRD/Utils/Muxing/FontsManager.cs +++ b/CRD/Utils/Muxing/FontsManager.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using CRD.Utils.Files; using CRD.Utils.Structs; +using CRD.Views; namespace CRD.Utils.Muxing; @@ -65,7 +66,7 @@ public class FontsManager{ var fontLoc = Path.Combine(CfgManager.PathFONTS_DIR, font); if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length != 0){ - Console.WriteLine($"{font} already downloaded!"); + // Console.WriteLine($"{font} already downloaded!"); } else{ var fontFolder = Path.GetDirectoryName(fontLoc); if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length == 0){ @@ -82,19 +83,18 @@ public class FontsManager{ var fontUrl = root + font; - using (var httpClient = HttpClientReq.Instance.GetHttpClient()){ - try{ - var response = await httpClient.GetAsync(fontUrl); - if (response.IsSuccessStatusCode){ - var fontData = await response.Content.ReadAsByteArrayAsync(); - await File.WriteAllBytesAsync(fontLoc, fontData); - Console.WriteLine($"Downloaded: {font}"); - } else{ - Console.Error.WriteLine($"Failed to download: {font}"); - } - } catch (Exception e){ - Console.Error.WriteLine($"Error downloading {font}: {e.Message}"); + var httpClient = HttpClientReq.Instance.GetHttpClient(); + try{ + var response = await httpClient.GetAsync(fontUrl); + if (response.IsSuccessStatusCode){ + var fontData = await response.Content.ReadAsByteArrayAsync(); + await File.WriteAllBytesAsync(fontLoc, fontData); + Console.WriteLine($"Downloaded: {font}"); + } else{ + Console.Error.WriteLine($"Failed to download: {font}"); } + } catch (Exception e){ + Console.Error.WriteLine($"Error downloading {font}: {e.Message}"); } } } @@ -171,6 +171,8 @@ public class FontsManager{ Console.WriteLine((isNstr ? "\n" : "") + "Required fonts: {0} (Total: {1})", string.Join(", ", fontsNameList), fontsNameList.Count); } + List missingFonts = new List(); + foreach (var f in fontsNameList){ if (Fonts.TryGetValue(f.Key, out var fontFiles)){ foreach (var fontFile in fontFiles){ @@ -180,9 +182,15 @@ public class FontsManager{ fontsList.Add(new ParsedFont{ Name = fontFile, Path = fontPath, Mime = mime }); } } + } else{ + missingFonts.Add(f.Key); } } + if (missingFonts.Count > 0){ + MainWindow.Instance.ShowError($"Missing Fonts: \n{string.Join(", ", fontsNameList)}"); + } + return fontsList; } } diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index e24099f..b905dea 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -385,7 +385,7 @@ public class Merger{ } - public async Task Merge(string type, string bin){ + public async Task Merge(string type, string bin){ string command = type switch{ "ffmpeg" => FFmpeg(), "mkvmerge" => MkvMerge(), @@ -394,7 +394,7 @@ public class Merger{ if (string.IsNullOrEmpty(command)){ Console.Error.WriteLine("Unable to merge files."); - return; + return false; } Console.WriteLine($"[{type}] Started merging"); @@ -404,9 +404,12 @@ public class Merger{ Console.WriteLine($"[{type}] Mkvmerge finished with at least one warning"); } else if (!result.IsOk){ Console.Error.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}"); + return false; } else{ Console.WriteLine($"[{type} Done]"); } + + return true; } @@ -459,6 +462,7 @@ public class CrunchyMuxOptions{ public bool? KeepAllVideos{ get; set; } public bool? Novids{ get; set; } public bool Mp4{ get; set; } + public bool MuxFonts{ get; set; } public bool MuxDescription{ get; set; } public string ForceMuxer{ get; set; } public bool? NoCleanup{ get; set; } diff --git a/CRD/Utils/Structs/AnilistUpcoming.cs b/CRD/Utils/Structs/AnilistUpcoming.cs index 0b45f54..b766720 100644 --- a/CRD/Utils/Structs/AnilistUpcoming.cs +++ b/CRD/Utils/Structs/AnilistUpcoming.cs @@ -64,6 +64,16 @@ public partial class AnilistSeries : ObservableObject{ [JsonIgnore] [ObservableProperty] public bool _isInHistory; + + [ObservableProperty] + public bool _isExpanded; + + [JsonIgnore] + public List AudioLocales{ get; set; } =[]; + + [JsonIgnore] + public List SubtitleLocales{ get; set; } =[]; + } public class Title{ diff --git a/CRD/Utils/Structs/CalendarStructs.cs b/CRD/Utils/Structs/CalendarStructs.cs index a8aa643..3ce2995 100644 --- a/CRD/Utils/Structs/CalendarStructs.cs +++ b/CRD/Utils/Structs/CalendarStructs.cs @@ -47,22 +47,23 @@ public partial class CalendarEpisode : INotifyPropertyChanged{ public event PropertyChangedEventHandler? PropertyChanged; [RelayCommand] - public void AddEpisodeToQue(){ - if (CalendarEpisodes.Count > 0){ - foreach (var calendarEpisode in CalendarEpisodes){ - calendarEpisode.AddEpisodeToQue(); - } - } - + public async Task AddEpisodeToQue(){ if (EpisodeUrl != null){ var match = Regex.Match(EpisodeUrl, "/([^/]+)/watch/([^/]+)"); if (match.Success){ var locale = match.Groups[1].Value; // Capture the locale part var id = match.Groups[2].Value; // Capture the ID part - QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true); + await QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true); } } + + if (CalendarEpisodes.Count > 0){ + foreach (var calendarEpisode in CalendarEpisodes){ + calendarEpisode.AddEpisodeToQue(); + } + } + } public async Task LoadImage(int width = 0, int height = 0){ diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index 9a1bdbd..47b6dc1 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -36,6 +36,13 @@ public class CrDownloadOptions{ [JsonProperty("background_image_path")] public string? BackgroundImagePath{ get; set; } + [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; } @@ -180,6 +187,9 @@ public class CrDownloadOptions{ [JsonProperty("mux_mp4")] public bool Mp4{ get; set; } + + [JsonProperty("mux_fonts")] + public bool MuxFonts{ get; set; } [JsonProperty("mux_video_title")] public string? VideoTitle{ get; set; } @@ -369,8 +379,7 @@ public class CrDownloadOptionsYaml{ public string? ProxyPassword{ get; set; } #endregion - - + #region Crunchyroll Settings [YamlIgnore] diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs index 121089b..cf2a99e 100644 --- a/CRD/Utils/Structs/HelperClasses.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; +using CRD.Utils.Structs.History; using CRD.Views; using Newtonsoft.Json; @@ -73,7 +74,7 @@ public class DownloadResponse{ public string? FolderPath{ get; set; } public string? TempFolderPath{ get; set; } - + public string VideoTitle{ get; set; } public bool Error{ get; set; } public string ErrorText{ get; set; } @@ -107,14 +108,13 @@ public class StringItem{ public string stringValue{ get; set; } } -public class WindowSettings -{ - public double Width { get; set; } - public double Height { get; set; } - public int ScreenIndex { get; set; } - public int PosX { get; set; } - public int PosY { get; set; } - public bool IsMaximized { get; set; } +public class WindowSettings{ + public double Width{ get; set; } + public double Height{ get; set; } + public int ScreenIndex{ get; set; } + public int PosX{ get; set; } + public int PosY{ get; set; } + public bool IsMaximized{ get; set; } } public class ToastMessage(string message, ToastType type, int i){ @@ -143,4 +143,14 @@ public partial class SeasonViewModel : ObservableObject{ public int Year{ get; set; } public string Display => $"{Season}\n{Year}"; +} + +public class SeasonDialogArgs{ + public HistorySeries? Series{ get; set; } + public HistorySeason? Season{ get; set; } + + public SeasonDialogArgs(HistorySeries? series, HistorySeason? season){ + Series = series; + Season = season; + } } \ No newline at end of file diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs index d073ea6..7b98b57 100644 --- a/CRD/Utils/Structs/History/HistorySeries.cs +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; +using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Media.Imaging; @@ -99,6 +100,12 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonIgnore] private bool _editModeEnabled; + [JsonIgnore] + public string SeriesFolderPath{ get; set; } + + [JsonIgnore] + public bool SeriesFolderPathExists{ get; set; } + #region Settings Override [JsonIgnore] @@ -213,6 +220,9 @@ public class HistorySeries : INotifyPropertyChanged{ SelectedSubLang.CollectionChanged += Changes; SelectedDubLang.CollectionChanged += Changes; + + UpdateSeriesFolderPath(); + Loading = false; } @@ -516,4 +526,59 @@ public class HistorySeries : INotifyPropertyChanged{ break; } } + + public void UpdateSeriesFolderPath(){ + var season = Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath)); + + if (!string.IsNullOrEmpty(SeriesDownloadPath) && Directory.Exists(SeriesDownloadPath)){ + SeriesFolderPath = SeriesDownloadPath; + SeriesFolderPathExists = true; + } + + if (season is{ SeasonDownloadPath: not null }){ + try{ + var seasonPath = season.SeasonDownloadPath; + var directoryInfo = new DirectoryInfo(seasonPath); + + if (!string.IsNullOrEmpty(directoryInfo.Parent?.FullName)){ + string parentFolderPath = directoryInfo.Parent?.FullName ?? string.Empty; + + if (Directory.Exists(parentFolderPath)){ + SeriesFolderPath = parentFolderPath; + SeriesFolderPathExists = true; + } + } + } catch (Exception e){ + Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}"); + } + } else{ + string customPath; + + if (string.IsNullOrEmpty(SeriesTitle)) + return; + + var seriesTitle = FileNameManager.CleanupFilename(SeriesTitle); + + if (string.IsNullOrEmpty(seriesTitle)) + return; + + // Check Crunchyroll download directory + var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath; + if (!string.IsNullOrEmpty(downloadDirPath)){ + customPath = Path.Combine(downloadDirPath, seriesTitle); + } else{ + // Fallback to configured VIDEOS_DIR path + customPath = Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle); + } + + // Check if custom path exists + if (Directory.Exists(customPath)){ + SeriesFolderPath = customPath; + SeriesFolderPathExists = true; + } + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists))); + } + } \ No newline at end of file diff --git a/CRD/Utils/Structs/Languages.cs b/CRD/Utils/Structs/Languages.cs index 347efbc..fa81222 100644 --- a/CRD/Utils/Structs/Languages.cs +++ b/CRD/Utils/Structs/Languages.cs @@ -40,26 +40,29 @@ public class Languages{ }; public static List SortListByLangList(List langList){ - var orderMap = languages.Select((value, index) => new { Value = value.CrLocale, Index = index }) + var orderMap = languages.Select((value, index) => new{ Value = value.CrLocale, Index = index }) .ToDictionary(x => x.Value, x => x.Index); - langList.Sort((x, y) => - { + langList.Sort((x, y) => { bool xExists = orderMap.ContainsKey(x); bool yExists = orderMap.ContainsKey(y); if (xExists && yExists) - return orderMap[x].CompareTo(orderMap[y]); // Sort by main list order + return orderMap[x].CompareTo(orderMap[y]); // Sort by main list order else if (xExists) - return -1; // x comes before any missing value + return -1; // x comes before any missing value else if (yExists) - return 1; // y comes before any missing value + return 1; // y comes before any missing value else - return string.CompareOrdinal(x, y); // Sort alphabetically or by another logic for missing values + return string.CompareOrdinal(x, y); // Sort alphabetically or by another logic for missing values }); return langList; } + public static List LocalListToLangList(List langList){ + return SortListByLangList(langList.Select(seriesMetadataAudioLocale => seriesMetadataAudioLocale.GetEnumMemberValue()).ToList()); + } + public static LanguageItem FixAndFindCrLc(string cr_locale){ if (string.IsNullOrEmpty(cr_locale)){ return new LanguageItem(); @@ -69,14 +72,14 @@ public class Languages{ return FindLang(str); } - public static string SubsFile(string fnOutput, string subsIndex, LanguageItem langItem, bool isCC, string ccTag , bool? isSigns = false, string? format = "ass", bool addIndexAndLangCode = true){ + public static string SubsFile(string fnOutput, string subsIndex, LanguageItem langItem, bool isCC, string ccTag, bool? isSigns = false, string? format = "ass", bool addIndexAndLangCode = true){ subsIndex = (int.Parse(subsIndex) + 1).ToString().PadLeft(2, '0'); string fileName = $"{fnOutput}"; if (addIndexAndLangCode){ - fileName += $".{subsIndex}.{langItem.CrLocale}"; + fileName += $".{langItem.CrLocale}"; //.{subsIndex} } - + //removed .{langItem.language} from file name at end if (isCC){ diff --git a/CRD/Utils/UI/UiSeriesSeasonConverter.cs b/CRD/Utils/UI/UiSeriesSeasonConverter.cs new file mode 100644 index 0000000..83787e6 --- /dev/null +++ b/CRD/Utils/UI/UiSeriesSeasonConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; +using CRD.Utils.Structs; +using CRD.Utils.Structs.History; + +namespace CRD.Utils.UI; + +public class UiSeriesSeasonConverter : IMultiValueConverter{ + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture){ + var series = values.Count > 0 && values[0] is HistorySeries hs ? hs : null; + var season = values.Count > 1 && values[1] is HistorySeason hsn ? hsn : null; + return new SeasonDialogArgs(series, season); + } + + public IList ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/CRD/Utils/Updater/Updater.cs b/CRD/Utils/Updater/Updater.cs index 98c8b21..98d6716 100644 --- a/CRD/Utils/Updater/Updater.cs +++ b/CRD/Utils/Updater/Updater.cs @@ -8,8 +8,10 @@ using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading.Tasks; using CRD.Utils.Files; +using Newtonsoft.Json; namespace CRD.Utils.Updater; @@ -17,6 +19,8 @@ public class Updater : INotifyPropertyChanged{ public double progress = 0; public bool failed = false; + public string latestVersion = ""; + #region Singelton private static Updater? _instance; @@ -47,8 +51,10 @@ public class Updater : INotifyPropertyChanged{ private string downloadUrl = ""; private readonly string tempPath = Path.Combine(CfgManager.PathTEMP_DIR, "Update.zip"); private readonly string extractPath = Path.Combine(CfgManager.PathTEMP_DIR, "ExtractedUpdate"); + private readonly string changelogFilePath = Path.Combine(AppContext.BaseDirectory, "CHANGELOG.md"); - private readonly string apiEndpoint = "https://api.github.com/repos/Crunchy-DL/Crunchy-Downloader/releases/latest"; + private static readonly string apiEndpoint = "https://api.github.com/repos/Crunchy-DL/Crunchy-Downloader/releases"; + private static readonly string apiEndpointLatest = apiEndpoint + "/latest"; public async Task CheckForUpdatesAsync(){ if (File.Exists(tempPath)){ @@ -86,19 +92,25 @@ public class Updater : INotifyPropertyChanged{ handler.UseProxy = false; using (var client = new HttpClient(handler)){ client.DefaultRequestHeaders.Add("User-Agent", "C# App"); - var response = await client.GetStringAsync(apiEndpoint); - var releaseInfo = Helpers.Deserialize(response, null); + var response = await client.GetStringAsync(apiEndpointLatest); + var releaseInfo = Helpers.Deserialize(response, null); - var latestVersion = releaseInfo.tag_name; - - foreach (var asset in releaseInfo.assets){ - string assetName = (string)asset.name; - if (assetName.Contains(platformName)){ - downloadUrl = asset.browser_download_url; - break; - } + if (releaseInfo == null){ + Console.WriteLine($"Failed to get Update info"); + return false; } + latestVersion = releaseInfo.TagName; + + if (releaseInfo.Assets != null) + foreach (var asset in releaseInfo.Assets){ + string assetName = (string)asset.name; + if (assetName.Contains(platformName)){ + downloadUrl = asset.browser_download_url; + break; + } + } + if (string.IsNullOrEmpty(downloadUrl)){ Console.WriteLine($"Failed to get Update url for {platformName}"); return false; @@ -109,10 +121,12 @@ public class Updater : INotifyPropertyChanged{ if (latestVersion != currentVersion){ Console.WriteLine("Update available: " + latestVersion + " - Current Version: " + currentVersion); + _ = UpdateChangelogAsync(); return true; } Console.WriteLine("No updates available."); + _ = UpdateChangelogAsync(); return false; } } catch (Exception e){ @@ -121,6 +135,99 @@ public class Updater : INotifyPropertyChanged{ } } + public async Task UpdateChangelogAsync(){ + var client = HttpClientReq.Instance.GetHttpClient(); + client.DefaultRequestHeaders.Add("User-Agent", "C# App"); + + string existingVersion = GetLatestVersionFromFile(); + + if (string.IsNullOrEmpty(existingVersion)){ + existingVersion = "v1.0.0"; + } + + if (string.IsNullOrEmpty(latestVersion)){ + latestVersion = "v1.0.0"; + } + + if (existingVersion == latestVersion || Version.Parse(existingVersion.TrimStart('v')) >= Version.Parse(latestVersion.TrimStart('v'))){ + Console.WriteLine("CHANGELOG.md is already up to date."); + return; + } + + try{ + string jsonResponse = await client.GetStringAsync(apiEndpoint); // + "?per_page=100&page=1" + + var releases = Helpers.Deserialize>(jsonResponse, null); + + // Filter out pre-releases + if (releases != null){ + releases = releases.Where(r => !r.Prerelease).ToList(); + + if (releases.Count == 0){ + Console.WriteLine("No stable releases found."); + return; + } + + var newReleases = releases.TakeWhile(r => r.TagName != existingVersion).ToList(); + + if (newReleases.Count == 0){ + Console.WriteLine("CHANGELOG.md is already up to date."); + return; + } + + Console.WriteLine($"Adding {newReleases.Count} new releases to CHANGELOG.md..."); + + AppendNewReleasesToChangelog(newReleases); + + Console.WriteLine("CHANGELOG.md updated successfully."); + } + } catch (Exception ex){ + Console.Error.WriteLine($"Error updating changelog: {ex.Message}"); + } + } + + private string GetLatestVersionFromFile(){ + if (!File.Exists(changelogFilePath)) + return string.Empty; + + string[] lines = File.ReadAllLines(changelogFilePath); + foreach (string line in lines){ + Match match = Regex.Match(line, @"## \[(v?\d+\.\d+\.\d+)\]"); + if (match.Success) + return match.Groups[1].Value; + } + + return string.Empty; + } + + private void AppendNewReleasesToChangelog(List newReleases){ + string existingContent = ""; + + if (File.Exists(changelogFilePath)){ + existingContent = File.ReadAllText(changelogFilePath); + } + + string newEntries = ""; + + foreach (var release in newReleases){ + string version = release.TagName; + string date = release.PublishedAt.Split('T')[0]; + string notes = RemoveUnwantedContent(release.Body); + + newEntries += $"## [{version}] - {date}\n\n{notes}\n\n---\n\n"; + } + + + if (string.IsNullOrWhiteSpace(existingContent)){ + File.WriteAllText(changelogFilePath, "# Changelog\n\n" + newEntries); + } else{ + File.WriteAllText(changelogFilePath, "# Changelog\n\n" + newEntries + existingContent.Substring("# Changelog\n\n".Length)); + } + } + + private static string RemoveUnwantedContent(string notes){ + return Regex.Split(notes, @"##\r\n\r\n### Linux/MacOS Builds", RegexOptions.IgnoreCase)[0].Trim(); + } public async Task DownloadAndUpdateAsync(){ try{ @@ -216,4 +323,17 @@ public class Updater : INotifyPropertyChanged{ OnPropertyChanged(nameof(failed)); } } + + public class GithubRelease{ + [JsonProperty("tag_name")] + public string TagName{ get; set; } = string.Empty; + + public dynamic? Assets{ get; set; } + public string Body{ get; set; } = string.Empty; + + [JsonProperty("published_at")] + public string PublishedAt{ get; set; } = string.Empty; + + public bool Prerelease{ get; set; } + } } \ No newline at end of file diff --git a/CRD/ViewModels/DownloadsPageViewModel.cs b/CRD/ViewModels/DownloadsPageViewModel.cs index bb1d814..eda179d 100644 --- a/CRD/ViewModels/DownloadsPageViewModel.cs +++ b/CRD/ViewModels/DownloadsPageViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; @@ -10,6 +11,7 @@ using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils; +using CRD.Utils.CustomList; using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll; @@ -24,18 +26,23 @@ public partial class DownloadsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _removeFinished; - + + [ObservableProperty] + private QueueManager _queueManagerIns; + public DownloadsPageViewModel(){ - QueueManager.Instance.UpdateDownloadListItems(); - Items = QueueManager.Instance.DownloadItemModels; + QueueManagerIns = QueueManager.Instance; + QueueManagerIns.UpdateDownloadListItems(); + Items = QueueManagerIns.DownloadItemModels; AutoDownload = CrunchyrollManager.Instance.CrunOptions.AutoDownload; RemoveFinished = CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload; } + partial void OnAutoDownloadChanged(bool value){ CrunchyrollManager.Instance.CrunOptions.AutoDownload = value; if (value){ - QueueManager.Instance.UpdateDownloadListItems(); + QueueManagerIns.UpdateDownloadListItems(); } CfgManager.WriteCrSettings(); @@ -48,8 +55,8 @@ public partial class DownloadsPageViewModel : ViewModelBase{ [RelayCommand] public void ClearQueue(){ - var items = QueueManager.Instance.Queue; - QueueManager.Instance.Queue.Clear(); + var items = QueueManagerIns.Queue; + QueueManagerIns.Queue.Clear(); foreach (var crunchyEpMeta in items){ if (!crunchyEpMeta.DownloadProgress.Done){ @@ -65,6 +72,20 @@ public partial class DownloadsPageViewModel : ViewModelBase{ } } } + + [RelayCommand] + public void RetryQueue(){ + var items = QueueManagerIns.Queue; + + foreach (var crunchyEpMeta in items){ + if (crunchyEpMeta.DownloadProgress.Error){ + crunchyEpMeta.DownloadProgress = new(); + } + } + + QueueManagerIns.UpdateDownloadListItems(); + } + } public partial class DownloadItemModel : INotifyPropertyChanged{ diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs index 3e3c080..bffc40b 100644 --- a/CRD/ViewModels/HistoryPageViewModel.cs +++ b/CRD/ViewModels/HistoryPageViewModel.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Avalonia; @@ -339,7 +340,6 @@ public partial class HistoryPageViewModel : ViewModelBase{ } SelectedSeries = null; - } [RelayCommand] @@ -356,7 +356,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ [RelayCommand] public void NavToSeries(){ - if (ProgramManager.FetchingData){ + if (ProgramManager.FetchingData && SelectedSeries is{ FetchingData: true }){ return; } @@ -461,8 +461,9 @@ public partial class HistoryPageViewModel : ViewModelBase{ } } + [RelayCommand] - public async Task OpenFolderDialogAsyncSeason(HistorySeason? season){ + public async Task OpenFolderDialogAsync(SeasonDialogArgs? seriesArgs){ if (_storageProvider == null){ Console.Error.WriteLine("StorageProvider must be set before using the dialog."); throw new InvalidOperationException("StorageProvider must be set before using the dialog."); @@ -475,38 +476,19 @@ public partial class HistoryPageViewModel : ViewModelBase{ if (result.Count > 0){ var selectedFolder = result[0]; - // Do something with the selected folder path - Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}"); + var folderPath = selectedFolder.Path.IsAbsoluteUri ? selectedFolder.Path.LocalPath : selectedFolder.Path.ToString(); + Console.WriteLine($"Selected folder: {folderPath}"); - if (season != null){ - season.SeasonDownloadPath = selectedFolder.Path.LocalPath; + if (seriesArgs?.Season != null){ + seriesArgs.Season.SeasonDownloadPath = folderPath; + CfgManager.UpdateHistoryFile(); + } else if (seriesArgs?.Series != null){ + seriesArgs.Series.SeriesDownloadPath = folderPath; CfgManager.UpdateHistoryFile(); } } - } - [RelayCommand] - public async Task OpenFolderDialogAsyncSeries(HistorySeries? series){ - if (_storageProvider == null){ - Console.Error.WriteLine("StorageProvider must be set before using the dialog."); - throw new InvalidOperationException("StorageProvider must be set before using the dialog."); - } - - - var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{ - Title = "Select Folder" - }); - - if (result.Count > 0){ - var selectedFolder = result[0]; - // Do something with the selected folder path - Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}"); - - if (series != null){ - series.SeriesDownloadPath = selectedFolder.Path.LocalPath; - CfgManager.UpdateHistoryFile(); - } - } + seriesArgs?.Series?.UpdateSeriesFolderPath(); } [RelayCommand] @@ -534,6 +516,34 @@ public partial class HistoryPageViewModel : ViewModelBase{ await Task.WhenAll(downloadTasks); } + + [RelayCommand] + public void ToggleDownloadedMark(SeasonDialogArgs seriesArgs){ + if (seriesArgs.Season != null){ + bool allDownloaded = seriesArgs.Season.EpisodesList.All(ep => ep.WasDownloaded); + + foreach (var historyEpisode in seriesArgs.Season.EpisodesList){ + if (historyEpisode.WasDownloaded == allDownloaded){ + seriesArgs.Season.UpdateDownloaded(historyEpisode.EpisodeId); + } + } + } + + seriesArgs.Series?.UpdateNewEpisodes(); + } + + [RelayCommand] + public void OpenFolderPath(HistorySeries? series){ + try{ + Process.Start(new ProcessStartInfo{ + FileName = series?.SeriesFolderPath, + UseShellExecute = true, + Verb = "open" + }); + } catch (Exception ex){ + Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}"); + } + } } public class HistoryPageProperties{ diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index 3b3cbad..923ef4e 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -43,13 +43,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ [ObservableProperty] private string _availableSubs; - - [ObservableProperty] - private string _seriesFolderPath; - - [ObservableProperty] - public bool _seriesFolderPathExists; - + public SeriesPageViewModel(){ _storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider)); @@ -79,60 +73,10 @@ public partial class SeriesPageViewModel : ViewModelBase{ AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang); AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs); - UpdateSeriesFolderPath(); + SelectedSeries.UpdateSeriesFolderPath(); } - private void UpdateSeriesFolderPath(){ - var season = SelectedSeries.Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath)); - - if (!string.IsNullOrEmpty(SelectedSeries.SeriesDownloadPath) && Directory.Exists(SelectedSeries.SeriesDownloadPath)){ - SeriesFolderPath = SelectedSeries.SeriesDownloadPath; - SeriesFolderPathExists = true; - } - - if (season is{ SeasonDownloadPath: not null }){ - try{ - var seasonPath = season.SeasonDownloadPath; - var directoryInfo = new DirectoryInfo(seasonPath); - - if (!string.IsNullOrEmpty(directoryInfo.Parent?.FullName)){ - string parentFolderPath = directoryInfo.Parent?.FullName ?? string.Empty; - - if (Directory.Exists(parentFolderPath)){ - SeriesFolderPath = parentFolderPath; - SeriesFolderPathExists = true; - } - } - } catch (Exception e){ - Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}"); - } - } else{ - string customPath; - - if (string.IsNullOrEmpty(SelectedSeries.SeriesTitle)) - return; - - var seriesTitle = FileNameManager.CleanupFilename(SelectedSeries.SeriesTitle); - - if (string.IsNullOrEmpty(seriesTitle)) - return; - - // Check Crunchyroll download directory - var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath; - if (!string.IsNullOrEmpty(downloadDirPath)){ - customPath = Path.Combine(downloadDirPath, seriesTitle); - } else{ - // Fallback to configured VIDEOS_DIR path - customPath = Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle); - } - - // Check if custom path exists - if (Directory.Exists(customPath)){ - SeriesFolderPath = customPath; - SeriesFolderPathExists = true; - } - } - } + [RelayCommand] public async Task OpenFolderDialogAsync(HistorySeason? season){ @@ -148,19 +92,19 @@ public partial class SeriesPageViewModel : ViewModelBase{ if (result.Count > 0){ var selectedFolder = result[0]; - // Do something with the selected folder path - Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}"); + var folderPath = selectedFolder.Path.IsAbsoluteUri ? selectedFolder.Path.LocalPath : selectedFolder.Path.ToString(); + Console.WriteLine($"Selected folder: {folderPath}"); if (season != null){ - season.SeasonDownloadPath = selectedFolder.Path.LocalPath; + season.SeasonDownloadPath = folderPath; CfgManager.UpdateHistoryFile(); } else{ - SelectedSeries.SeriesDownloadPath = selectedFolder.Path.LocalPath; + SelectedSeries.SeriesDownloadPath = folderPath; CfgManager.UpdateHistoryFile(); } } - UpdateSeriesFolderPath(); + SelectedSeries.UpdateSeriesFolderPath(); } [RelayCommand] @@ -277,7 +221,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ public void OpenFolderPath(){ try{ Process.Start(new ProcessStartInfo{ - FileName = SeriesFolderPath, + FileName = SelectedSeries.SeriesFolderPath, UseShellExecute = true, Verb = "open" }); diff --git a/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs b/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs index 9c7c806..f847156 100644 --- a/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs +++ b/CRD/ViewModels/UpcomingSeasonsPageViewModel.cs @@ -197,8 +197,18 @@ public partial class UpcomingPageViewModel : ViewModelBase{ var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false); 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 }){ + var crunchySeries = crunchySimul.Data.FirstOrDefault(ele => ele.Id == anilistSeries.CrunchyrollID); + if (crunchySeries != null){ + anilistSeries.AudioLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.AudioLocales ??[])); + anilistSeries.SubtitleLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.SubtitleLocales ??[])); + } + } } SortItems(); @@ -212,8 +222,18 @@ public partial class UpcomingPageViewModel : ViewModelBase{ var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false); 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 }){ + var crunchySeries = crunchySimul.Data.FirstOrDefault(ele => ele.Id == anilistSeries.CrunchyrollID); + if (crunchySeries != null){ + anilistSeries.AudioLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.AudioLocales ??[])); + anilistSeries.SubtitleLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.SubtitleLocales ??[])); + } + } } SortItems(); } @@ -411,6 +431,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{ } public void SelectionChangedOfSeries(AnilistSeries? value){ + if (value != null) value.IsExpanded = !value.IsExpanded; SelectedSeries = null; SelectedIndex = -1; } diff --git a/CRD/ViewModels/UpdateViewModel.cs b/CRD/ViewModels/UpdateViewModel.cs new file mode 100644 index 0000000..d2cfc25 --- /dev/null +++ b/CRD/ViewModels/UpdateViewModel.cs @@ -0,0 +1,188 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using Avalonia; +using Avalonia.Media; +using Avalonia.Styling; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CRD.Downloader; +using CRD.Utils.Updater; +using Markdig; + +namespace CRD.ViewModels; + +public partial class UpdateViewModel : ViewModelBase{ + + [ObservableProperty] + private bool _updateAvailable; + + [ObservableProperty] + private bool _updating; + + [ObservableProperty] + private double _progress; + + [ObservableProperty] + private bool _failed; + + private AccountPageViewModel accountPageViewModel; + + [ObservableProperty] + private string _changelogText = "

No changelog found.

"; + + [ObservableProperty] + private string _currentVersion; + + public UpdateViewModel(){ + + var version = Assembly.GetExecutingAssembly().GetName().Version; + _currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}"; + + LoadChangelog(); + + UpdateAvailable = ProgramManager.Instance.UpdateAvailable; + + Updater.Instance.PropertyChanged += Progress_PropertyChanged; + } + + [RelayCommand] + public void StartUpdate(){ + Updating = true; + ProgramManager.Instance.NavigationLock = true; + // Title = "Updating"; + _ = Updater.Instance.DownloadAndUpdateAsync(); + } + + private void Progress_PropertyChanged(object? sender, PropertyChangedEventArgs e){ + if (e.PropertyName == nameof(Updater.Instance.progress)){ + Progress = Updater.Instance.progress; + } else if (e.PropertyName == nameof(Updater.Instance.failed)){ + Failed = Updater.Instance.failed; + ProgramManager.Instance.NavigationLock = !Failed; + } + } + + private void LoadChangelog(){ + string changelogPath = "CHANGELOG.md"; + + if (!File.Exists(changelogPath)){ + ChangelogText = "

No changelog found.

"; + return; + } + + string markdownText = File.ReadAllText(changelogPath); + + markdownText = PreprocessMarkdown(markdownText); + + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseSoftlineBreakAsHardlineBreak() + .Build(); + + string htmlContent = Markdown.ToHtml(markdownText, pipeline); + + htmlContent = MakeIssueLinksClickable(htmlContent); + htmlContent = ModifyImages(htmlContent); + + Color themeTextColor = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark ? Colors.White : Color.Parse("#E4000000"); + string cssColor = $"#{themeTextColor.R:X2}{themeTextColor.G:X2}{themeTextColor.B:X2}"; + + string styledHtml = $@" + + + + + + {htmlContent} + + "; + + ChangelogText = styledHtml; + } + + private string MakeIssueLinksClickable(string htmlContent){ + // Match GitHub issue links + string issuePattern = @"]*>[^<]+<\/a>"; + + // Match GitHub discussion links + string discussionPattern = @"]*>[^<]+<\/a>"; + + htmlContent = Regex.Replace(htmlContent, issuePattern, match => { + string fullUrl = match.Groups[1].Value; + string issueNumber = match.Groups[2].Value; + return $"#{issueNumber}"; + }); + + htmlContent = Regex.Replace(htmlContent, discussionPattern, match => { + string fullUrl = match.Groups[1].Value; + string discussionNumber = match.Groups[2].Value; + return $"#{discussionNumber}"; + }); + + return htmlContent; + } + + + private string ModifyImages(string htmlContent){ + // Regex to match tags + string imgPattern = @""; + + return Regex.Replace(htmlContent, imgPattern, match => { + string imgUrl = match.Groups[1].Value; + string altText = "View Image"; // match.Groups[3].Success ? match.Groups[3].Value : "View Image"; + + return $"{altText}"; + }); + } + + + private string PreprocessMarkdown(string markdownText){ + // Regex to match
blocks containing an image + string detailsPattern = @"
\s*.*?<\/summary>\s*\s*<\/details>"; + + return Regex.Replace(markdownText, detailsPattern, match => { + string imageUrl = match.Groups[1].Value; + string altText = match.Groups[2].Value; + + return $"![{altText}]({imageUrl})"; + }); + } + +} \ No newline at end of file diff --git a/CRD/ViewModels/Utils/ContentDialogUpdateViewModel.cs b/CRD/ViewModels/Utils/ContentDialogUpdateViewModel.cs deleted file mode 100644 index 55bd78b..0000000 --- a/CRD/ViewModels/Utils/ContentDialogUpdateViewModel.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.ComponentModel; -using CommunityToolkit.Mvvm.ComponentModel; -using CRD.Utils.Updater; -using FluentAvalonia.UI.Controls; - -namespace CRD.ViewModels.Utils; - -public partial class ContentDialogUpdateViewModel : ViewModelBase{ - private readonly ContentDialog dialog; - - [ObservableProperty] - private double _progress; - - [ObservableProperty] - private bool _failed; - - private AccountPageViewModel accountPageViewModel; - - public ContentDialogUpdateViewModel(ContentDialog dialog){ - if (dialog is null){ - throw new ArgumentNullException(nameof(dialog)); - } - - this.dialog = dialog; - dialog.Closed += DialogOnClosed; - Updater.Instance.PropertyChanged += Progress_PropertyChanged; - } - - private void Progress_PropertyChanged(object? sender, PropertyChangedEventArgs e){ - if (e.PropertyName == nameof(Updater.Instance.progress)){ - Progress = Updater.Instance.progress; - }else if (e.PropertyName == nameof(Updater.Instance.failed)){ - Failed = Updater.Instance.failed; - dialog.IsPrimaryButtonEnabled = !Failed; - } - - } - - - private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){ - dialog.Closed -= DialogOnClosed; - } -} \ No newline at end of file diff --git a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs index 5381d90..801f1b1 100644 --- a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs +++ b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs @@ -38,10 +38,10 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _historyIncludeCrArtists; - + [ObservableProperty] private bool _historyAddSpecials; - + [ObservableProperty] private bool _historySkipUnmonitored; @@ -193,6 +193,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [ObservableProperty] private string _tempDownloadDirPath; + [ObservableProperty] + private bool _downloadFinishedPlaySound; + + [ObservableProperty] + private string _downloadFinishedSoundPath; + [ObservableProperty] private string _currentIp = ""; @@ -208,7 +214,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ var version = Assembly.GetExecutingAssembly().GetName().Version; _currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}"; - _faTheme = App.Current.Styles[0] as FluentAvaloniaTheme; + _faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme ??[]; if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){ CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor); @@ -221,6 +227,10 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ BackgroundImageBlurRadius = options.BackgroundImageBlurRadius; BackgroundImageOpacity = options.BackgroundImageOpacity; BackgroundImagePath = options.BackgroundImagePath ?? string.Empty; + + DownloadFinishedSoundPath = options.DownloadFinishedSoundPath ?? string.Empty; + DownloadFinishedPlaySound = options.DownloadFinishedPlaySound; + DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath; TempDownloadDirPath = string.IsNullOrEmpty(options.DownloadTempDirPath) ? CfgManager.PathTEMP_DIR : options.DownloadTempDirPath; @@ -269,6 +279,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ return; } + CrunchyrollManager.Instance.CrunOptions.DownloadFinishedPlaySound = DownloadFinishedPlaySound; + CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40); CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1); @@ -371,14 +383,17 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ if (result.Count > 0){ var selectedFolder = result[0]; - Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}"); - pathSetter(selectedFolder.Path.LocalPath); + var folderPath = selectedFolder.Path.IsAbsoluteUri ? selectedFolder.Path.LocalPath : selectedFolder.Path.ToString(); + Console.WriteLine($"Selected folder: {folderPath}"); + pathSetter(folderPath); var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter(); pathSetter(finalPath); CfgManager.WriteCrSettings(); } } + #region Background Image + [RelayCommand] public void ClearBackgroundImagePath(){ CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath = string.Empty; @@ -388,7 +403,13 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [RelayCommand] public async Task OpenImageFileDialogAsyncInternalBackgroundImage(){ - await OpenImageFileDialogAsyncInternal( + await OpenFileDialogAsyncInternal( + title: "Select Image File", + fileTypes: new List{ + new("Image Files"){ + Patterns = new[]{ "*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif" } + } + }, pathSetter: (path) => { CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath = path; BackgroundImagePath = path; @@ -399,20 +420,51 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ ); } - private async Task OpenImageFileDialogAsyncInternal(Action pathSetter, Func pathGetter, string defaultPath){ + #endregion + + + #region Download Finished Sound + + [RelayCommand] + public void ClearFinishedSoundPath(){ + CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath = string.Empty; + DownloadFinishedSoundPath = string.Empty; + } + + [RelayCommand] + public async Task OpenImageFileDialogAsyncInternalFinishedSound(){ + await OpenFileDialogAsyncInternal( + title: "Select Audio File", + fileTypes: new List{ + new("Audio Files"){ + Patterns = new[]{ "*.mp3", "*.wav", "*.ogg", "*.flac", "*.aac" } + } + }, + pathSetter: (path) => { + CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath = path; + DownloadFinishedSoundPath = path; + }, + pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath, + defaultPath: string.Empty + ); + } + + #endregion + + private async Task OpenFileDialogAsyncInternal( + string title, + List fileTypes, + Action pathSetter, + Func pathGetter, + string defaultPath){ if (_storageProvider == null){ Console.Error.WriteLine("StorageProvider must be set before using the dialog."); throw new InvalidOperationException("StorageProvider must be set before using the dialog."); } - // Open the file picker dialog with only image file types allowed var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions{ - Title = "Select Image File", - FileTypeFilter = new List{ - new FilePickerFileType("Image Files"){ - Patterns = new[]{ "*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif" } - } - }, + Title = title, + FileTypeFilter = fileTypes, AllowMultiple = false }); @@ -426,6 +478,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ } } + partial void OnCurrentAppThemeChanged(ComboBoxItem? value){ if (value?.Content?.ToString() == "System"){ _faTheme.PreferSystemTheme = true; diff --git a/CRD/Views/DownloadsPageView.axaml b/CRD/Views/DownloadsPageView.axaml index 01d7b05..d118730 100644 --- a/CRD/Views/DownloadsPageView.axaml +++ b/CRD/Views/DownloadsPageView.axaml @@ -23,15 +23,32 @@ - + + diff --git a/CRD/Views/DownloadsPageView.axaml.cs b/CRD/Views/DownloadsPageView.axaml.cs index dbd1e7c..a2a439a 100644 --- a/CRD/Views/DownloadsPageView.axaml.cs +++ b/CRD/Views/DownloadsPageView.axaml.cs @@ -1,4 +1,6 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using CRD.ViewModels; namespace CRD.Views; @@ -6,4 +8,5 @@ public partial class DownloadsPageView : UserControl{ public DownloadsPageView(){ InitializeComponent(); } + } \ No newline at end of file diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml index 48849bd..9d6b76e 100644 --- a/CRD/Views/HistoryPageView.axaml +++ b/CRD/Views/HistoryPageView.axaml @@ -7,7 +7,6 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:history="clr-namespace:CRD.Utils.Structs.History" xmlns:structs="clr-namespace:CRD.Utils.Structs" - xmlns:local="clr-namespace:CRD.Utils" x:DataType="vm:HistoryPageViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="CRD.Views.HistoryPageView"> @@ -15,7 +14,11 @@ + + + + @@ -25,7 +28,7 @@ - + @@ -393,13 +396,26 @@ + + - - + - + + + + + + + + + + + + @@ -427,6 +443,20 @@ Height="30" /> + + @@ -437,8 +467,13 @@ + + @@ -770,16 +818,34 @@ Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).DownloadSeasonMissingSonarr}" CommandParameter="{Binding }"> + - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CRD/Views/UpdateView.axaml b/CRD/Views/UpdateView.axaml new file mode 100644 index 0000000..c546fc0 --- /dev/null +++ b/CRD/Views/UpdateView.axaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Views/UpdateView.axaml.cs b/CRD/Views/UpdateView.axaml.cs new file mode 100644 index 0000000..8bb9721 --- /dev/null +++ b/CRD/Views/UpdateView.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls; + +namespace CRD.Views; + +public partial class UpdateView : UserControl{ + public UpdateView(){ + InitializeComponent(); + } +} \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogUpdateView.axaml b/CRD/Views/Utils/ContentDialogUpdateView.axaml deleted file mode 100644 index e317023..0000000 --- a/CRD/Views/Utils/ContentDialogUpdateView.axaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogUpdateView.axaml.cs b/CRD/Views/Utils/ContentDialogUpdateView.axaml.cs deleted file mode 100644 index 8688188..0000000 --- a/CRD/Views/Utils/ContentDialogUpdateView.axaml.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Avalonia.Controls; - -namespace CRD.Views.Utils; - -public partial class ContentDialogUpdateView : UserControl{ - public ContentDialogUpdateView(){ - InitializeComponent(); - } -} \ No newline at end of file diff --git a/CRD/Views/Utils/GeneralSettingsView.axaml b/CRD/Views/Utils/GeneralSettingsView.axaml index bcb140b..cefd96b 100644 --- a/CRD/Views/Utils/GeneralSettingsView.axaml +++ b/CRD/Views/Utils/GeneralSettingsView.axaml @@ -14,7 +14,7 @@ - + - + - + - + @@ -145,6 +145,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -201,13 +291,13 @@ - + - + @@ -225,7 +315,7 @@ - + - +