From 9d66eb34c9275315a2e1423b62be869ae96d8adc Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:04:50 +0200 Subject: [PATCH] Add - Added Table view to history tab Add - Added Sorting to history tab Add - Added next air date to history posters Add - Added scale slider to history tab for posters Chg - Auto download always starts new downloads even when you are not on the download queue tab Chg - Small size adjustment for the text in the calendar tab Fix - Simultaneous downloads set to 1 and auto download didn't work together Fix - Finished downloads didn't resume correctly --- CRD/Downloader/CrSeries.cs | 8 +- CRD/Downloader/Crunchyroll.cs | 51 +- CRD/Downloader/History.cs | 335 ++++------- CRD/Utils/Enums/EnumCollection.cs | 15 + CRD/Utils/HLS/HLSDownloader.cs | 9 + CRD/Utils/Helpers.cs | 11 + CRD/Utils/Muxing/Merger.cs | 22 +- CRD/Utils/Structs/CrDownloadOptions.cs | 4 + CRD/Utils/Structs/History/HistoryEpisode.cs | 55 ++ CRD/Utils/Structs/History/HistorySeason.cs | 51 ++ CRD/Utils/Structs/History/HistorySeries.cs | 175 ++++++ .../UI/UiSonarrIdToVisibilityConverter.cs | 21 + CRD/ViewModels/AddDownloadPageViewModel.cs | 34 +- CRD/ViewModels/DownloadsPageViewModel.cs | 52 +- CRD/ViewModels/HistoryPageViewModel.cs | 221 +++++++ CRD/ViewModels/SeriesPageViewModel.cs | 45 +- CRD/Views/CalendarPageView.axaml | 5 +- CRD/Views/DownloadsPageView.axaml.cs | 11 - CRD/Views/HistoryPageView.axaml | 553 ++++++++++++++++-- CRD/Views/MainWindow.axaml.cs | 3 + CRD/Views/SeriesPageView.axaml | 14 +- 21 files changed, 1288 insertions(+), 407 deletions(-) create mode 100644 CRD/Utils/Structs/History/HistoryEpisode.cs create mode 100644 CRD/Utils/Structs/History/HistorySeason.cs create mode 100644 CRD/Utils/Structs/History/HistorySeries.cs create mode 100644 CRD/Utils/UI/UiSonarrIdToVisibilityConverter.cs diff --git a/CRD/Downloader/CrSeries.cs b/CRD/Downloader/CrSeries.cs index 7b74313..b28ec20 100644 --- a/CRD/Downloader/CrSeries.cs +++ b/CRD/Downloader/CrSeries.cs @@ -356,7 +356,7 @@ public class CrSeries(){ return ret; } - public async Task ParseSeriesById(string id,string? locale){ + public async Task ParseSeriesById(string id,string? locale,bool forced = false){ if (crunInstance.CmsToken?.Cms == null){ Console.Error.WriteLine("Missing CMS Access Token"); return null; @@ -366,7 +366,11 @@ public class CrSeries(){ query["preferred_audio_language"] = "ja-JP"; if (!string.IsNullOrEmpty(locale)){ - query["locale"] = Languages.Locale2language(locale).CrLocale; + query["locale"] = Languages.Locale2language(locale).CrLocale; + if (forced){ + query["force_locale"] = Languages.Locale2language(locale).CrLocale; + } + } diff --git a/CRD/Downloader/Crunchyroll.cs b/CRD/Downloader/Crunchyroll.cs index 19944d9..8624fec 100644 --- a/CRD/Downloader/Crunchyroll.cs +++ b/CRD/Downloader/Crunchyroll.cs @@ -22,6 +22,7 @@ using CRD.Utils.Muxing; using CRD.Utils.Sonarr; using CRD.Utils.Sonarr.Models; using CRD.Utils.Structs; +using CRD.Utils.Structs.History; using CRD.ViewModels; using CRD.Views; using HtmlAgilityPack; @@ -104,8 +105,9 @@ public class Crunchyroll{ public Crunchyroll(){ CrunOptions = new CrDownloadOptions(); + Queue.CollectionChanged += UpdateItemListOnRemove; } - + public async Task Init(){ _widevine = Widevine.Instance; @@ -199,6 +201,41 @@ public class Crunchyroll{ } } + private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){ + if (e.Action == NotifyCollectionChangedAction.Remove){ + if (e.OldItems != null) + foreach (var eOldItem in e.OldItems){ + var downloadItem = DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(eOldItem)); + if (downloadItem != null){ + DownloadItemModels.Remove(downloadItem); + } else{ + Console.Error.WriteLine("Failed to Remove Episode from list"); + } + } + } + + UpdateDownloadListItems(); + } + + public void UpdateDownloadListItems(){ + var list = Queue; + + foreach (CrunchyEpMeta crunchyEpMeta in list){ + var downloadItem = DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(crunchyEpMeta)); + if (downloadItem != null){ + downloadItem.Refresh(); + } else{ + downloadItem = new DownloadItemModel(crunchyEpMeta); + downloadItem.LoadImage(); + DownloadItemModels.Add(downloadItem); + } + + if (downloadItem is{ isDownloading: false, Error: false } && CrunOptions.AutoDownload && ActiveDownloads < CrunOptions.SimultaneousDownloads){ + downloadItem.StartDownload(); + } + } + } + public async Task GetCalendarForDate(string weeksMondayDate, bool forceUpdate){ if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){ @@ -447,13 +484,12 @@ public class Crunchyroll{ if (CrunOptions.RemoveFinishedDownload){ Queue.Remove(data); } - - Queue.Refresh(); } else{ Console.WriteLine("Skipping mux"); } ActiveDownloads--; + Queue.Refresh(); if (CrunOptions.History && data.Data != null && data.Data.Count > 0){ CrHistory.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId); @@ -506,7 +542,7 @@ public class Crunchyroll{ Console.Error.WriteLine("No xml description file found to mux description"); } } - + var merger = new Merger(new MergerOptions{ OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), @@ -528,7 +564,7 @@ public class Crunchyroll{ }, CcTag = options.CcTag, mp3 = muxToMp3, - MuxDescription = muxDesc + Description = data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList(), }); if (!File.Exists(CfgManager.PathFFMPEG)){ @@ -1380,6 +1416,11 @@ public class Crunchyroll{ writer.WriteEndElement(); // End Tags writer.WriteEndDocument(); } + + files.Add(new DownloadedMedia{ + Type = DownloadMediaType.Description, + Path = fullPath, + }); } Console.WriteLine($"{fileName} has been created with the description."); diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index d948301..16141b9 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -1,16 +1,21 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.Input; using CRD.Utils; using CRD.Utils.Sonarr; using CRD.Utils.Sonarr.Models; using CRD.Utils.Structs; +using CRD.Utils.Structs.History; using CRD.Views; +using DynamicData; using Newtonsoft.Json; using ReactiveUI; @@ -22,7 +27,7 @@ public class History(){ public async Task UpdateSeries(string seriesId, string? seasonId){ await crunInstance.CrAuth.RefreshToken(true); - CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja"); + CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja", true); if (parsedSeries == null){ Console.Error.WriteLine("Parse Data Invalid"); @@ -64,7 +69,7 @@ public class History(){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); if (historySeries != null){ - var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId); + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); if (historySeason != null){ var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); @@ -84,7 +89,7 @@ public class History(){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); if (historySeries != null){ - var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId); + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); if (historySeason != null){ var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); @@ -102,9 +107,9 @@ public class History(){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var downloadDirPath = ""; - + if (historySeries != null){ - var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId); + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){ downloadDirPath = historySeries.SeriesDownloadPath; } @@ -145,7 +150,7 @@ public class History(){ var seriesId = episode.SeriesId; var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); if (historySeries != null){ - var historySeason = historySeries.Seasons.Find(s => s.SeasonId == episode.SeasonId); + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == episode.SeasonId); var series = await crunInstance.CrSeries.SeriesById(seriesId); if (series?.Data != null){ @@ -174,7 +179,7 @@ public class History(){ historySeries.Seasons.Add(newSeason); - historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList(); + SortSeasons(historySeries); } historySeries.UpdateNewEpisodes(); @@ -198,11 +203,7 @@ public class History(){ historySeries.UpdateNewEpisodes(); } - var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList(); - crunInstance.HistoryList.Clear(); - foreach (var item in sortedList){ - crunInstance.HistoryList.Add(item); - } + SortItems(); MatchHistorySeriesWithSonarr(false); await MatchHistoryEpisodesWithSonarr(false, historySeries); @@ -215,7 +216,7 @@ public class History(){ var seriesId = firstEpisode.SeriesId; var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); if (historySeries != null){ - var historySeason = historySeries.Seasons.Find(s => s.SeasonId == firstEpisode.SeasonId); + var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.SeasonId); var series = await crunInstance.CrSeries.SeriesById(seriesId); if (series?.Data != null){ historySeries.SeriesTitle = series.Data.First().Title; @@ -257,7 +258,7 @@ public class History(){ historySeries.Seasons.Add(newSeason); - historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList(); + SortSeasons(historySeries); } historySeries.UpdateNewEpisodes(); @@ -286,11 +287,7 @@ public class History(){ historySeries.UpdateNewEpisodes(); } - var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList(); - crunInstance.HistoryList.Clear(); - foreach (var item in sortedList){ - crunInstance.HistoryList.Add(item); - } + SortItems(); MatchHistorySeriesWithSonarr(false); await MatchHistoryEpisodesWithSonarr(false, historySeries); @@ -298,6 +295,66 @@ public class History(){ } } + private void SortSeasons(HistorySeries series){ + var sortedSeasons = series.Seasons + .OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0) + .ToList(); + + series.Seasons.Clear(); + + foreach (var season in sortedSeasons){ + series.Seasons.Add(season); + } + } + + public void SortItems(){ + var currentSortingType = Crunchyroll.Instance.CrunOptions.HistoryPageProperties?.SelectedSorting ?? SortingType.SeriesTitle; + switch (currentSortingType){ + case SortingType.SeriesTitle: + var sortedList = Crunchyroll.Instance.HistoryList.OrderBy(s => s.SeriesTitle).ToList(); + + Crunchyroll.Instance.HistoryList.Clear(); + + Crunchyroll.Instance.HistoryList.AddRange(sortedList); + + + return; + + case SortingType.NextAirDate: + + DateTime today = DateTime.UtcNow.Date; + + var sortedSeriesDates = Crunchyroll.Instance.HistoryList + .OrderByDescending(s => s.SonarrNextAirDate == "Today") + .ThenBy(s => s.SonarrNextAirDate == "Today" ? s.SeriesTitle : null) + .ThenBy(s => { + var date = ParseDate(s.SonarrNextAirDate, today); + return date.HasValue ? date.Value : DateTime.MaxValue; + }) + .ThenBy(s => s.SeriesTitle) + .ToList(); + + Crunchyroll.Instance.HistoryList.Clear(); + + Crunchyroll.Instance.HistoryList.AddRange(sortedSeriesDates); + + + return; + } + } + + public static DateTime? ParseDate(string dateStr, DateTime today){ + if (dateStr == "Today"){ + return today; + } + + if (DateTime.TryParseExact(dateStr, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date)){ + return date; + } + + return null; + } + private string GetSeriesThumbnail(CrSeriesBase series){ // var series = await crunInstance.CrSeries.SeriesById(seriesId); @@ -389,6 +446,8 @@ public class History(){ if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); + historySeries.SonarrNextAirDate = GetNextAirDate(episodes); + List allHistoryEpisodes =[]; foreach (var historySeriesSeason in historySeries.Seasons){ @@ -451,6 +510,29 @@ public class History(){ } } + private string GetNextAirDate(List episodes){ + DateTime today = DateTime.UtcNow.Date; + + // Check if any episode air date matches today + var todayEpisode = episodes.FirstOrDefault(e => e.AirDateUtc.Date == today); + if (todayEpisode != null){ + return "Today"; + } + + // Find the next episode date + var nextEpisode = episodes + .Where(e => e.AirDateUtc.Date > today) + .OrderBy(e => e.AirDateUtc.Date) + .FirstOrDefault(); + + if (nextEpisode != null){ + return nextEpisode.AirDateUtc.ToString("dd.MM.yyyy"); + } + + // If no future episode date is found + return string.Empty; + } + private SonarrSeries? FindClosestMatch(string title){ SonarrSeries? closestMatch = null; double highestSimilarity = 0.0; @@ -543,218 +625,3 @@ public class NumericStringPropertyComparer : IComparer{ } } -public class HistorySeries : INotifyPropertyChanged{ - [JsonProperty("series_title")] - public string? SeriesTitle{ get; set; } - - [JsonProperty("series_id")] - public string? SeriesId{ get; set; } - - [JsonProperty("sonarr_series_id")] - public string? SonarrSeriesId{ get; set; } - - [JsonProperty("sonarr_tvdb_id")] - public string? SonarrTvDbId{ get; set; } - - [JsonProperty("sonarr_slug_title")] - public string? SonarrSlugTitle{ get; set; } - - [JsonProperty("series_description")] - public string? SeriesDescription{ get; set; } - - [JsonProperty("series_thumbnail_url")] - public string? ThumbnailImageUrl{ get; set; } - - [JsonProperty("series_new_episodes")] - public int NewEpisodes{ get; set; } - - [JsonIgnore] - public Bitmap? ThumbnailImage{ get; set; } - - [JsonProperty("series_season_list")] - public required List Seasons{ get; set; } - - [JsonProperty("series_download_path")] - public string? SeriesDownloadPath{ get; set; } - - public event PropertyChangedEventHandler? PropertyChanged; - - [JsonIgnore] - public bool FetchingData{ get; set; } - - public async Task LoadImage(){ - try{ - using (var client = new HttpClient()){ - var response = await client.GetAsync(ThumbnailImageUrl); - response.EnsureSuccessStatusCode(); - using (var stream = await response.Content.ReadAsStreamAsync()){ - ThumbnailImage = new Bitmap(stream); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage))); - } - } - } catch (Exception ex){ - // Handle exceptions - Console.Error.WriteLine("Failed to load image: " + ex.Message); - } - } - - public void UpdateNewEpisodes(){ - int count = 0; - bool foundWatched = false; - - // Iterate over the Seasons list from the end to the beginning - for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){ - if (Seasons[i].SpecialSeason == true){ - continue; - } - - // Iterate over the Episodes from the end to the beginning - for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){ - if (Seasons[i].EpisodesList[j].SpecialEpisode){ - continue; - } - - if (!Seasons[i].EpisodesList[j].WasDownloaded){ - count++; - } else{ - foundWatched = true; - } - } - } - - NewEpisodes = count; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); - } - - public void SetFetchingData(){ - FetchingData = true; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); - } - - public async Task AddNewMissingToDownloads(){ - bool foundWatched = false; - - // Iterate over the Seasons list from the end to the beginning - for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){ - if (Seasons[i].SpecialSeason == true){ - continue; - } - - // Iterate over the Episodes from the end to the beginning - for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){ - if (Seasons[i].EpisodesList[j].SpecialEpisode){ - continue; - } - - if (!Seasons[i].EpisodesList[j].WasDownloaded){ - //ADD to download queue - await Seasons[i].EpisodesList[j].DownloadEpisode(); - } else{ - foundWatched = true; - } - } - } - } - - public async Task FetchData(string? seasonId){ - FetchingData = true; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); - await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId); - FetchingData = false; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); - Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, this); - } -} - -public class HistorySeason : INotifyPropertyChanged{ - [JsonProperty("season_title")] - public string? SeasonTitle{ get; set; } - - [JsonProperty("season_id")] - public string? SeasonId{ get; set; } - - [JsonProperty("season_cr_season_number")] - public string? SeasonNum{ get; set; } - - [JsonProperty("season_special_season")] - public bool? SpecialSeason{ get; set; } - - [JsonIgnore] - public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}"; - - [JsonProperty("season_downloaded_episodes")] - public int DownloadedEpisodes{ get; set; } - - [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){ - if (!string.IsNullOrEmpty(EpisodeId)){ - EpisodesList.First(e => e.EpisodeId == EpisodeId).ToggleWasDownloaded(); - } - - DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); - } - - public void UpdateDownloaded(){ - DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); - } -} - -public partial class HistoryEpisode : INotifyPropertyChanged{ - [JsonProperty("episode_title")] - public string? EpisodeTitle{ get; set; } - - [JsonProperty("episode_id")] - public string? EpisodeId{ get; set; } - - [JsonProperty("episode_cr_episode_number")] - public string? Episode{ get; set; } - - [JsonProperty("episode_cr_episode_description")] - public string? EpisodeDescription{ get; set; } - - [JsonProperty("episode_cr_season_number")] - public string? EpisodeSeasonNum{ get; set; } - - [JsonProperty("episode_was_downloaded")] - public bool WasDownloaded{ get; set; } - - [JsonProperty("episode_special_episode")] - public bool SpecialEpisode{ get; set; } - - [JsonProperty("sonarr_episode_id")] - public string? SonarrEpisodeId{ get; set; } - - [JsonProperty("sonarr_has_file")] - public bool SonarrHasFile{ get; set; } - - [JsonProperty("sonarr_episode_number")] - public string? SonarrEpisodeNumber{ get; set; } - - [JsonProperty("sonarr_season_number")] - public string? SonarrSeasonNumber{ get; set; } - - [JsonProperty("sonarr_absolut_number")] - public string? SonarrAbsolutNumber{ get; set; } - - public event PropertyChangedEventHandler? PropertyChanged; - - public void ToggleWasDownloaded(){ - WasDownloaded = !WasDownloaded; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded))); - } - - public async Task DownloadEpisode(){ - await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang); - } -} \ No newline at end of file diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index 52fd597..ef01b85 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -163,6 +163,9 @@ public enum DownloadMediaType{ [EnumMember(Value = "Subtitle")] Subtitle, + + [EnumMember(Value = "Description")] + Description, } public enum ScaledBorderAndShadowSelection{ @@ -171,6 +174,18 @@ public enum ScaledBorderAndShadowSelection{ ScaledBorderAndShadowNo, } +public enum HistoryViewType{ + Posters, + Table, +} + +public enum SortingType{ + [EnumMember(Value = "Series Title")] + SeriesTitle, + [EnumMember(Value = "Next Air Date")] + NextAirDate, +} + public enum SonarrCoverType{ Banner, FanArt, diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index 379ec31..bd224e6 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -71,6 +71,15 @@ public class HlsDownloader{ _data.Offset = resumeData.Completed; _data.IsResume = true; } else{ + + if (resumeData.Total == _data.M3U8Json?.Segments.Count && + resumeData.Completed == resumeData.Total && + !double.IsNaN(resumeData.Completed)){ + + Console.WriteLine("Already finished"); + return (Ok: true, _data.Parts); + } + Console.WriteLine("Resume data is wrong!"); Console.WriteLine($"Resume: {{ total: {resumeData.Total}, dled: {resumeData.Completed} }}, " + $"Current: {{ total: {_data.M3U8Json?.Segments.Count} }}"); diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 0ad3f21..cd722ec 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -27,6 +27,17 @@ public class Helpers{ } } + public static void OpenUrl(string url){ + try{ + Process.Start(new ProcessStartInfo{ + FileName = url, + UseShellExecute = true + }); + } catch (Exception e){ + Console.Error.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}"); + } + } + public static void EnsureDirectoriesExist(string path){ // Check if the path is absolute bool isAbsolute = Path.IsPathRooted(path); diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 35e14f2..18cc43e 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -141,10 +141,12 @@ public class Merger{ foreach (var aud in options.OnlyAudio){ string trackName = aud.Language.Name; + args.Add("--audio-tracks 0"); + args.Add("--no-video"); args.Add($"--track-name 0:\"{trackName}\""); args.Add($"--language 0:{aud.Language.Code}"); - args.Add("--no-video"); - args.Add("--audio-tracks 0"); + + if (options.Defaults.Audio.Code == aud.Language.Code){ args.Add("--default-track 0"); @@ -181,7 +183,7 @@ public class Merger{ args.Add("--no-subtitles"); } - if (options.Fonts != null && options.Fonts.Count > 0){ + if (options.Fonts is{ Count: > 0 }){ foreach (var font in options.Fonts){ args.Add($"--attachment-name \"{font.Name}\""); args.Add($"--attachment-mime-type \"{font.Mime}\""); @@ -191,7 +193,7 @@ public class Merger{ args.Add("--no-attachments"); } - if (options.Chapters != null && options.Chapters.Count > 0){ + if (options.Chapters is{ Count: > 0 }){ args.Add($"--chapters \"{options.Chapters[0].Path}\""); } @@ -199,8 +201,8 @@ public class Merger{ args.Add($"--title \"{options.VideoTitle}\""); } - if (options.MuxDescription){ - args.Add($"--global-tags \"{Path.Combine(Path.GetDirectoryName(options.Output), Path.GetFileNameWithoutExtension(options.Output))}.xml\""); + if (options.Description is{ Count: > 0 }){ + args.Add($"--global-tags \"{options.Description[0].Path}\""); } @@ -242,10 +244,8 @@ public class Merger{ allMediaFiles.ForEach(file => DeleteFile(file.Path)); allMediaFiles.ForEach(file => DeleteFile(file.Path + ".resume")); - if (options.MuxDescription){ - DeleteFile(Path.Combine(Path.GetDirectoryName(options.Output), Path.GetFileNameWithoutExtension(options.Output)) + ".xml"); - } - + options.Description?.ForEach(chapter => DeleteFile(chapter.Path)); + // Delete chapter files if any options.Chapters?.ForEach(chapter => DeleteFile(chapter.Path)); @@ -319,7 +319,7 @@ public class MergerOptions{ public MuxOptions Options{ get; set; } public Defaults Defaults{ get; set; } public bool mp3{ get; set; } - public bool MuxDescription{ get; set; } + public List Description{ get; set; } = new List(); } public class MuxOptions{ diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/CrDownloadOptions.cs index 6fd4e29..3871f83 100644 --- a/CRD/Utils/Structs/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/CrDownloadOptions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using CRD.Utils.Sonarr; +using CRD.ViewModels; using YamlDotNet.Serialization; namespace CRD.Utils.Structs; @@ -143,4 +144,7 @@ public class CrDownloadOptions{ [YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)] public string? DownloadDirPath{ get; set; } + [YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)] + public HistoryPageProperties? HistoryPageProperties{ get; set; } + } \ No newline at end of file diff --git a/CRD/Utils/Structs/History/HistoryEpisode.cs b/CRD/Utils/Structs/History/HistoryEpisode.cs new file mode 100644 index 0000000..4daf690 --- /dev/null +++ b/CRD/Utils/Structs/History/HistoryEpisode.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using CRD.Downloader; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs.History; + +public class HistoryEpisode : INotifyPropertyChanged{ + [JsonProperty("episode_title")] + public string? EpisodeTitle{ get; set; } + + [JsonProperty("episode_id")] + public string? EpisodeId{ get; set; } + + [JsonProperty("episode_cr_episode_number")] + public string? Episode{ get; set; } + + [JsonProperty("episode_cr_episode_description")] + public string? EpisodeDescription{ get; set; } + + [JsonProperty("episode_cr_season_number")] + public string? EpisodeSeasonNum{ get; set; } + + [JsonProperty("episode_was_downloaded")] + public bool WasDownloaded{ get; set; } + + [JsonProperty("episode_special_episode")] + public bool SpecialEpisode{ get; set; } + + [JsonProperty("sonarr_episode_id")] + public string? SonarrEpisodeId{ get; set; } + + [JsonProperty("sonarr_has_file")] + public bool SonarrHasFile{ get; set; } + + [JsonProperty("sonarr_episode_number")] + public string? SonarrEpisodeNumber{ get; set; } + + [JsonProperty("sonarr_season_number")] + public string? SonarrSeasonNumber{ get; set; } + + [JsonProperty("sonarr_absolut_number")] + public string? SonarrAbsolutNumber{ get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + public void ToggleWasDownloaded(){ + WasDownloaded = !WasDownloaded; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded))); + } + + public async Task DownloadEpisode(){ + await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang); + } +} \ No newline at end of file diff --git a/CRD/Utils/Structs/History/HistorySeason.cs b/CRD/Utils/Structs/History/HistorySeason.cs new file mode 100644 index 0000000..1907513 --- /dev/null +++ b/CRD/Utils/Structs/History/HistorySeason.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using CRD.Downloader; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs.History; + +public class HistorySeason : INotifyPropertyChanged{ + [JsonProperty("season_title")] + public string? SeasonTitle{ get; set; } + + [JsonProperty("season_id")] + public string? SeasonId{ get; set; } + + [JsonProperty("season_cr_season_number")] + public string? SeasonNum{ get; set; } + + [JsonProperty("season_special_season")] + public bool? SpecialSeason{ get; set; } + + [JsonIgnore] + public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}"; + + [JsonProperty("season_downloaded_episodes")] + public int DownloadedEpisodes{ get; set; } + + [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){ + if (!string.IsNullOrEmpty(EpisodeId)){ + EpisodesList.First(e => e.EpisodeId == EpisodeId).ToggleWasDownloaded(); + } + + DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } + + public void UpdateDownloaded(){ + DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } +} \ No newline at end of file diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs new file mode 100644 index 0000000..c60d9f3 --- /dev/null +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Avalonia.Media.Imaging; +using CRD.Downloader; +using Newtonsoft.Json; + +namespace CRD.Utils.Structs.History; + +public class HistorySeries : INotifyPropertyChanged{ + [JsonProperty("series_title")] + public string? SeriesTitle{ get; set; } + + [JsonProperty("series_id")] + public string? SeriesId{ get; set; } + + [JsonProperty("sonarr_series_id")] + public string? SonarrSeriesId{ get; set; } + + [JsonProperty("sonarr_tvdb_id")] + public string? SonarrTvDbId{ get; set; } + + [JsonProperty("sonarr_slug_title")] + public string? SonarrSlugTitle{ get; set; } + + [JsonProperty("sonarr_next_air_date")] + public string? SonarrNextAirDate{ get; set; } + + [JsonProperty("series_description")] + public string? SeriesDescription{ get; set; } + + [JsonProperty("series_thumbnail_url")] + public string? ThumbnailImageUrl{ get; set; } + + [JsonProperty("series_new_episodes")] + public int NewEpisodes{ get; set; } + + [JsonIgnore] + public Bitmap? ThumbnailImage{ get; set; } + + [JsonProperty("series_season_list")] + public required ObservableCollection Seasons{ get; set; } + + [JsonProperty("series_download_path")] + public string? SeriesDownloadPath{ get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + [JsonIgnore] + public bool FetchingData{ get; set; } + + [JsonIgnore] + public bool EditModeEnabled{ + get => _editModeEnabled; + set{ + if (_editModeEnabled != value){ + _editModeEnabled = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditModeEnabled))); + } + } + } + + [JsonIgnore] + private bool _editModeEnabled; + + public async Task LoadImage(){ + try{ + using (var client = new HttpClient()){ + var response = await client.GetAsync(ThumbnailImageUrl); + response.EnsureSuccessStatusCode(); + using (var stream = await response.Content.ReadAsStreamAsync()){ + ThumbnailImage = new Bitmap(stream); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage))); + } + } + } catch (Exception ex){ + // Handle exceptions + Console.Error.WriteLine("Failed to load image: " + ex.Message); + } + } + + public void UpdateNewEpisodes(){ + int count = 0; + bool foundWatched = false; + + // Iterate over the Seasons list from the end to the beginning + for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){ + if (Seasons[i].SpecialSeason == true){ + continue; + } + + // Iterate over the Episodes from the end to the beginning + for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){ + if (Seasons[i].EpisodesList[j].SpecialEpisode){ + continue; + } + + if (!Seasons[i].EpisodesList[j].WasDownloaded){ + count++; + } else{ + foundWatched = true; + } + } + } + + NewEpisodes = count; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); + } + + public void SetFetchingData(){ + FetchingData = true; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); + } + + public async Task AddNewMissingToDownloads(){ + bool foundWatched = false; + + // Iterate over the Seasons list from the end to the beginning + for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){ + if (Seasons[i].SpecialSeason == true){ + continue; + } + + // Iterate over the Episodes from the end to the beginning + for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){ + if (Seasons[i].EpisodesList[j].SpecialEpisode){ + continue; + } + + if (!Seasons[i].EpisodesList[j].WasDownloaded){ + //ADD to download queue + await Seasons[i].EpisodesList[j].DownloadEpisode(); + } else{ + foundWatched = true; + } + } + } + } + + public async Task FetchData(string? seasonId){ + FetchingData = true; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); + await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId); + FetchingData = false; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); + Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, this); + UpdateNewEpisodes(); + } + + public void RemoveSeason(string? season){ + HistorySeason? objectToRemove = Seasons.FirstOrDefault(se => se.SeasonId == season) ?? null; + if (objectToRemove != null){ + Seasons.Remove(objectToRemove); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Seasons))); + } + + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } + + public void OpenSonarrPage(){ + var sonarrProp = Crunchyroll.Instance.CrunOptions.SonarrProperties; + + if (sonarrProp == null) return; + + Helpers.OpenUrl($"http{(sonarrProp.UseSsl ? "s" : "")}://{sonarrProp.Host}:{sonarrProp.Port}{(sonarrProp.UrlBase ?? "")}/series/{SonarrSlugTitle}"); + } + + public void OpenCrPage(){ + Helpers.OpenUrl($"https://www.crunchyroll.com/series/{SeriesId}"); + } +} \ No newline at end of file diff --git a/CRD/Utils/UI/UiSonarrIdToVisibilityConverter.cs b/CRD/Utils/UI/UiSonarrIdToVisibilityConverter.cs new file mode 100644 index 0000000..c0e3d4c --- /dev/null +++ b/CRD/Utils/UI/UiSonarrIdToVisibilityConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using CRD.Downloader; +using FluentAvalonia.UI.Controls; + +namespace CRD.Utils.UI; + +public class UiSonarrIdToVisibilityConverter : IValueConverter{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture){ + if (value is string stringValue){ + return Crunchyroll.Instance.CrunOptions.SonarrProperties != null && (stringValue.Length > 0 && Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled); + } + + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){ + throw new NotImplementedException("This converter only works for one-way binding"); + } +} \ No newline at end of file diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index 479bf30..bbb46f0 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -19,17 +19,30 @@ using ReactiveUI; namespace CRD.ViewModels; public partial class AddDownloadPageViewModel : ViewModelBase{ - [ObservableProperty] public string _urlInput = ""; - [ObservableProperty] public string _buttonText = "Enter Url"; - [ObservableProperty] public bool _addAllEpisodes = false; + [ObservableProperty] + public string _urlInput = ""; + + [ObservableProperty] + public string _buttonText = "Enter Url"; + + [ObservableProperty] + public bool _addAllEpisodes = false; + + [ObservableProperty] + public bool _buttonEnabled = false; + + [ObservableProperty] + public bool _allButtonEnabled = false; + + [ObservableProperty] + public bool _showLoading = false; - [ObservableProperty] public bool _buttonEnabled = false; - [ObservableProperty] public bool _allButtonEnabled = false; - [ObservableProperty] public bool _showLoading = false; public ObservableCollection Items{ get; } = new(); public ObservableCollection SelectedItems{ get; } = new(); - [ObservableProperty] public ComboBoxItem _currentSelectedSeason; + [ObservableProperty] + public ComboBoxItem _currentSelectedSeason; + public ObservableCollection SeasonList{ get; } = new(); private Dictionary> episodesBySeason = new(); @@ -79,7 +92,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (currentSeriesList != null){ Crunchyroll.Instance.AddSeriesToQueue(currentSeriesList.Value, new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, AddAllEpisodes, false, selectedEpisodes)); - } @@ -106,7 +118,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (match.Success){ var locale = match.Groups[1].Value; // Capture the locale part - var id = match.Groups[2].Value; // Capture the ID part + var id = match.Groups[2].Value; // Capture the ID part Crunchyroll.Instance.AddEpisodeToQue(id, locale, Crunchyroll.Instance.CrunOptions.DubLang); UrlInput = ""; selectedEpisodes.Clear(); @@ -122,7 +134,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ if (match.Success){ var locale = match.Groups[1].Value; // Capture the locale part - var id = match.Groups[2].Value; // Capture the ID part + var id = match.Groups[2].Value; // Capture the ID part if (id.Length != 9){ return; @@ -130,7 +142,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ ButtonEnabled = false; ShowLoading = true; - var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id,"", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); + var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id, "", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); ShowLoading = false; if (list != null){ currentSeriesList = list; diff --git a/CRD/ViewModels/DownloadsPageViewModel.cs b/CRD/ViewModels/DownloadsPageViewModel.cs index 71d058f..3a3e055 100644 --- a/CRD/ViewModels/DownloadsPageViewModel.cs +++ b/CRD/ViewModels/DownloadsPageViewModel.cs @@ -1,18 +1,13 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; -using Avalonia; using Avalonia.Media.Imaging; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; -using CRD.Utils; using CRD.Utils.Structs; namespace CRD.ViewModels; @@ -27,64 +22,23 @@ public partial class DownloadsPageViewModel : ViewModelBase{ public bool _removeFinished; public DownloadsPageViewModel(){ - UpdateListItems(); + Crunchyroll.Instance.UpdateDownloadListItems(); Items = Crunchyroll.Instance.DownloadItemModels; AutoDownload = Crunchyroll.Instance.CrunOptions.AutoDownload; RemoveFinished = Crunchyroll.Instance.CrunOptions.RemoveFinishedDownload; - Crunchyroll.Instance.Queue.CollectionChanged += UpdateItemListOnRemove; - // Items.Add(new DownloadItemModel{Title = "Test - S1E1"}); - } - - private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){ - if (e.Action == NotifyCollectionChangedAction.Remove){ - if (e.OldItems != null) - foreach (var eOldItem in e.OldItems){ - var downloadItem = Crunchyroll.Instance.DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(eOldItem)); - if (downloadItem != null){ - Crunchyroll.Instance.DownloadItemModels.Remove(downloadItem); - } else{ - Console.Error.WriteLine("Failed to Remove Episode from list"); - } - } - } - - UpdateListItems(); - } - - - public void UpdateListItems(){ - var list = Crunchyroll.Instance.Queue; - - foreach (CrunchyEpMeta crunchyEpMeta in list){ - var downloadItem = Crunchyroll.Instance.DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(crunchyEpMeta)); - if (downloadItem != null){ - downloadItem.Refresh(); - } else{ - downloadItem = new DownloadItemModel(crunchyEpMeta); - downloadItem.LoadImage(); - Crunchyroll.Instance.DownloadItemModels.Add(downloadItem); - } - - if (downloadItem is{ isDownloading: false, Error: false } && Crunchyroll.Instance.CrunOptions.AutoDownload && Crunchyroll.Instance.ActiveDownloads < Crunchyroll.Instance.CrunOptions.SimultaneousDownloads){ - downloadItem.StartDownload(); - } - } } partial void OnAutoDownloadChanged(bool value){ Crunchyroll.Instance.CrunOptions.AutoDownload = value; if (value){ - UpdateListItems(); + Crunchyroll.Instance.UpdateDownloadListItems(); } } partial void OnRemoveFinishedChanged(bool value){ Crunchyroll.Instance.CrunOptions.RemoveFinishedDownload = value; } - - public void Cleanup(){ - Crunchyroll.Instance.Queue.CollectionChanged -= UpdateItemListOnRemove; - } + } public partial class DownloadItemModel : INotifyPropertyChanged{ diff --git a/CRD/ViewModels/HistoryPageViewModel.cs b/CRD/ViewModels/HistoryPageViewModel.cs index 62714fc..4579554 100644 --- a/CRD/ViewModels/HistoryPageViewModel.cs +++ b/CRD/ViewModels/HistoryPageViewModel.cs @@ -1,11 +1,21 @@ using System; using System.Collections.ObjectModel; +using System.Globalization; using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Utils; +using CRD.Utils.Structs; +using CRD.Utils.Structs.History; using CRD.Views; +using DynamicData; using ReactiveUI; namespace CRD.ViewModels; @@ -22,9 +32,84 @@ public partial class HistoryPageViewModel : ViewModelBase{ [ObservableProperty] public static bool _editMode; + [ObservableProperty] + public double _scaleValue; + + [ObservableProperty] + public ComboBoxItem _selectedView; + + public ObservableCollection ViewsList{ get; } =[]; + + [ObservableProperty] + public ComboBoxItem _selectedSorting; + + public ObservableCollection SortingList{ get; } =[]; + + [ObservableProperty] + public double _posterWidth; + + [ObservableProperty] + public double _posterHeight; + + [ObservableProperty] + public double _posterImageWidth; + + [ObservableProperty] + public double _posterImageHeight; + + [ObservableProperty] + public double _posterTextSize; + + [ObservableProperty] + public Thickness _cornerMargin; + + private HistoryViewType currentViewType = HistoryViewType.Posters; + + [ObservableProperty] + public bool _isPosterViewSelected = false; + + [ObservableProperty] + public bool _isTableViewSelected = false; + + [ObservableProperty] + public static bool _viewSelectionOpen; + + [ObservableProperty] + public static bool _sortingSelectionOpen; + + private IStorageProvider _storageProvider; + + private SortingType currentSortingType = SortingType.NextAirDate; + public HistoryPageViewModel(){ Items = Crunchyroll.Instance.HistoryList; + HistoryPageProperties? properties = Crunchyroll.Instance.CrunOptions.HistoryPageProperties; + + currentViewType = properties?.SelectedView ?? HistoryViewType.Posters; + currentSortingType = properties?.SelectedSorting ?? SortingType.SeriesTitle; + ScaleValue = properties?.ScaleValue ?? 0.73; + + foreach (HistoryViewType viewType in Enum.GetValues(typeof(HistoryViewType))){ + var combobox = new ComboBoxItem{ Content = viewType }; + ViewsList.Add(combobox); + if (viewType == currentViewType){ + SelectedView = combobox; + } + } + + foreach (SortingType sortingType in Enum.GetValues(typeof(SortingType))){ + var combobox = new ComboBoxItem{ Content = sortingType.GetEnumMemberValue() }; + SortingList.Add(combobox); + if (sortingType == currentSortingType){ + SelectedSorting = combobox; + } + } + + IsPosterViewSelected = currentViewType == HistoryViewType.Posters; + IsTableViewSelected = currentViewType == HistoryViewType.Table; + + foreach (var historySeries in Items){ if (historySeries.ThumbnailImage == null){ historySeries.LoadImage(); @@ -32,13 +117,87 @@ public partial class HistoryPageViewModel : ViewModelBase{ historySeries.UpdateNewEpisodes(); } + + Crunchyroll.Instance.CrHistory.SortItems(); } + private void UpdateSettings(){ + if (Crunchyroll.Instance.CrunOptions.HistoryPageProperties != null){ + Crunchyroll.Instance.CrunOptions.HistoryPageProperties.ScaleValue = ScaleValue; + Crunchyroll.Instance.CrunOptions.HistoryPageProperties.SelectedView = currentViewType; + Crunchyroll.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType; + } else{ + Crunchyroll.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties(){ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType }; + } + + CfgManager.WriteSettingsToFile(); + } + + partial void OnSelectedViewChanged(ComboBoxItem value){ + if (Enum.TryParse(value.Content + "", out HistoryViewType viewType)){ + currentViewType = viewType; + IsPosterViewSelected = currentViewType == HistoryViewType.Posters; + IsTableViewSelected = currentViewType == HistoryViewType.Table; + } else{ + Console.Error.WriteLine("Invalid viewtype selected"); + } + + ViewSelectionOpen = false; + UpdateSettings(); + } + + partial void OnSelectedSortingChanged(ComboBoxItem value){ + if (TryParseEnum(value.Content + "", out var sortingType)){ + currentSortingType = sortingType; + Crunchyroll.Instance.CrHistory.SortItems(); + } else{ + Console.Error.WriteLine("Invalid viewtype selected"); + } + + SortingSelectionOpen = false; + UpdateSettings(); + } + + private bool TryParseEnum(string value, out T result) where T : struct, Enum{ + foreach (var field in typeof(T).GetFields()){ + var attribute = field.GetCustomAttribute(); + if (attribute != null && attribute.Value == value){ + result = (T)field.GetValue(null); + return true; + } + } + + result = default; + return false; + } + + + partial void OnScaleValueChanged(double value){ + double t = (ScaleValue - 0.5) / (1 - 0.5); + + PosterHeight = Math.Clamp(225 + t * (410 - 225), 225, 410); + PosterWidth = 250 * ScaleValue; + PosterImageHeight = 360 * ScaleValue; + PosterImageWidth = 240 * ScaleValue; + + + double posterTextSizeCalc = 11 + t * (15 - 11); + + PosterTextSize = Math.Clamp(posterTextSizeCalc, 11, 15); + CornerMargin = new Thickness(0, 0, Math.Clamp(3 + t * (5 - 3), 3, 5), 0); + UpdateSettings(); + } partial void OnSelectedSeriesChanged(HistorySeries value){ Crunchyroll.Instance.SelectedSeries = value; + + if (!string.IsNullOrEmpty(value.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){ + Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(true, SelectedSeries); + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } + NavToSeries(); _selectedSeries = null; } @@ -81,6 +240,7 @@ public partial class HistoryPageViewModel : ViewModelBase{ FetchingData = false; RaisePropertyChanged(nameof(FetchingData)); + Crunchyroll.Instance.CrHistory.SortItems(); } [RelayCommand] @@ -89,4 +249,65 @@ public partial class HistoryPageViewModel : ViewModelBase{ await Items[i].AddNewMissingToDownloads(); } } + + [RelayCommand] + public async Task OpenFolderDialogAsyncSeason(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; + } + + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } + } + + [RelayCommand] + public async Task OpenFolderDialogAsyncSeries(HistorySeries? series){ + if (_storageProvider == null){ + Console.Error.WriteLine("StorageProvider must be set before using the dialog."); + throw new InvalidOperationException("StorageProvider must be set before using the dialog."); + } + + + var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{ + Title = "Select Folder" + }); + + if (result.Count > 0){ + var selectedFolder = result[0]; + // Do something with the selected folder path + Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}"); + + if (series != null){ + series.SeriesDownloadPath = selectedFolder.Path.LocalPath; + } + + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + } + } + + + public void SetStorageProvider(IStorageProvider storageProvider){ + _storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider)); + } +} + +public class HistoryPageProperties(){ + public SortingType? SelectedSorting{ get; set; } + public HistoryViewType SelectedView{ get; set; } + public double? ScaleValue{ get; set; } } \ No newline at end of file diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index 703a740..1599438 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; @@ -7,6 +8,8 @@ using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Utils; using CRD.Utils.Sonarr; +using CRD.Utils.Structs; +using CRD.Utils.Structs.History; using CRD.Views; using ReactiveUI; @@ -30,14 +33,13 @@ public partial class SeriesPageViewModel : ViewModelBase{ if (_selectedSeries.ThumbnailImage == null){ _selectedSeries.LoadImage(); } - - if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){ - SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0; - Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(true,SelectedSeries); - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); - } else{ + + if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties != null){ + SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled; + }else{ SonarrAvailable = false; } + } @@ -72,23 +74,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ _storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider)); } - - [RelayCommand] - public void OpenSonarrPage(){ - var sonarrProp = Crunchyroll.Instance.CrunOptions.SonarrProperties; - - if (sonarrProp == null) return; - - OpenUrl($"http{(sonarrProp.UseSsl ? "s" : "")}://{sonarrProp.Host}:{sonarrProp.Port}{(sonarrProp.UrlBase ?? "")}/series/{SelectedSeries.SonarrSlugTitle}"); - } - - [RelayCommand] - public void OpenCrPage(){ - - OpenUrl($"https://www.crunchyroll.com/series/{SelectedSeries.SeriesId}"); - - } - + [RelayCommand] public async Task UpdateData(string? season){ await SelectedSeries.FetchData(season); @@ -98,7 +84,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ [RelayCommand] public void RemoveSeason(string? season){ - HistorySeason? objectToRemove = SelectedSeries.Seasons.Find(se => se.SeasonId == season) ?? null; + HistorySeason? objectToRemove = SelectedSeries.Seasons.FirstOrDefault(se => se.SeasonId == season) ?? null; if (objectToRemove != null){ SelectedSeries.Seasons.Remove(objectToRemove); } @@ -115,14 +101,5 @@ public partial class SeriesPageViewModel : ViewModelBase{ } - private void OpenUrl(string url){ - try{ - Process.Start(new ProcessStartInfo{ - FileName = url, - UseShellExecute = true - }); - } catch (Exception e){ - Console.Error.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}"); - } - } + } \ No newline at end of file diff --git a/CRD/Views/CalendarPageView.axaml b/CRD/Views/CalendarPageView.axaml index 4c2e918..fe091fc 100644 --- a/CRD/Views/CalendarPageView.axaml +++ b/CRD/Views/CalendarPageView.axaml @@ -124,8 +124,9 @@ - diff --git a/CRD/Views/DownloadsPageView.axaml.cs b/CRD/Views/DownloadsPageView.axaml.cs index 54c9d6f..3cec45d 100644 --- a/CRD/Views/DownloadsPageView.axaml.cs +++ b/CRD/Views/DownloadsPageView.axaml.cs @@ -10,15 +10,4 @@ public partial class DownloadsPageView : UserControl{ public DownloadsPageView(){ InitializeComponent(); } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e){ - base.OnDetachedFromVisualTree(e); - if (DataContext is DownloadsPageViewModel vm){ - vm.Cleanup(); - } - } - - private void Button_OnClick(object? sender, RoutedEventArgs e){ - // Crunchy.Instance.TestMethode(); - } } \ No newline at end of file diff --git a/CRD/Views/HistoryPageView.axaml b/CRD/Views/HistoryPageView.axaml index 0292875..e13f4df 100644 --- a/CRD/Views/HistoryPageView.axaml +++ b/CRD/Views/HistoryPageView.axaml @@ -5,28 +5,136 @@ xmlns:vm="clr-namespace:CRD.ViewModels" xmlns:ui="clr-namespace:CRD.Utils.UI" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:history="clr-namespace:CRD.Utils.Structs.History" x:DataType="vm:HistoryPageViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="CRD.Views.HistoryPageView"> + - + - + - - - - - Edit + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -35,32 +143,68 @@ - + - - + - - - - + + + + + + + - + + + + + + + + + - - - - - - - - - + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Edit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Views/MainWindow.axaml.cs b/CRD/Views/MainWindow.axaml.cs index d5ecd6a..4def1e4 100644 --- a/CRD/Views/MainWindow.axaml.cs +++ b/CRD/Views/MainWindow.axaml.cs @@ -116,6 +116,9 @@ public partial class MainWindow : AppWindow{ break; case "History": navView.Content = Activator.CreateInstance(typeof(HistoryPageViewModel)); + if ( navView.Content is HistoryPageViewModel){ + ((HistoryPageViewModel)navView.Content).SetStorageProvider(StorageProvider); + } navigationStack.Clear(); navigationStack.Push(navView.Content); selectedNavVieItem = selectedItem; diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml index d95bd68..0a1b776 100644 --- a/CRD/Views/SeriesPageView.axaml +++ b/CRD/Views/SeriesPageView.axaml @@ -4,8 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:vm="clr-namespace:CRD.ViewModels" - xmlns:ui="clr-namespace:CRD.Utils.UI" - xmlns:downloader="clr-namespace:CRD.Downloader" + xmlns:history="clr-namespace:CRD.Utils.Structs.History" x:DataType="vm:SeriesPageViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="CRD.Views.SeriesPageView"> @@ -46,7 +45,7 @@ - - + @@ -138,7 +136,7 @@