diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs index e3c2cba..093d342 100644 --- a/CRD/Downloader/CalendarManager.cs +++ b/CRD/Downloader/CalendarManager.cs @@ -11,8 +11,10 @@ using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.Structs; using CRD.Utils.Structs.History; +using CRD.Views; using HtmlAgilityPack; using Newtonsoft.Json; +using ReactiveUI; namespace CRD.Downloader; @@ -75,6 +77,20 @@ public class CalendarManager{ var response = await HttpClientReq.Instance.SendHttpRequest(request); + if (!response.IsOk){ + 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("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5)); + Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar."); + } else{ + Console.Error.WriteLine($"Calendar request failed"); + } + return new CalendarWeek(); + } + CalendarWeek week = new CalendarWeek(); week.CalendarDays = new List(); @@ -314,8 +330,8 @@ public class CalendarManager{ if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){ var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")]; - foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date) - .Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){ + foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date.Day == calendarEpisode.DateTime.Date.Day) + .Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID && ele.SeasonName != calendarEpisode.SeasonName))){ calendarDay.CalendarEpisodes.Add(calendarEpisode); } } diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index f298019..3144176 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -42,6 +42,24 @@ public class CrEpisode(){ return null; } + if (epsidoe is{ Total: 1, Data: not null } && + (epsidoe.Data.First().Versions ?? []) + .GroupBy(v => v.AudioLocale) + .Any(g => g.Count() > 1)){ + Console.Error.WriteLine("Episode has Duplicate Audio Locales"); + var list = (epsidoe.Data.First().Versions ?? []).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList(); + //guid for episode id + foreach (var episodeVersionse in list){ + foreach (var version in episodeVersionse){ + var checkRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{version.Guid}", HttpMethod.Get, true, true, query); + var checkResponse = await HttpClientReq.Instance.SendHttpRequest(checkRequest,true); + if (!checkResponse.IsOk){ + epsidoe.Data.First().Versions?.Remove(version); + } + } + } + } + if (epsidoe.Total == 1 && epsidoe.Data != null){ return epsidoe.Data.First(); } diff --git a/CRD/Downloader/Crunchyroll/CrMusic.cs b/CRD/Downloader/Crunchyroll/CrMusic.cs index 64a9ac0..ede28cf 100644 --- a/CRD/Downloader/Crunchyroll/CrMusic.cs +++ b/CRD/Downloader/Crunchyroll/CrMusic.cs @@ -17,7 +17,7 @@ public class CrMusic{ public async Task ParseFeaturedMusicVideoByIdAsync(string seriesId, string crLocale, bool forcedLang = false, bool updateHistory = false){ var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/featured/{seriesId}", crLocale, forcedLang); - if (musicVideos.Data is{ Count: > 0 } && updateHistory){ + if (musicVideos.Data is{ Count: > 0 } && updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){ await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); } @@ -27,7 +27,7 @@ public class CrMusic{ public async Task ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){ var musicVideo = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos"); - if (musicVideo != null && updateHistory){ + if (musicVideo != null && updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){ await crunInstance.History.UpdateWithMusicEpisodeList([musicVideo]); } @@ -39,7 +39,7 @@ public class CrMusic{ if (concert != null){ concert.EpisodeType = EpisodeType.Concert; - if (updateHistory){ + if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){ await crunInstance.History.UpdateWithMusicEpisodeList([concert]); } } @@ -50,7 +50,7 @@ public class CrMusic{ public async Task ParseArtistMusicVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){ var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang); - if (updateHistory){ + if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){ await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); } @@ -66,7 +66,7 @@ public class CrMusic{ } } - if (updateHistory){ + if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){ await crunInstance.History.UpdateWithMusicEpisodeList(concerts.Data); } @@ -97,7 +97,7 @@ public class CrMusic{ musicVideos.Data.AddRange(concerts.Data); } - if (updateHistory){ + if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){ await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); } diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 704ead7..7fb5767 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -337,6 +337,7 @@ public class CrunchyrollManager{ SkipSubMux = options.SkipSubsMux, Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", Mp4 = options.Mp4, + Mp3 = options.AudioOnlyToMp3, MuxFonts = options.MuxFonts, VideoTitle = res.VideoTitle, Novids = options.Novids, @@ -402,6 +403,7 @@ public class CrunchyrollManager{ SkipSubMux = options.SkipSubsMux, Output = fileNameAndPath, Mp4 = options.Mp4, + Mp3 = options.AudioOnlyToMp3, MuxFonts = options.MuxFonts, VideoTitle = res.VideoTitle, Novids = options.Novids, @@ -599,8 +601,10 @@ public class CrunchyrollManager{ if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ if (data.FindAll(a => a.Type == DownloadMediaType.Audio).Count > 0){ - Console.WriteLine("Mux to MP3"); - muxToMp3 = true; + if (options.Mp3){ + Console.WriteLine("Mux to MP3"); + muxToMp3 = true; + } } else{ Console.WriteLine("Skip muxing since no videos are downloaded"); return (null, false, false, ""); @@ -643,9 +647,9 @@ public class CrunchyrollManager{ var merger = new Merger(new MergerOptions{ DubLangList = options.DubLangList, SubLangList = options.SubLangList, - OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), + OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty ,Bitrate = a.bitrate}).ToList(), SkipSubMux = options.SkipSubMux, - OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), + OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty ,Bitrate = a.bitrate}).ToList(), Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}", Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput { File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(), @@ -682,7 +686,7 @@ public class CrunchyrollManager{ List notSyncedDubs =[]; - if (options is{ SyncTiming: true, DlVideoOnce: true }){ + if (options is{ SyncTiming: true, DlVideoOnce: true } && merger.options.OnlyVid.Count > 0 && merger.options.OnlyAudio.Count > 0){ crunchyEpMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = true, Percent = 100, @@ -916,7 +920,7 @@ public class CrunchyrollManager{ if (mediaGuid.Contains(':')){ mediaGuid = mediaGuid.Split(':')[1]; } - + Console.WriteLine("MediaGuid: " + mediaId); #region Chapters @@ -1154,7 +1158,7 @@ public class CrunchyrollManager{ Console.WriteLine("Downloading video..."); curStream = streams[options.Kstream - 1]; - Console.WriteLine($"Playlists URL: {curStream.Url} ({curStream.Type})"); + Console.WriteLine($"Playlists URL: {string.Join(", ",curStream.Url)} ({curStream.Type})"); } string tsFile = ""; @@ -1376,7 +1380,7 @@ public class CrunchyrollManager{ 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()); + fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray()); string onlyFileName = Path.GetFileName(fileName); int maxLength = 220; @@ -1391,7 +1395,7 @@ public class CrunchyrollManager{ if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){ titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength); - fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); + fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray()); onlyFileName = Path.GetFileName(fileName); if (onlyFileName.Length > maxLength){ @@ -1410,7 +1414,7 @@ public class CrunchyrollManager{ string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale); string tempFile = Path.Combine(FileNameManager - .ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override) + .ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override) .ToArray()); string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile); @@ -1608,7 +1612,8 @@ public class CrunchyrollManager{ Path = $"{tsFile}.video.m4s", Lang = lang, Language = lang, - IsPrimary = isPrimary + IsPrimary = isPrimary, + bitrate = chosenVideoSegments.bandwidth / 1024 }; files.Add(videoDownloadMedia); data.downloadedFiles.Add($"{tsFile}.video.m4s"); @@ -1676,7 +1681,8 @@ public class CrunchyrollManager{ Type = DownloadMediaType.Audio, Path = $"{tsFile}.audio.m4s", Lang = lang, - IsPrimary = isPrimary + IsPrimary = isPrimary, + bitrate = chosenAudioSegments.bandwidth / 1000 }); data.downloadedFiles.Add($"{tsFile}.audio.m4s"); } else{ @@ -1693,7 +1699,8 @@ public class CrunchyrollManager{ Path = $"{tsFile}.video.m4s", Lang = lang, Language = lang, - IsPrimary = isPrimary + IsPrimary = isPrimary, + bitrate = chosenVideoSegments.bandwidth / 1024 }; files.Add(videoDownloadMedia); data.downloadedFiles.Add($"{tsFile}.video.m4s"); @@ -1704,13 +1711,14 @@ public class CrunchyrollManager{ Type = DownloadMediaType.Audio, Path = $"{tsFile}.audio.m4s", Lang = lang, - IsPrimary = isPrimary + IsPrimary = isPrimary, + bitrate = chosenAudioSegments.bandwidth / 1000 }); data.downloadedFiles.Add($"{tsFile}.audio.m4s"); } } } else if (options.Novids){ - fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); + fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray()); Console.WriteLine("Downloading skipped!"); } } @@ -1718,14 +1726,14 @@ public class CrunchyrollManager{ variables.Add(new Variable("height", 360, false)); variables.Add(new Variable("width", 640, false)); - fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); + fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray()); } if (compiledChapters.Count > 0 && options is not{ Novids: true, Noaudio: true }){ try{ // Parsing and constructing the file names - fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); - var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray()); + fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray()); + var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray()); if (Path.IsPathRooted(outFile)){ tsFile = outFile; } else{ @@ -1847,7 +1855,7 @@ public class CrunchyrollManager{ Error = dlFailed, FileName = fileName.Length > 0 ? fileName : "unknown - " + Guid.NewGuid(), ErrorText = "", - VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers, options.Override).Last(), + VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).Last(), FolderPath = fileDir, TempFolderPath = tempFolderPath }; diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index 0adcdfc..84242cd 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -60,6 +60,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _muxToMp4; + + [ObservableProperty] + private bool _muxToMp3; [ObservableProperty] private bool _muxFonts; @@ -93,6 +96,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private string _fileName = ""; + + [ObservableProperty] + private string _fileNameWhitespaceSubstitute = ""; [ObservableProperty] private string _fileTitle = ""; @@ -347,11 +353,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ KeepDubsSeparate = options.KeepDubsSeperate; DownloadChapters = options.Chapters; MuxToMp4 = options.Mp4; + MuxToMp3 = options.AudioOnlyToMp3; MuxFonts = options.MuxFonts; SyncTimings = options.SyncTiming; SkipSubMux = options.SkipSubsMux; LeadingNumbers = options.Numbers; FileName = options.FileName; + FileNameWhitespaceSubstitute = options.FileNameWhitespaceSubstitute; SearchFetchFeaturedMusic = options.SearchFetchFeaturedMusic; ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null; @@ -413,11 +421,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters; CrunchyrollManager.Instance.CrunOptions.SkipMuxing = SkipMuxing; CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4; + CrunchyrollManager.Instance.CrunOptions.AudioOnlyToMp3 = MuxToMp3; 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); CrunchyrollManager.Instance.CrunOptions.FileName = FileName; + CrunchyrollManager.Instance.CrunOptions.FileNameWhitespaceSubstitute = FileNameWhitespaceSubstitute; CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 1), 1, 10000); diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml index 3f5cce5..8b54821 100644 --- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -319,6 +319,14 @@ HorizontalAlignment="Stretch" /> + + + + + + @@ -345,11 +353,17 @@ - + + + + + + + diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index 042a5e1..f840b67 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -37,8 +37,12 @@ public class History{ } } } else{ - foreach (var historyEpisode in historySeries.Seasons.First(historySeason => historySeason.SeasonId == seasonId).EpisodesList){ - historyEpisode.IsEpisodeAvailableOnStreamingService = false; + var matchingSeason = historySeries.Seasons.FirstOrDefault(historySeason => historySeason.SeasonId == seasonId); + + if (matchingSeason != null){ + foreach (var historyEpisode in matchingSeason.EpisodesList){ + historyEpisode.IsEpisodeAvailableOnStreamingService = false; + } } } } @@ -165,6 +169,7 @@ public class History{ EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), EpisodeType = historySource.GetEpisodeType(), IsEpisodeAvailableOnStreamingService = true, + ThumbnailImageUrl = historySource.GetImageUrl(), }; historySeason.EpisodesList.Add(newHistoryEpisode); @@ -179,6 +184,7 @@ public class History{ historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate(); historyEpisode.EpisodeType = historySource.GetEpisodeType(); historyEpisode.IsEpisodeAvailableOnStreamingService = true; + historyEpisode.ThumbnailImageUrl = historySource.GetImageUrl(); historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(); historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(); @@ -577,7 +583,8 @@ public class History{ HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(), EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), EpisodeType = historySource.GetEpisodeType(), - IsEpisodeAvailableOnStreamingService = true + IsEpisodeAvailableOnStreamingService = true, + ThumbnailImageUrl = historySource.GetImageUrl(), }; newSeason.EpisodesList.Add(newHistoryEpisode); @@ -634,7 +641,7 @@ public class History{ var historyEpisodesWithSonarrIds = allHistoryEpisodes .Where(e => !string.IsNullOrEmpty(e.SonarrEpisodeId)) .ToList(); - + Parallel.ForEach(historyEpisodesWithSonarrIds, historyEpisode => { var sonarrEpisode = episodes.FirstOrDefault(e => e.Id.ToString().Equals(historyEpisode.SonarrEpisodeId)); @@ -644,9 +651,9 @@ public class History{ }); var historyEpisodeIds = new HashSet(historyEpisodesWithSonarrIds.Select(e => e.SonarrEpisodeId!)); - + episodes.RemoveAll(e => historyEpisodeIds.Contains(e.Id.ToString())); - + allHistoryEpisodes = allHistoryEpisodes .Where(e => string.IsNullOrEmpty(e.SonarrEpisodeId)) .ToList(); diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs index a95b8dd..3031dcc 100644 --- a/CRD/Downloader/QueueManager.cs +++ b/CRD/Downloader/QueueManager.cs @@ -95,7 +95,7 @@ public partial class QueueManager : ObservableObject{ } - public async Task CrAddEpisodeToQueue(string epId, string crLocale, List dubLang, bool updateHistory = false, bool onlySubs = false){ + public async Task CrAddEpisodeToQueue(string epId, string crLocale, List dubLang, bool updateHistory = false, EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){ if (string.IsNullOrEmpty(epId)){ return; } @@ -158,7 +158,7 @@ public partial class QueueManager : ObservableObject{ selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; - selected.OnlySubs = onlySubs; + selected.OnlySubs = episodeDownloadMode == EpisodeDownloadMode.OnlySubs; if (CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub && selected.Data.Count > 1){ var sortedMetaData = selected.Data @@ -178,9 +178,24 @@ public partial class QueueManager : ObservableObject{ var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions); - if (selected.OnlySubs){ - newOptions.Novids = true; - newOptions.Noaudio = true; + switch (episodeDownloadMode){ + case EpisodeDownloadMode.OnlyVideo: + newOptions.Novids = false; + newOptions.Noaudio = true; + selected.DownloadSubs = ["none"]; + break; + case EpisodeDownloadMode.OnlyAudio: + newOptions.Novids = true; + newOptions.Noaudio = false; + selected.DownloadSubs = ["none"]; + break; + case EpisodeDownloadMode.OnlySubs: + newOptions.Novids = true; + newOptions.Noaudio = true; + break; + case EpisodeDownloadMode.Default: + default: + break; } newOptions.DubLang = dubLang; @@ -227,13 +242,28 @@ public partial class QueueManager : ObservableObject{ if (movieMeta != null){ movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs; - movieMeta.OnlySubs = onlySubs; + movieMeta.OnlySubs = episodeDownloadMode == EpisodeDownloadMode.OnlySubs; var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions); - if (movieMeta.OnlySubs){ - newOptions.Novids = true; - newOptions.Noaudio = true; + switch (episodeDownloadMode){ + case EpisodeDownloadMode.OnlyVideo: + newOptions.Novids = false; + newOptions.Noaudio = true; + movieMeta.DownloadSubs = ["none"]; + break; + case EpisodeDownloadMode.OnlyAudio: + newOptions.Novids = true; + newOptions.Noaudio = false; + movieMeta.DownloadSubs = ["none"]; + break; + case EpisodeDownloadMode.OnlySubs: + newOptions.Novids = true; + newOptions.Noaudio = true; + break; + case EpisodeDownloadMode.Default: + default: + break; } newOptions.DubLang = dubLang; diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index 5d76d44..d036d1a 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -251,6 +251,13 @@ public enum CrunchyUrlType{ Unknown } +public enum EpisodeDownloadMode{ + Default, + OnlyVideo, + OnlyAudio, + OnlySubs, +} + public enum SonarrCoverType{ Banner, FanArt, diff --git a/CRD/Utils/Files/FileNameManager.cs b/CRD/Utils/Files/FileNameManager.cs index b4f0eb7..f1b2f81 100644 --- a/CRD/Utils/Files/FileNameManager.cs +++ b/CRD/Utils/Files/FileNameManager.cs @@ -9,7 +9,7 @@ using CRD.Utils.Structs; namespace CRD.Utils.Files; public class FileNameManager{ - public static List ParseFileName(string input, List variables, int numbers, List @override){ + public static List ParseFileName(string input, List variables, int numbers,string whiteSpaceReplace, List @override){ Regex varRegex = new Regex(@"\${[A-Za-z1-9]+}"); var matches = varRegex.Matches(input).Cast().Select(m => m.Value).ToList(); var overriddenVars = ParseOverride(variables, @override); @@ -27,7 +27,7 @@ public class FileNameManager{ continue; } - string replacement = variable.ReplaceWith.ToString(); + string replacement = variable.ReplaceWith.ToString() ?? string.Empty; if (variable.Type == "int32"){ int len = replacement.Length; replacement = len < numbers ? new string('0', numbers - len) + replacement : replacement; @@ -38,6 +38,9 @@ public class FileNameManager{ replacement = replacement.Replace(",", "."); } else if (variable.Sanitize){ replacement = CleanupFilename(replacement); + if (variable.Type == "string" && !string.IsNullOrEmpty(whiteSpaceReplace)){ + replacement = replacement.Replace(" ",whiteSpaceReplace); + } } input = input.Replace(match, replacement); diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 49333e7..76d0354 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -131,15 +131,14 @@ public class Merger{ } - foreach (var aud in options.OnlyAudio){ - args.Add($"-i \"{aud.Path}\""); - metaData.Add($"-map {index}"); - metaData.Add($"-metadata:s:a:{audioIndex} language={aud.Language.Code}"); - index++; - audioIndex++; + if (options.OnlyAudio.Count > 1){ + Console.Error.WriteLine("Multiple audio files detected. Only one audio file can be converted to MP3 at a time."); } - args.Add("-c:a copy"); + var audio = options.OnlyAudio.First(); + + args.Add($"-i \"{audio.Path}\""); + args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : "") ); args.Add($"\"{options.Output}\""); return string.Join(" ", args); } @@ -443,6 +442,7 @@ public class MergerInput{ public int? Duration{ get; set; } public int? Delay{ get; set; } public bool? IsPrimary{ get; set; } + public int? Bitrate{ get; set; } } public class SubtitleInput{ @@ -469,6 +469,7 @@ public class CrunchyMuxOptions{ public bool? KeepAllVideos{ get; set; } public bool? Novids{ get; set; } public bool Mp4{ get; set; } + public bool Mp3{ get; set; } public bool MuxFonts{ get; set; } public bool MuxDescription{ get; set; } public string ForceMuxer{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index e0d340d..93b134d 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -155,6 +155,9 @@ public class CrDownloadOptions{ [JsonProperty("quality_audio")] public string QualityAudio{ get; set; } = ""; + [JsonProperty("file_name_whitespace_substitute")] + public string FileNameWhitespaceSubstitute{ get; set; } = ""; + [JsonProperty("file_name")] public string FileName{ get; set; } = ""; @@ -194,6 +197,9 @@ public class CrDownloadOptions{ [JsonProperty("mux_mp4")] public bool Mp4{ get; set; } + [JsonProperty("mux_audio_only_to_mp3")] + public bool AudioOnlyToMp3 { get; set; } + [JsonProperty("mux_fonts")] public bool MuxFonts{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs index 5f0bff7..9127745 100644 --- a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs @@ -289,6 +289,14 @@ public class CrunchyEpisode : IHistorySource{ public EpisodeType GetEpisodeType(){ return EpisodeType; } + + public string GetImageUrl(){ + if (Images != null){ + return Images.Thumbnail?.First().First().Source ?? string.Empty; + } + + return string.Empty; + } #endregion } diff --git a/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs index 312ab10..c0e440f 100644 --- a/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs +++ b/CRD/Utils/Structs/Crunchyroll/Music/CrMusicVideo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using CRD.Utils.Structs.History; using Newtonsoft.Json; @@ -170,6 +171,14 @@ public class CrunchyMusicVideo : IHistorySource{ public EpisodeType GetEpisodeType(){ return EpisodeType; } + + public string GetImageUrl(){ + if (Images != null){ + return Images.Thumbnail.First().Source ?? string.Empty; + } + + return string.Empty; + } #endregion } diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs index b5114c1..b4f4c64 100644 --- a/CRD/Utils/Structs/HelperClasses.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -84,7 +84,8 @@ public class DownloadedMedia : SxItem{ public DownloadMediaType Type{ get; set; } public required LanguageItem Lang{ get; set; } public bool IsPrimary{ get; set; } - + + public int bitrate{ get; set; } public bool? Cc{ get; set; } public bool? Signs{ get; set; } diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs index c41e7ca..e955d0e 100644 --- a/CRD/Utils/Structs/History/HistoryEpisode.cs +++ b/CRD/Utils/Structs/History/HistoryEpisode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; +using Avalonia.Media.Imaging; using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils.Files; @@ -34,19 +35,22 @@ public class HistoryEpisode : INotifyPropertyChanged{ [JsonProperty("episode_special_episode")] public bool SpecialEpisode{ get; set; } - + [JsonProperty("episode_available_on_streaming_service")] public bool IsEpisodeAvailableOnStreamingService{ get; set; } - + [JsonProperty("episode_type")] public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown; + [JsonProperty("episode_thumbnail_url")] + public string? ThumbnailImageUrl{ get; set; } + [JsonProperty("sonarr_episode_id")] public string? SonarrEpisodeId{ get; set; } [JsonProperty("sonarr_has_file")] public bool SonarrHasFile{ get; set; } - + [JsonProperty("sonarr_is_monitored")] public bool SonarrIsMonitored{ get; set; } @@ -70,12 +74,32 @@ public class HistoryEpisode : INotifyPropertyChanged{ return $"S{SonarrSeasonNumber}E{SonarrEpisodeNumber}"; } } - + [JsonProperty("history_episode_available_soft_subs")] public List HistoryEpisodeAvailableSoftSubs{ get; set; } =[]; [JsonProperty("history_episode_available_dub_lang")] public List HistoryEpisodeAvailableDubLang{ get; set; } =[]; + + [JsonIgnore] + public Bitmap? ThumbnailImage{ get; set; } + + [JsonIgnore] + public bool IsImageLoaded{ get; private set; } = false; + + public async Task LoadImage(){ + if (IsImageLoaded || string.IsNullOrEmpty(ThumbnailImageUrl)) + return; + + try{ + ThumbnailImage = await Helpers.LoadImage(ThumbnailImageUrl); + IsImageLoaded = true; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage))); + } catch (Exception ex){ + Console.Error.WriteLine("Failed to load image: " + ex.Message); + } + } [JsonIgnore] public string ReleaseDateFormated{ @@ -92,7 +116,7 @@ public class HistoryEpisode : INotifyPropertyChanged{ return string.Format("{0:00}.{1}.{2}", EpisodeCrPremiumAirDate.Value.Day, monthAbbreviation, EpisodeCrPremiumAirDate.Value.Year); } } - + public event PropertyChangedEventHandler? PropertyChanged; public void ToggleWasDownloaded(){ @@ -115,7 +139,11 @@ public class HistoryEpisode : INotifyPropertyChanged{ CfgManager.UpdateHistoryFile(); } - public async Task DownloadEpisode(bool onlySubs = false){ + public async Task DownloadEpisodeDefault(){ + await DownloadEpisode(); + } + + public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){ switch (EpisodeType){ case EpisodeType.MusicVideo: await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty); @@ -128,19 +156,19 @@ public class HistoryEpisode : INotifyPropertyChanged{ default: await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId ?? string.Empty, string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, - CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs); + CrunchyrollManager.Instance.CrunOptions.DubLang, false, episodeDownloadMode); break; } } - - public void AssignSonarrEpisodeData(SonarrEpisode episode) { + + public void AssignSonarrEpisodeData(SonarrEpisode episode){ SonarrEpisodeId = episode.Id.ToString(); SonarrEpisodeNumber = episode.EpisodeNumber.ToString(); SonarrHasFile = episode.HasFile; SonarrIsMonitored = episode.Monitored; SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber.ToString(); SonarrSeasonNumber = episode.SeasonNumber.ToString(); - + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SonarrSeasonEpisodeText))); } } \ No newline at end of file diff --git a/CRD/Utils/Structs/History/IHistorySource.cs b/CRD/Utils/Structs/History/IHistorySource.cs index 7e9fc00..2eb61ea 100644 --- a/CRD/Utils/Structs/History/IHistorySource.cs +++ b/CRD/Utils/Structs/History/IHistorySource.cs @@ -10,6 +10,8 @@ public interface IHistorySource{ string GetSeasonNum(); string GetSeasonId(); + string GetImageUrl(); + string GetEpisodeId(); string GetEpisodeNumber(); string GetEpisodeTitle(); diff --git a/CRD/Utils/UI/UiEnumToBoolConverter.cs b/CRD/Utils/UI/UiEnumToBoolConverter.cs new file mode 100644 index 0000000..993752d --- /dev/null +++ b/CRD/Utils/UI/UiEnumToBoolConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace CRD.Utils.UI; + +public class UiEnumToBoolConverter : IValueConverter{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture){ + if (value == null || parameter == null) + return false; + + string enumString = parameter.ToString(); + if (enumString == null) + return false; + + return value.ToString() == enumString; + } + + public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture){ + if ((bool)value && parameter != null){ + return Enum.Parse(targetType, parameter.ToString() ?? string.Empty); + } + + return Avalonia.Data.BindingOperations.DoNothing; + } +} \ No newline at end of file diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index c00b527..b2557ce 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -208,6 +208,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ QueueManager.Instance.CrAddMusicMetaToQueue(meta); } } + } else if (AddAllEpisodes){ + var musicClass = CrunchyrollManager.Instance.CrMusic; + foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){ + QueueManager.Instance.CrAddMusicMetaToQueue(meta); + } } } @@ -363,7 +368,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } private void PopulateItemsFromMusicVideoList(){ - if (currentMusicVideoList?.Data != null){ + if (currentMusicVideoList?.Data is{ Count: > 0 }){ foreach (var episode in currentMusicVideoList.Data){ string seasonKey; switch (episode.EpisodeType){ @@ -394,7 +399,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ } } - CurrentSelectedSeason = SeasonList.First(); + if (SeasonList.Count > 0){ + CurrentSelectedSeason = SeasonList.First(); + } } } diff --git a/CRD/ViewModels/CalendarPageViewModel.cs b/CRD/ViewModels/CalendarPageViewModel.cs index 67d8a8f..d491278 100644 --- a/CRD/ViewModels/CalendarPageViewModel.cs +++ b/CRD/ViewModels/CalendarPageViewModel.cs @@ -217,7 +217,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ } var refreshDate = DateTime.Now; - if (currentWeek?.FirstDayOfWeek != null){ + if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){ refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1); } @@ -239,7 +239,7 @@ public partial class CalendarPageViewModel : ViewModelBase{ } var refreshDate = DateTime.Now; - if (currentWeek?.FirstDayOfWeek != null){ + if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){ refreshDate = currentWeek.FirstDayOfWeek.AddDays(13); } diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs index 7e6cdc6..d9bef91 100644 --- a/CRD/ViewModels/HistoryPageViewModel.cs +++ b/CRD/ViewModels/HistoryPageViewModel.cs @@ -20,6 +20,7 @@ using CRD.Utils.Structs; using CRD.Utils.Structs.History; using CRD.Views; using DynamicData; +using FluentAvalonia.UI.Controls; using ReactiveUI; namespace CRD.ViewModels; @@ -115,6 +116,16 @@ public partial class HistoryPageViewModel : ViewModelBase{ [ObservableProperty] private static string _progressText; + #region Table Mode + + [ObservableProperty] + private static EpisodeDownloadMode _selectedDownloadMode = EpisodeDownloadMode.OnlySubs; + + [ObservableProperty] + public Symbol _selectedDownloadIcon = Symbol.ClosedCaption; + + #endregion + public Vector LastScrollOffset { get; set; } = Vector.Zero; public HistoryPageViewModel(){ @@ -528,6 +539,26 @@ public partial class HistoryPageViewModel : ViewModelBase{ await episode.DownloadEpisode(); } } + + [RelayCommand] + public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){ + var downloadMode = SelectedDownloadMode; + + if (downloadMode != EpisodeDownloadMode.Default){ + await episode.DownloadEpisode(downloadMode); + } + } + + [RelayCommand] + public async Task DownloadSeasonAllOnlyOptions(HistorySeason season){ + var downloadMode = SelectedDownloadMode; + + if (downloadMode != EpisodeDownloadMode.Default){ + foreach (var episode in season.EpisodesList){ + await episode.DownloadEpisode(downloadMode); + } + } + } [RelayCommand] public void ToggleDownloadedMark(SeasonDialogArgs seriesArgs){ @@ -575,6 +606,15 @@ public partial class HistoryPageViewModel : ViewModelBase{ public void ToggleInactive(){ CfgManager.UpdateHistoryFile(); } + + partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){ + SelectedDownloadIcon = SelectedDownloadMode switch{ + EpisodeDownloadMode.OnlyVideo => Symbol.Video, + EpisodeDownloadMode.OnlyAudio => Symbol.Audio, + EpisodeDownloadMode.OnlySubs => Symbol.ClosedCaption, + _ => Symbol.ClosedCaption + }; + } } public class HistoryPageProperties{ diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index dd039bc..c2df0c3 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Downloader.Crunchyroll; +using CRD.Utils; using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Utils.Structs.History; @@ -32,9 +33,18 @@ public partial class SeriesPageViewModel : ViewModelBase{ [ObservableProperty] public static bool _showMonitoredBookmark; + [ObservableProperty] + public static bool _showFeaturedMusicButton; + [ObservableProperty] public static bool _sonarrConnected; + [ObservableProperty] + private static EpisodeDownloadMode _selectedDownloadMode = EpisodeDownloadMode.OnlySubs; + + [ObservableProperty] + public Symbol _selectedDownloadIcon = Symbol.ClosedCaption; + private IStorageProvider? _storageProvider; public SeriesPageViewModel(){ @@ -63,6 +73,10 @@ public partial class SeriesPageViewModel : ViewModelBase{ } SelectedSeries.UpdateSeriesFolderPath(); + + if (SelectedSeries.SeriesStreamingService == StreamingService.Crunchyroll && SelectedSeries.SeriesType != SeriesType.Artist){ + ShowFeaturedMusicButton = true; + } } @@ -95,6 +109,33 @@ public partial class SeriesPageViewModel : ViewModelBase{ SelectedSeries.UpdateSeriesFolderPath(); } + [RelayCommand] + public async Task OpenFeaturedMusicDialog(){ + if (SelectedSeries.SeriesStreamingService != StreamingService.Crunchyroll || SelectedSeries.SeriesType == SeriesType.Artist){ + return; + } + + var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(SelectedSeries.SeriesId ?? string.Empty, + CrunchyrollManager.Instance.CrunOptions.HistoryLang ?? CrunchyrollManager.Instance.DefaultLocale, true, true); + + if (musicList is{ Data.Count: > 0 }){ + var dialog = new CustomContentDialog(){ + Title = "Featured Music", + CloseButtonText = "Close", + FullSizeDesired = true + }; + + var viewModel = new ContentDialogFeaturedMusicViewModel(dialog, musicList, CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists); + dialog.Content = new ContentDialogFeaturedMusicView(){ + DataContext = viewModel + }; + + var dialogResult = await dialog.ShowAsync(); + } else{ + MessageBus.Current.SendMessage(new ToastMessage($"No featured music found", ToastType.Warning, 3)); + } + } + [RelayCommand] public async Task MatchSonarrSeries_Button(){ var dialog = new ContentDialog(){ @@ -151,7 +192,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ if (dialogResult == ContentDialogResult.Primary){ var sonarrEpisode = viewModel.CurrentSonarrEpisode; - + foreach (var selectedSeriesSeason in SelectedSeries.Seasons){ foreach (var historyEpisode in selectedSeriesSeason.EpisodesList.Where(historyEpisode => historyEpisode.SonarrEpisodeId == sonarrEpisode.Id.ToString())){ historyEpisode.SonarrEpisodeId = string.Empty; @@ -162,7 +203,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ historyEpisode.SonarrIsMonitored = false; } } - + episode.AssignSonarrEpisodeData(sonarrEpisode); CfgManager.UpdateHistoryFile(); } @@ -190,6 +231,26 @@ public partial class SeriesPageViewModel : ViewModelBase{ } } + [RelayCommand] + public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){ + var downloadMode = SelectedDownloadMode; + + if (downloadMode != EpisodeDownloadMode.Default){ + await episode.DownloadEpisode(downloadMode); + } + } + + [RelayCommand] + public async Task DownloadSeasonAllOnlyOptions(HistorySeason season){ + var downloadMode = SelectedDownloadMode; + + if (downloadMode != EpisodeDownloadMode.Default){ + foreach (var episode in season.EpisodesList){ + await episode.DownloadEpisode(downloadMode); + } + } + } + [RelayCommand] public async Task DownloadSeasonMissingSonarr(HistorySeason season){ foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){ @@ -263,4 +324,14 @@ public partial class SeriesPageViewModel : ViewModelBase{ Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}"); } } + + + partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){ + SelectedDownloadIcon = SelectedDownloadMode switch{ + EpisodeDownloadMode.OnlyVideo => Symbol.Video, + EpisodeDownloadMode.OnlyAudio => Symbol.Audio, + EpisodeDownloadMode.OnlySubs => Symbol.ClosedCaption, + _ => Symbol.ClosedCaption + }; + } } \ No newline at end of file diff --git a/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs b/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs new file mode 100644 index 0000000..d5c1221 --- /dev/null +++ b/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CRD.Downloader; +using CRD.Downloader.Crunchyroll; +using CRD.Utils.Structs.Crunchyroll.Music; +using CRD.Utils.Structs.History; +using CRD.Utils.UI; +using DynamicData; +using FluentAvalonia.UI.Controls; + +namespace CRD.ViewModels.Utils; + +public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{ + private readonly CustomContentDialog dialog; + + [ObservableProperty] + private ObservableCollection _featuredMusicList = new(); + + [ObservableProperty] + private bool _musicInHistory; + + private CrunchyMusicVideoList featuredMusic; + + public ContentDialogFeaturedMusicViewModel(CustomContentDialog contentDialog, CrunchyMusicVideoList featuredMusic, bool crunOptionsHistoryIncludeCrArtists){ + ArgumentNullException.ThrowIfNull(contentDialog); + + this.featuredMusic = featuredMusic; + + dialog = contentDialog; + dialog.Closed += DialogOnClosed; + dialog.PrimaryButtonClick += SaveButton; + + if (crunOptionsHistoryIncludeCrArtists){ + var episodeList = featuredMusic.Data + .Select(video => + CrunchyrollManager.Instance.HistoryList + .FirstOrDefault(h => h.SeriesId == video.GetSeriesId())? + .Seasons.FirstOrDefault(s => s.SeasonId == video.GetSeasonId())? + .EpisodesList.FirstOrDefault(e => e.EpisodeId == video.GetEpisodeId())) + .Where(episode => episode != null) + .ToList(); + + if (episodeList.Count > 0){ + FeaturedMusicList.Clear(); + FeaturedMusicList!.AddRange(episodeList); + } + } else{ + List episodeList =[]; + + foreach (var crunchyMusicVideo in featuredMusic.Data){ + var newHistoryEpisode = new HistoryEpisode{ + EpisodeTitle = crunchyMusicVideo.GetEpisodeTitle(), + EpisodeDescription = crunchyMusicVideo.GetEpisodeDescription(), + EpisodeId = crunchyMusicVideo.GetEpisodeId(), + Episode = crunchyMusicVideo.GetEpisodeNumber(), + EpisodeSeasonNum = crunchyMusicVideo.GetSeasonNum(), + SpecialEpisode = crunchyMusicVideo.IsSpecialEpisode(), + HistoryEpisodeAvailableDubLang = crunchyMusicVideo.GetEpisodeAvailableDubLang(), + HistoryEpisodeAvailableSoftSubs = crunchyMusicVideo.GetEpisodeAvailableSoftSubs(), + EpisodeCrPremiumAirDate = crunchyMusicVideo.GetAvailableDate(), + EpisodeType = crunchyMusicVideo.GetEpisodeType(), + IsEpisodeAvailableOnStreamingService = true, + ThumbnailImageUrl = crunchyMusicVideo.GetImageUrl(), + }; + episodeList.Add(newHistoryEpisode); + newHistoryEpisode.LoadImage(); + } + + if (episodeList.Count > 0){ + FeaturedMusicList.Clear(); + FeaturedMusicList.AddRange(episodeList); + } + } + } + + [RelayCommand] + public void DownloadEpisode(HistoryEpisode episode){ + episode.DownloadEpisode(); + } + + private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){ + dialog.PrimaryButtonClick -= SaveButton; + } + + private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){ + dialog.Closed -= DialogOnClosed; + } +} \ No newline at end of file diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml index e083e80..3dbb7cc 100644 --- a/CRD/Views/HistoryPageView.axaml +++ b/CRD/Views/HistoryPageView.axaml @@ -13,12 +13,13 @@ Unloaded="OnUnloaded" Loaded="Control_OnLoaded"> - + + @@ -406,6 +407,7 @@ + @@ -419,7 +421,8 @@ FontSize="15" Opacity="0.8" TextWrapping="Wrap" /> - + + - + + @@ -516,7 +520,7 @@ @@ -717,27 +721,41 @@ + - - - + + + + + + + + + + + - + - + - + - + + Command="{Binding DownloadEpisodeDefault}"> @@ -817,13 +835,14 @@ @@ -933,6 +952,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -137,6 +139,19 @@ + + + Edit - + - @@ -448,13 +460,12 @@ - - + - + - + + @@ -520,8 +532,7 @@ @@ -630,6 +643,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs b/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs new file mode 100644 index 0000000..33af031 --- /dev/null +++ b/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using CRD.Utils.UI; + +namespace CRD.Views.Utils; + +public partial class ContentDialogFeaturedMusicView : UserControl{ + public ContentDialogFeaturedMusicView(){ + InitializeComponent(); + } +} \ No newline at end of file