From 3d74fa766751b4efe49a322d4e624e6964ff5157 Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Fri, 21 Jun 2024 03:49:44 +0200 Subject: [PATCH] Add - Added download path to download settings Add - Added download path to history series and also to each season Fix - When adding an episode it always showed "Added to Queue" even if it wasn't added --- CRD/Downloader/CrEpisode.cs | 328 +++++++++++------------- CRD/Downloader/Crunchyroll.cs | 167 ++++++------ CRD/Downloader/History.cs | 63 +++-- CRD/Utils/Helpers.cs | 60 ++++- CRD/Utils/Structs/CrDownloadOptions.cs | 3 + CRD/Utils/Structs/EpisodeStructs.cs | 19 +- CRD/ViewModels/SeriesPageViewModel.cs | 35 +++ CRD/ViewModels/SettingsPageViewModel.cs | 70 +++-- CRD/Views/MainWindow.axaml.cs | 10 +- CRD/Views/SeriesPageView.axaml | 35 ++- CRD/Views/SettingsPageView.axaml | 19 ++ CRD/Views/SettingsPageView.axaml.cs | 1 - 12 files changed, 500 insertions(+), 310 deletions(-) diff --git a/CRD/Downloader/CrEpisode.cs b/CRD/Downloader/CrEpisode.cs index 8b0c94b..694a4ca 100644 --- a/CRD/Downloader/CrEpisode.cs +++ b/CRD/Downloader/CrEpisode.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; +using Avalonia.Remote.Protocol.Input; using CRD.Utils; using CRD.Utils.Structs; using Newtonsoft.Json; @@ -14,10 +15,9 @@ using Newtonsoft.Json; namespace CRD.Downloader; public class CrEpisode(){ - private readonly Crunchyroll crunInstance = Crunchyroll.Instance; - - public async Task ParseEpisodeById(string id,string locale){ + + public async Task ParseEpisodeById(string id, string locale){ if (crunInstance.CmsToken?.Cms == null){ Console.Error.WriteLine("Missing CMS Access Token"); return null; @@ -26,7 +26,7 @@ public class CrEpisode(){ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); query["preferred_audio_language"] = "ja-JP"; - query["locale"] = Languages.Locale2language(locale).CrLocale; + query["locale"] = Languages.Locale2language(locale).CrLocale; var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/episodes/{id}", HttpMethod.Get, true, true, query); @@ -43,217 +43,191 @@ public class CrEpisode(){ return null; } - return epsidoe; + if (epsidoe.Total == 1 && epsidoe.Data != null){ + return epsidoe.Data.First(); + } + + Console.Error.WriteLine("Multiple episodes returned with one ID?"); + if (epsidoe.Data != null) return epsidoe.Data.First(); + return null; } - public async Task EpisodeData(CrunchyEpisodeList dlEpisodes){ + public async Task EpisodeData(CrunchyEpisode dlEpisode){ bool serieshasversions = true; - Dictionary episodes = new Dictionary(); + // Dictionary episodes = new Dictionary(); - if (dlEpisodes.Data != null){ - foreach (var episode in dlEpisodes.Data){ - - if (crunInstance.CrunOptions.History){ - await crunInstance.CrHistory.UpdateWithEpisode(episode); - } - - // Prepare the episode array - EpisodeAndLanguage item; - var seasonIdentifier = !string.IsNullOrEmpty(episode.Identifier) ? episode.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}"; - var episodeKey = $"{seasonIdentifier}E{episode.Episode ?? (episode.EpisodeNumber + "")}"; + CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData(); - if (!episodes.ContainsKey(episodeKey)){ - item = new EpisodeAndLanguage{ - Items = new List(), - Langs = new List() - }; - episodes[episodeKey] = item; - } else{ - item = episodes[episodeKey]; - } + if (crunInstance.CrunOptions.History){ + await crunInstance.CrHistory.UpdateWithEpisode(dlEpisode); + } - if (episode.Versions != null){ - foreach (var version in episode.Versions){ - // Ensure there is only one of the same language - if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){ - // Push to arrays if there are no duplicates of the same language - item.Items.Add(episode); - item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale)); - } - } - } else{ - // Episode didn't have versions, mark it as such to be logged. - serieshasversions = false; - // Ensure there is only one of the same language - if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){ - // Push to arrays if there are no duplicates of the same language - item.Items.Add(episode); - item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale)); - } + var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier) ? dlEpisode.Identifier.Split('|')[1] : $"S{dlEpisode.SeasonNumber}"; + episode.Key = $"{seasonIdentifier}E{dlEpisode.Episode ?? (dlEpisode.EpisodeNumber + "")}"; + episode.EpisodeAndLanguages = new EpisodeAndLanguage{ + Items = new List(), + Langs = new List() + }; + + if (dlEpisode.Versions != null){ + foreach (var version in dlEpisode.Versions){ + // Ensure there is only one of the same language + if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){ + // Push to arrays if there are no duplicates of the same language + episode.EpisodeAndLanguages.Items.Add(dlEpisode); + episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale)); } } + } else{ + // Episode didn't have versions, mark it as such to be logged. + serieshasversions = false; + // Ensure there is only one of the same language + if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){ + // Push to arrays if there are no duplicates of the same language + episode.EpisodeAndLanguages.Items.Add(dlEpisode); + episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale)); + } } + int specialIndex = 1; int epIndex = 1; - var keys = new List(episodes.Keys); // Copying the keys to a new list to avoid modifying the collection while iterating. - foreach (var key in keys){ - EpisodeAndLanguage item = episodes[key]; - var isSpecial = !Regex.IsMatch(item.Items[0].Episode ?? string.Empty, @"^\d+$"); // Checking if the episode is not a number (i.e., special). - string newKey; - if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){ - newKey = item.Items[0].Episode ?? "SP" + (specialIndex + " " + item.Items[0].Id); - } else{ - newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}"; - } - - episodes.Remove(key); - episodes.Add(newKey, item); - - if (isSpecial){ - specialIndex++; - } else{ - epIndex++; - } + var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+$"); // Checking if the episode is not a number (i.e., special). + string newKey; + if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){ + newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id); + } else{ + newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id) : episode.EpisodeAndLanguages.Items[0].Episode ?? epIndex + "")}"; } - var specials = episodes.Where(e => e.Key.StartsWith("SP")).ToList(); - var normal = episodes.Where(e => e.Key.StartsWith("E")).ToList(); + episode.Key = newKey; - // Combining and sorting episodes with normal first, then specials. - var sortedEpisodes = new Dictionary(normal.Concat(specials)); + var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle + ?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); - foreach (var kvp in sortedEpisodes){ - var key = kvp.Key; - var item = kvp.Value; + var title = episode.EpisodeAndLanguages.Items[0].Title; + var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString(); - var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle - ?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); + var languages = episode.EpisodeAndLanguages.Items.Select((a, index) => + $"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆ - var title = item.Items[0].Title; - var seasonNumber = Helpers.ExtractNumberAfterS(item.Items[0].Identifier) ?? item.Items[0].SeasonNumber.ToString(); + Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]"); - var languages = item.Items.Select((a, index) => - $"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆ - - Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]"); - } if (!serieshasversions){ - Console.WriteLine("Couldn\'t find versions on some episodes, fell back to old method."); + Console.WriteLine("Couldn\'t find versions on episode, fell back to old method."); } - CrunchySeriesList crunchySeriesList = new CrunchySeriesList(); - crunchySeriesList.Data = sortedEpisodes; - crunchySeriesList.List = sortedEpisodes.Select(kvp => { - var key = kvp.Key; - var value = kvp.Value; - var images = (value.Items[0].Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); - var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0); - return new Episode{ - E = key.StartsWith("E") ? key.Substring(1) : key, - Lang = value.Langs.Select(a => a.Code).ToList(), - Name = value.Items[0].Title, - Season = Helpers.ExtractNumberAfterS(value.Items[0].Identifier) ?? value.Items[0].SeasonNumber.ToString(), - SeriesTitle = Regex.Replace(value.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(), - SeasonTitle = Regex.Replace(value.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(), - EpisodeNum = value.Items[0].EpisodeNumber?.ToString() ?? value.Items[0].Episode ?? "?", - Id = value.Items[0].SeasonId, - Img = images[images.Count / 2].FirstOrDefault().Source, - Description = value.Items[0].Description, - Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds. - }; - }).ToList(); + // crunchySeriesList.Data = sortedEpisodes; + // + // + // var images = (episode.EpisodeAndLanguages.Items[0].Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); + // var seconds = (int)Math.Floor(episode.EpisodeAndLanguages.Items[0].DurationMs / 1000.0); + // + // var newEpisode = new Episode{ + // E = episode.Key.StartsWith("E") ? episode.Key.Substring(1) : episode.Key, + // Lang = episode.EpisodeAndLanguages.Langs.Select(a => a.Code).ToList(), + // Name = episode.EpisodeAndLanguages.Items[0].Title, + // Season = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString(), + // SeriesTitle = Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(), + // SeasonTitle = Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(), + // EpisodeNum = episode.EpisodeAndLanguages.Items[0].EpisodeNumber?.ToString() ?? episode.EpisodeAndLanguages.Items[0].Episode ?? "?", + // Id = episode.EpisodeAndLanguages.Items[0].SeasonId, + // Img = images[images.Count / 2].FirstOrDefault().Source, + // Description = episode.EpisodeAndLanguages.Items[0].Description, + // Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds. + // }; + // + // CrunchySeriesList crunchySeriesList = new CrunchySeriesList(); - return crunchySeriesList; + return episode; } - public Dictionary EpisodeMeta(Dictionary eps, List dubLang){ - var ret = new Dictionary(); + public CrunchyEpMeta EpisodeMeta(CrunchyRollEpisodeData episodeP, List dubLang){ + // var ret = new Dictionary(); + var retMeta = new CrunchyEpMeta(); + + - foreach (var kvp in eps){ - var key = kvp.Key; - var episode = kvp.Value; + for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){ + var item = episodeP.EpisodeAndLanguages.Items[index]; - for (int index = 0; index < episode.Items.Count; index++){ - var item = episode.Items[index]; + if (!dubLang.Contains(episodeP.EpisodeAndLanguages.Langs[index].CrLocale)) + continue; - if (!dubLang.Contains(episode.Langs[index].CrLocale)) - continue; - - item.HideSeasonTitle = true; - if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){ - item.SeasonTitle = item.SeriesTitle; - item.HideSeasonTitle = false; - item.HideSeasonNumber = true; - } - - if (string.IsNullOrEmpty(item.SeasonTitle) && string.IsNullOrEmpty(item.SeriesTitle)){ - item.SeasonTitle = "NO_TITLE"; - item.SeriesTitle = "NO_TITLE"; - } - - var epNum = key.StartsWith('E') ? key[1..] : key; - var images = (item.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); - - Regex dubPattern = new Regex(@"\(\w+ Dub\)"); - - var epMeta = new CrunchyEpMeta(); - epMeta.Data = new List{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } }; - epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(); - epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); - epMeta.EpisodeNumber = item.Episode; - epMeta.EpisodeTitle = item.Title; - epMeta.SeasonId = item.SeasonId; - epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; - epMeta.ShowId = item.SeriesId; - epMeta.AbsolutEpisodeNumberE = epNum; - epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; - epMeta.DownloadProgress = new DownloadProgress(){ - IsDownloading = false, - Done = false, - Error = false, - Percent = 0, - Time = 0, - DownloadSpeed = 0 - }; - epMeta.AvailableSubs = item.SubtitleLocales; - if (episode.Langs.Count > 0){ - epMeta.SelectedDubs = dubLang - .Where(language => episode.Langs.Any(epLang => epLang.CrLocale == language)) - .ToList(); - } - - var epMetaData = epMeta.Data[0]; - if (!string.IsNullOrEmpty(item.StreamsLink)){ - epMetaData.Playback = item.StreamsLink; - if (string.IsNullOrEmpty(item.Playback)){ - item.Playback = item.StreamsLink; - } - } - - if (ret.TryGetValue(key, out var epMe)){ - epMetaData.Lang = episode.Langs[index]; - epMe.Data?.Add(epMetaData); - } else{ - epMetaData.Lang = episode.Langs[index]; - epMeta.Data[0] = epMetaData; - ret.Add(key, epMeta); - } - - - // show ep - item.SeqId = epNum; + item.HideSeasonTitle = true; + if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){ + item.SeasonTitle = item.SeriesTitle; + item.HideSeasonTitle = false; + item.HideSeasonNumber = true; } + + if (string.IsNullOrEmpty(item.SeasonTitle) && string.IsNullOrEmpty(item.SeriesTitle)){ + item.SeasonTitle = "NO_TITLE"; + item.SeriesTitle = "NO_TITLE"; + } + + var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key; + var images = (item.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); + + Regex dubPattern = new Regex(@"\(\w+ Dub\)"); + + var epMeta = new CrunchyEpMeta(); + epMeta.Data = new List{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } }; + epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(); + epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); + epMeta.EpisodeNumber = item.Episode; + epMeta.EpisodeTitle = item.Title; + epMeta.SeasonId = item.SeasonId; + epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; + epMeta.ShowId = item.SeriesId; + epMeta.AbsolutEpisodeNumberE = epNum; + epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; + epMeta.DownloadProgress = new DownloadProgress(){ + IsDownloading = false, + Done = false, + Error = false, + Percent = 0, + Time = 0, + DownloadSpeed = 0 + }; + epMeta.AvailableSubs = item.SubtitleLocales; + if (episodeP.EpisodeAndLanguages.Langs.Count > 0){ + epMeta.SelectedDubs = dubLang + .Where(language => episodeP.EpisodeAndLanguages.Langs.Any(epLang => epLang.CrLocale == language)) + .ToList(); + } + + var epMetaData = epMeta.Data[0]; + if (!string.IsNullOrEmpty(item.StreamsLink)){ + epMetaData.Playback = item.StreamsLink; + if (string.IsNullOrEmpty(item.Playback)){ + item.Playback = item.StreamsLink; + } + } + + if (retMeta.Data != null){ + epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index]; + retMeta.Data.Add(epMetaData); + + } else{ + epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index]; + epMeta.Data[0] = epMetaData; + retMeta = epMeta; + } + + + // show ep + item.SeqId = epNum; } - return ret; + return retMeta; } } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll.cs b/CRD/Downloader/Crunchyroll.cs index df1956a..7dea7c9 100644 --- a/CRD/Downloader/Crunchyroll.cs +++ b/CRD/Downloader/Crunchyroll.cs @@ -286,34 +286,48 @@ public class Crunchyroll{ if (episodeL != null){ - if (episodeL.Value.Data != null && episodeL.Value.Data.First().IsPremiumOnly && !Profile.HasPremium){ + if (episodeL.Value.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; } - var sList = await CrEpisode.EpisodeData((CrunchyEpisodeList)episodeL); - var selected = CrEpisode.EpisodeMeta(sList.Data, dubLang); - var metas = selected.Values.ToList(); + var sList = await CrEpisode.EpisodeData((CrunchyEpisode)episodeL); + var selected = CrEpisode.EpisodeMeta(sList, dubLang); - foreach (var crunchyEpMeta in metas){ - if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ - var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId); - if (historyEpisode != null){ - if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){ - crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber; - } + if (selected.Data is{ Count: > 0 }){ + if (CrunOptions.History){ + var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(selected.ShowId, selected.SeasonId, selected.Data.First().MediaId); + if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ + if (historyEpisode.historyEpisode != null){ + if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){ + selected.EpisodeNumber = historyEpisode.historyEpisode.SonarrEpisodeNumber; + } - if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){ - crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber; + if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrSeasonNumber)){ + selected.Season = historyEpisode.historyEpisode.SonarrSeasonNumber; + } } } + + if (!string.IsNullOrEmpty(historyEpisode.downloadDirPath)){ + selected.DownloadPath = historyEpisode.downloadDirPath; + } } - Queue.Add(crunchyEpMeta); - } + Queue.Add(selected); - Console.WriteLine("Added Episode to Queue"); - MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1)); + + if (selected.Data.Count < dubLang.Count){ + Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs"); + MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue but couldn't find all selected dubs", ToastType.Warning, 2)); + } else{ + Console.WriteLine("Added Episode to Queue"); + MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1)); + } + } else{ + Console.WriteLine("Episode couldn't be added to Queue"); + MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2)); + } } } @@ -324,17 +338,23 @@ public class Crunchyroll{ foreach (var crunchyEpMeta in selected.Values.ToList()){ if (crunchyEpMeta.Data?.First().Playback != null){ - if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ - var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId); - if (historyEpisode != null){ - if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeNumber)){ - crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber; - } + if (CrunOptions.History){ + var historyEpisode = CrHistory.GetHistoryEpisodeWithDownloadDir(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId); + if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){ + if (historyEpisode.historyEpisode != null){ + if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){ + crunchyEpMeta.EpisodeNumber = historyEpisode.historyEpisode.SonarrEpisodeNumber; + } - if (!string.IsNullOrEmpty(historyEpisode.SonarrSeasonNumber)){ - crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber; + if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrSeasonNumber)){ + crunchyEpMeta.Season = historyEpisode.historyEpisode.SonarrSeasonNumber; + } } } + + if (!string.IsNullOrEmpty(historyEpisode.downloadDirPath)){ + crunchyEpMeta.DownloadPath = historyEpisode.downloadDirPath; + } } Queue.Add(crunchyEpMeta); @@ -596,7 +616,7 @@ public class Crunchyroll{ return new DownloadResponse{ Data = files, Error = true, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown", + FileName = "./unknown", ErrorText = "Video Data not found" }; } @@ -604,11 +624,18 @@ public class Crunchyroll{ bool dlFailed = false; bool dlVideoOnce = false; + string fileDir = CfgManager.PathVIDEOS_DIR; if (data.Data != null){ foreach (CrunchyEpMetaData epMeta in data.Data){ Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}"); + fileDir = !string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath : !string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR; + + if (!Helpers.IsValidPath(fileDir)){ + fileDir = CfgManager.PathVIDEOS_DIR; + } + string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId); await CrAuth.RefreshToken(true); @@ -964,12 +991,13 @@ public class Crunchyroll{ Console.WriteLine($"\tServer: {selectedServer}"); Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]); + fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.Name ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray()); string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override) .ToArray()); - string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(CfgManager.PathVIDEOS_DIR, tempFile); + string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile); bool audioDownloaded = false, videoDownloaded = false; @@ -978,7 +1006,7 @@ public class Crunchyroll{ } else if (options.Novids){ Console.WriteLine("Skipping video download..."); } else{ - var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tsFile, tempTsFile, data); + var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tsFile, tempTsFile, data, fileDir); tsFile = videoDownloadResult.tsFile; @@ -993,7 +1021,7 @@ public class Crunchyroll{ if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){ - var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tsFile, tempTsFile, data); + var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tsFile, tempTsFile, data, fileDir); tsFile = audioDownloadResult.tsFile; @@ -1011,7 +1039,7 @@ public class Crunchyroll{ return new DownloadResponse{ Data = files, Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown", + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", ErrorText = "" }; } @@ -1029,7 +1057,7 @@ public class Crunchyroll{ var assetIdRegexMatch = Regex.Match(chosenVideoSegments.segments[0].uri, @"/assets/(?:p/)?([^_,]+)"); var assetId = assetIdRegexMatch.Success ? assetIdRegexMatch.Groups[1].Value : null; var sessionId = Helpers.GenerateSessionId(); - + Console.WriteLine("Decryption Needed, attempting to decrypt"); if (!_widevine.canDecrypt){ @@ -1037,7 +1065,7 @@ public class Crunchyroll{ return new DownloadResponse{ Data = files, Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown", + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", ErrorText = "Decryption Needed but couldn't find CDM files" }; } @@ -1065,16 +1093,16 @@ public class Crunchyroll{ return new DownloadResponse{ Data = files, Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown", + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", ErrorText = "DRM Authentication failed" }; } - + DrmAuthData authData = Helpers.Deserialize(decRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new DrmAuthData(); - + Dictionary authDataDict = new Dictionary { { "dt-custom-data", authData.CustomData ?? string.Empty },{ "x-dt-auth-token", authData.Token ?? string.Empty } }; - + var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, "https://lic.drmtoday.com/license-proxy-widevine/cenc/", authDataDict); if (encryptionKeys.Count == 0){ @@ -1083,11 +1111,11 @@ public class Crunchyroll{ return new DownloadResponse{ Data = files, Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown", + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", ErrorText = "Couldn't get DRM encryption keys" }; } - + if (Path.Exists(CfgManager.PathMP4Decrypt)){ var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower(); @@ -1249,7 +1277,7 @@ public class Crunchyroll{ if (Path.IsPathRooted(outFile)){ tsFile = outFile; } else{ - tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile); + tsFile = Path.Combine(fileDir, outFile); } // Check if the path is absolute @@ -1259,7 +1287,7 @@ public class Crunchyroll{ string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty(); // Initialize the cumulative path based on whether the original path is absolute or not - string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR; + string cumulativePath = isAbsolute ? "" : fileDir; for (int i = 0; i < directories.Length; i++){ // Build the path incrementally cumulativePath = Path.Combine(cumulativePath, directories[i]); @@ -1295,7 +1323,7 @@ public class Crunchyroll{ } if (!options.SkipSubs && options.DlSubs.IndexOf("none") == -1){ - await DownloadSubtitles(options, pbData, audDub, fileName, files); + await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir); } else{ Console.WriteLine("Subtitles downloading skipped!"); } @@ -1311,12 +1339,12 @@ public class Crunchyroll{ return new DownloadResponse{ Data = files, Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown", + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", ErrorText = "" }; } - private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files){ + private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files, string fileDir){ if (pbData.Meta != null && pbData.Meta.Subtitles != null && pbData.Meta.Subtitles.Count > 0){ List subsData = pbData.Meta.Subtitles.Values.ToList(); List capsData = pbData.Meta.ClosedCaptions?.Values.ToList() ?? new List(); @@ -1354,26 +1382,9 @@ public class Crunchyroll{ var isSigns = langItem.Code == audDub && !subsItem.isCC; var isCc = subsItem.isCC; sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format); - sxData.Path = Path.Combine(CfgManager.PathVIDEOS_DIR, sxData.File); + sxData.Path = Path.Combine(fileDir, sxData.File); - // Check if the path is absolute - bool isAbsolute = Path.IsPathRooted(sxData.Path); - - // Get all directory parts of the path except the last segment (assuming it's a file) - string[] directories = Path.GetDirectoryName(sxData.Path)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty(); - - // Initialize the cumulative path based on whether the original path is absolute or not - string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR; - for (int i = 0; i < directories.Length; i++){ - // Build the path incrementally - cumulativePath = Path.Combine(cumulativePath, directories[i]); - - // Check if the directory exists and create it if it does not - if (!Directory.Exists(cumulativePath)){ - Directory.CreateDirectory(cumulativePath); - Console.WriteLine($"Created directory: {cumulativePath}"); - } - } + Helpers.EnsureDirectoriesExist(sxData.Path); // Check if any file matches the specified conditions if (files.Any(a => a.Type == DownloadMediaType.Subtitle && @@ -1437,7 +1448,8 @@ public class Crunchyroll{ } } - private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data){ + private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data, + string fileDir){ // Prepare for video download int totalParts = chosenVideoSegments.segments.Count; int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize); @@ -1447,28 +1459,10 @@ public class Crunchyroll{ if (Path.IsPathRooted(outFile)){ tsFile = outFile; } else{ - tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile); + tsFile = Path.Combine(fileDir, outFile); } - // var split = outFile.Split(Path.DirectorySeparatorChar).AsSpan().Slice(0, -1).ToArray(); - // Check if the path is absolute - bool isAbsolute = Path.IsPathRooted(outFile); - - // Get all directory parts of the path except the last segment (assuming it's a file) - string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty(); - - // Initialize the cumulative path based on whether the original path is absolute or not - string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR; - for (int i = 0; i < directories.Length; i++){ - // Build the path incrementally - cumulativePath = Path.Combine(cumulativePath, directories[i]); - - // Check if the directory exists and create it if it does not - if (!Directory.Exists(cumulativePath)){ - Directory.CreateDirectory(cumulativePath); - Console.WriteLine($"Created directory: {cumulativePath}"); - } - } + Helpers.EnsureDirectoriesExist(outFile); M3U8Json videoJson = new M3U8Json{ Segments = chosenVideoSegments.segments.Cast().ToList() @@ -1489,7 +1483,8 @@ public class Crunchyroll{ return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile); } - private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data){ + private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data, + string fileDir){ // Prepare for audio download int totalParts = chosenAudioSegments.segments.Count; int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize); @@ -1499,7 +1494,7 @@ public class Crunchyroll{ if (Path.IsPathRooted(outFile)){ tsFile = outFile; } else{ - tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile); + tsFile = Path.Combine(fileDir, outFile); } // Check if the path is absolute @@ -1509,7 +1504,7 @@ public class Crunchyroll{ string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty(); // Initialize the cumulative path based on whether the original path is absolute or not - string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR; + string cumulativePath = isAbsolute ? "" : fileDir; for (int i = 0; i < directories.Length; i++){ // Build the path incrementally cumulativePath = Path.Combine(cumulativePath, directories[i]); diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index c9bef51..d948301 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -79,7 +79,7 @@ public class History(){ MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1)); } - + public HistoryEpisode? GetHistoryEpisode(string? seriesId, string? seasonId, string episodeId){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); @@ -90,7 +90,6 @@ public class History(){ var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); if (historyEpisode != null){ - return historyEpisode; } } @@ -99,21 +98,45 @@ public class History(){ return null; } + public (HistoryEpisode? historyEpisode, string downloadDirPath) GetHistoryEpisodeWithDownloadDir(string? seriesId, string? seasonId, string episodeId){ + var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); + + var downloadDirPath = ""; + + if (historySeries != null){ + var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId); + if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){ + downloadDirPath = historySeries.SeriesDownloadPath; + } + + if (historySeason != null){ + var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); + if (!string.IsNullOrEmpty(historySeason.SeasonDownloadPath)){ + downloadDirPath = historySeason.SeasonDownloadPath; + } + + if (historyEpisode != null){ + return (historyEpisode, downloadDirPath); + } + } + } + + return (null, downloadDirPath); + } + public async Task UpdateWithEpisode(CrunchyEpisode episodeParam){ var episode = episodeParam; - + if (episode.Versions != null){ var version = episode.Versions.Find(a => a.Original); if (version.AudioLocale != episode.AudioLocale){ - var episodeById = await crunInstance.CrEpisode.ParseEpisodeById(version.Guid, ""); - if (episodeById?.Data != null){ - if (episodeById.Value.Total != 1){ - MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1)); - return; - } - - episode = episodeById.Value.Data.First(); + var crEpisode = await crunInstance.CrEpisode.ParseEpisodeById(version.Guid, ""); + if (crEpisode != null){ + episode = crEpisode.Value; + } else{ + MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1)); + return; } } } @@ -182,7 +205,7 @@ public class History(){ } MatchHistorySeriesWithSonarr(false); - await MatchHistoryEpisodesWithSonarr(false,historySeries); + await MatchHistoryEpisodesWithSonarr(false, historySeries); UpdateHistoryFile(); } @@ -262,6 +285,7 @@ public class History(){ historySeries.UpdateNewEpisodes(); } + var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList(); crunInstance.HistoryList.Clear(); foreach (var item in sortedList){ @@ -269,7 +293,7 @@ public class History(){ } MatchHistorySeriesWithSonarr(false); - await MatchHistoryEpisodesWithSonarr(false,historySeries); + await MatchHistoryEpisodesWithSonarr(false, historySeries); UpdateHistoryFile(); } } @@ -341,11 +365,10 @@ public class History(){ } public void MatchHistorySeriesWithSonarr(bool updateAll){ - if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){ return; } - + foreach (var historySeries in crunInstance.HistoryList){ if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle); @@ -362,7 +385,7 @@ public class History(){ if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){ return; } - + if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); @@ -551,6 +574,9 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonProperty("series_season_list")] public required List Seasons{ get; set; } + [JsonProperty("series_download_path")] + public string? SeriesDownloadPath{ get; set; } + public event PropertyChangedEventHandler? PropertyChanged; [JsonIgnore] @@ -636,7 +662,7 @@ public class HistorySeries : INotifyPropertyChanged{ await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId); FetchingData = false; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); - Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false,this); + Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, this); } } @@ -662,6 +688,9 @@ public class HistorySeason : INotifyPropertyChanged{ [JsonProperty("season_episode_list")] public required List EpisodesList{ get; set; } + [JsonProperty("series_download_path")] + public string? SeasonDownloadPath{ get; set; } + public event PropertyChangedEventHandler? PropertyChanged; public void UpdateDownloaded(string? EpisodeId){ diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index d6373db..0ad3f21 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -27,6 +27,64 @@ public class Helpers{ } } + public static void EnsureDirectoriesExist(string path){ + // Check if the path is absolute + bool isAbsolute = Path.IsPathRooted(path); + + // Get all directory parts of the path except the last segment (assuming it's a file) + string directoryPath = Path.GetDirectoryName(path); + + if (string.IsNullOrEmpty(directoryPath)){ + Console.WriteLine("The provided path does not contain any directory information."); + return; + } + + // Initialize the cumulative path based on whether the original path is absolute or not + string cumulativePath = isAbsolute ? Path.GetPathRoot(directoryPath) : Environment.CurrentDirectory; + + // Get all directory parts + string[] directories = directoryPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + // Start the loop from the correct initial index + int startIndex = isAbsolute && directories.Length > 0 && string.IsNullOrEmpty(directories[0]) ? 2 : 0; + + for (int i = startIndex; i < directories.Length; i++){ + // Skip empty parts (which can occur with UNC paths) + if (string.IsNullOrEmpty(directories[i])){ + continue; + } + + // Build the path incrementally + cumulativePath = Path.Combine(cumulativePath, directories[i]); + + // Check if the directory exists and create it if it does not + if (!Directory.Exists(cumulativePath)){ + Directory.CreateDirectory(cumulativePath); + Console.WriteLine($"Created directory: {cumulativePath}"); + } + } + } + + public static bool IsValidPath(string path){ + char[] invalidChars = Path.GetInvalidPathChars(); + + if (string.IsNullOrWhiteSpace(path)){ + return false; + } + + if (path.Any(ch => invalidChars.Contains(ch))){ + return false; + } + + try{ + // Use Path.GetFullPath to ensure that the path can be fully qualified + string fullPath = Path.GetFullPath(path); + return true; + } catch (Exception){ + return false; + } + } + public static Locale ConvertStringToLocale(string? value){ foreach (Locale locale in Enum.GetValues(typeof(Locale))){ var type = typeof(Locale); @@ -121,7 +179,7 @@ public class Helpers{ } } catch (Exception ex){ Console.Error.WriteLine($"An error occurred: {ex.Message}"); - return (IsOk: false, ErrorCode: -1); + return (IsOk: false, ErrorCode: -1); } } diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/CrDownloadOptions.cs index 774ef6a..1aaed92 100644 --- a/CRD/Utils/Structs/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/CrDownloadOptions.cs @@ -125,4 +125,7 @@ public class CrDownloadOptions{ [YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)] public string? StreamEndpoint{ get; set; } + [YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)] + public string? DownloadDirPath{ get; set; } + } \ No newline at end of file diff --git a/CRD/Utils/Structs/EpisodeStructs.cs b/CRD/Utils/Structs/EpisodeStructs.cs index c1cacb3..3dc7686 100644 --- a/CRD/Utils/Structs/EpisodeStructs.cs +++ b/CRD/Utils/Structs/EpisodeStructs.cs @@ -50,7 +50,7 @@ public struct CrunchyEpisode{ [JsonProperty("availability_starts")] public DateTime? AvailabilityStarts{ get; set; } - public Images? Images{ get; set; } + public Images? Images{ get; set; } [JsonProperty("season_id")] public string SeasonId{ get; set; } @@ -84,7 +84,7 @@ public struct CrunchyEpisode{ public string Id{ get; set; } [JsonProperty("media_type")] - public MediaType? MediaType{ get; set; } + public MediaType? MediaType{ get; set; } [JsonProperty("availability_ends")] public DateTime? AvailabilityEnds{ get; set; } @@ -95,7 +95,7 @@ public struct CrunchyEpisode{ public string Playback{ get; set; } [JsonProperty("channel_id")] - public ChannelId? ChannelId{ get; set; } + public ChannelId? ChannelId{ get; set; } public string? Episode{ get; set; } @@ -143,10 +143,10 @@ public struct CrunchyEpisode{ public int? EpisodeNumber{ get; set; } [JsonProperty("season_tags")] - public List SeasonTags{ get; set; } + public List SeasonTags{ get; set; } [JsonProperty("maturity_ratings")] - public List MaturityRatings{ get; set; } + public List MaturityRatings{ get; set; } [JsonProperty("streams_link")] public string? StreamsLink{ get; set; } @@ -247,6 +247,9 @@ public class CrunchyEpMeta{ public List? SelectedDubs{ get; set; } public List? AvailableSubs{ get; set; } + + public string? DownloadPath{ get; set; } + } public class DownloadProgress{ @@ -267,4 +270,10 @@ public struct CrunchyEpMetaData{ public List? Versions{ get; set; } public bool IsSubbed{ get; set; } public bool IsDubbed{ get; set; } + +} + +public struct CrunchyRollEpisodeData{ + public string Key{ get; set; } + public EpisodeAndLanguage EpisodeAndLanguages{ get; set; } } \ No newline at end of file diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index 05f18ae..703a740 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; +using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; @@ -21,6 +22,8 @@ public partial class SeriesPageViewModel : ViewModelBase{ [ObservableProperty] public static bool _sonarrAvailable; + private IStorageProvider _storageProvider; + public SeriesPageViewModel(){ _selectedSeries = Crunchyroll.Instance.SelectedSeries; @@ -37,6 +40,38 @@ public partial class SeriesPageViewModel : ViewModelBase{ } } + + [RelayCommand] + public async Task OpenFolderDialogAsync(HistorySeason? season){ + 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 (season != null){ + season.SeasonDownloadPath = selectedFolder.Path.LocalPath; + } else{ + SelectedSeries.SeriesDownloadPath = selectedFolder.Path.LocalPath; + } + + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } + } + + public void SetStorageProvider(IStorageProvider storageProvider){ + _storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider)); + } + [RelayCommand] public void OpenSonarrPage(){ diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index e9d2d35..3889a7f 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -6,9 +6,11 @@ using System.ComponentModel; using System.Linq; using System.Net.Mime; using System.Reflection; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Platform.Storage; using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -32,13 +34,13 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _downloadChapters = true; - + [ObservableProperty] private bool _addScaledBorderAndShadow = false; - + [ObservableProperty] private ComboBoxItem _selectedScaledBorderAndShadow; - + public ObservableCollection ScaledBorderAndShadow{ get; } = new(){ new ComboBoxItem(){ Content = "ScaledBorderAndShadow: yes" }, new ComboBoxItem(){ Content = "ScaledBorderAndShadow: no" }, @@ -46,7 +48,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _muxToMp4; - + [ObservableProperty] private bool _downloadVideoForEveryDub; @@ -55,7 +57,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _history; - + [ObservableProperty] private int _leadingNumbers; @@ -91,7 +93,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private ComboBoxItem _selectedStreamEndpoint; - + [ObservableProperty] private ComboBoxItem _selectedDefaultDubLang; @@ -232,7 +234,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ new ListBoxItem(){ Content = "all" }, new ListBoxItem(){ Content = "none" }, }; - + public ObservableCollection StreamEndpoints{ get; } = new(){ new ComboBoxItem(){ Content = "web/firefox" }, new ComboBoxItem(){ Content = "console/switch" }, @@ -248,16 +250,20 @@ public partial class SettingsPageViewModel : ViewModelBase{ new ComboBoxItem(){ Content = "android/phone" }, new ComboBoxItem(){ Content = "tv/samsung" }, }; + + [ObservableProperty] + private string _downloadDirPath; private readonly FluentAvaloniaTheme _faTheme; private bool settingsLoaded; + private IStorageProvider _storageProvider; + public SettingsPageViewModel(){ var version = Assembly.GetExecutingAssembly().GetName().Version; _currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}"; - _faTheme = App.Current.Styles[0] as FluentAvaloniaTheme; foreach (var languageItem in Languages.languages){ @@ -270,6 +276,8 @@ public partial class SettingsPageViewModel : ViewModelBase{ CrDownloadOptions options = Crunchyroll.Instance.CrunOptions; + DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath; + ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null; SelectedHSLang = hsLang ?? HardSubLangList[0]; @@ -281,7 +289,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null; SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0]; - + var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList(); SelectedSubLang.Clear(); @@ -310,7 +318,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options); - + DownloadVideo = !options.Novids; DownloadAudio = !options.Noaudio; DownloadVideoForEveryDub = !options.DlVideoOnce; @@ -391,8 +399,8 @@ public partial class SettingsPageViewModel : ViewModelBase{ Crunchyroll.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + ""; Crunchyroll.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + ""; - - + + Crunchyroll.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + ""; List dubLangs = new List(); @@ -404,7 +412,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ Crunchyroll.Instance.CrunOptions.SimultaneousDownloads = SimultaneousDownloads; - + Crunchyroll.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + ""; Crunchyroll.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + ""; Crunchyroll.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + ""; @@ -458,11 +466,11 @@ public partial class SettingsPageViewModel : ViewModelBase{ if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: yes"){ return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; } - + if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: no"){ return ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo; } - + return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; } @@ -476,8 +484,8 @@ public partial class SettingsPageViewModel : ViewModelBase{ return ScaledBorderAndShadow[0]; } } - - + + private void UpdateSubAndDubString(){ if (SelectedSubLang.Count == 0){ SelectedSubs = "none"; @@ -524,6 +532,32 @@ public partial class SettingsPageViewModel : ViewModelBase{ RaisePropertyChanged(nameof(FfmpegOptions)); } + [RelayCommand] + public async Task OpenFolderDialogAsync(){ + 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}"); + Crunchyroll.Instance.CrunOptions.DownloadDirPath = selectedFolder.Path.LocalPath; + DownloadDirPath = string.IsNullOrEmpty(Crunchyroll.Instance.CrunOptions.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : Crunchyroll.Instance.CrunOptions.DownloadDirPath; + CfgManager.WriteSettingsToFile(); + } + } + + public void SetStorageProvider(IStorageProvider storageProvider){ + _storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider)); + } + partial void OnCurrentAppThemeChanged(ComboBoxItem? value){ if (value?.Content?.ToString() == "System"){ _faTheme.PreferSystemTheme = true; @@ -617,7 +651,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ partial void OnSelectedVideoQualityChanged(ComboBoxItem? value){ UpdateSettings(); } - + partial void OnHistoryChanged(bool value){ UpdateSettings(); } diff --git a/CRD/Views/MainWindow.axaml.cs b/CRD/Views/MainWindow.axaml.cs index ba6507d..d5ecd6a 100644 --- a/CRD/Views/MainWindow.axaml.cs +++ b/CRD/Views/MainWindow.axaml.cs @@ -58,10 +58,16 @@ public partial class MainWindow : AppWindow{ if (message.Refresh){ navigationStack.Pop(); var viewModel = Activator.CreateInstance(message.ViewModelType); + if (viewModel is SeriesPageViewModel){ + ((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider); + } navigationStack.Push(viewModel); nv.Content = viewModel; } else if (!message.Back && message.ViewModelType != null){ var viewModel = Activator.CreateInstance(message.ViewModelType); + if (viewModel is SeriesPageViewModel){ + ((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider); + } navigationStack.Push(viewModel); nv.Content = viewModel; } else{ @@ -119,7 +125,9 @@ public partial class MainWindow : AppWindow{ selectedNavVieItem = selectedItem; break; case "Settings": - navView.Content = Activator.CreateInstance(typeof(SettingsPageViewModel)); + var viewModel = (SettingsPageViewModel)Activator.CreateInstance(typeof(SettingsPageViewModel)); + viewModel.SetStorageProvider(StorageProvider); + navView.Content = viewModel; selectedNavVieItem = selectedItem; break; case "UpdateAvailable": diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml index b940b51..d95bd68 100644 --- a/CRD/Views/SeriesPageView.axaml +++ b/CRD/Views/SeriesPageView.axaml @@ -66,7 +66,19 @@ - Edit + Edit + + + @@ -111,10 +123,12 @@ IsVisible="{Binding $parent[ItemsControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}"> - - - @@ -171,6 +185,19 @@ + + + + + + + +