diff --git a/CRD/App.axaml b/CRD/App.axaml index 00654fb..6d777cb 100644 --- a/CRD/App.axaml +++ b/CRD/App.axaml @@ -10,6 +10,13 @@ + + 500 + 1500 + 150 + 700 + + diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs index 83443e0..f07ce4c 100644 --- a/CRD/Downloader/CalendarManager.cs +++ b/CRD/Downloader/CalendarManager.cs @@ -152,7 +152,6 @@ public class CalendarManager{ return forDate; } - CalendarWeek week = new CalendarWeek(); week.CalendarDays = new List(); @@ -170,21 +169,17 @@ public class CalendarManager{ } week.CalendarDays.Reverse(); - + var firstDayOfWeek = week.CalendarDays.First().DateTime; - var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 200,firstDayOfWeek, true); - + var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 200, firstDayOfWeek, true); + if (newEpisodesBase is{ Data.Count: > 0 }){ var newEpisodes = newEpisodesBase.Data; foreach (var crBrowseEpisode in newEpisodes){ var targetDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic; - if (targetDate.Kind == DateTimeKind.Utc){ - targetDate = targetDate.ToLocalTime(); - } - if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && (crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){ continue; @@ -218,6 +213,18 @@ public class CalendarManager{ calendarDay.CalendarEpisodes?.Add(calEpisode); } } + + foreach (var weekCalendarDay in week.CalendarDays){ + if (weekCalendarDay.CalendarEpisodes != null) + weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes + .OrderBy(e => e.DateTime) + .ThenBy(e => e.SeasonName) + .ThenBy(e => { + double parsedNumber; + return double.TryParse(e.EpisodeNumber, out parsedNumber) ? parsedNumber : double.MinValue; + }) + .ToList(); + } } diff --git a/CRD/Downloader/Crunchyroll/CRAuth.cs b/CRD/Downloader/Crunchyroll/CRAuth.cs index b239501..51ea53a 100644 --- a/CRD/Downloader/Crunchyroll/CRAuth.cs +++ b/CRD/Downloader/Crunchyroll/CRAuth.cs @@ -48,7 +48,7 @@ public class CrAuth{ } private void JsonTokenToFileAndVariable(string content){ - crunInstance.Token = JsonConvert.DeserializeObject(content, crunInstance.SettingsJsonSerializerSettings); + crunInstance.Token = Helpers.Deserialize(content, crunInstance.SettingsJsonSerializerSettings); if (crunInstance.Token != null && crunInstance.Token.expires_in != null){ diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 7d6f973..589f915 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -166,7 +166,7 @@ public class CrunchyrollManager{ if (File.Exists(CfgManager.PathCrHistory)){ var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory); if (!string.IsNullOrEmpty(decompressedJson)){ - HistoryList = JsonConvert.DeserializeObject>(decompressedJson) ?? new ObservableCollection(); + HistoryList = Helpers.Deserialize>(decompressedJson,CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ?? new ObservableCollection(); foreach (var historySeries in HistoryList){ historySeries.Init(); @@ -1611,7 +1611,7 @@ public class CrunchyrollManager{ Data = new List>>() }; - var playStream = JsonConvert.DeserializeObject(responseContent, SettingsJsonSerializerSettings); + var playStream = Helpers.Deserialize(responseContent, SettingsJsonSerializerSettings); if (playStream == null) return temppbData; if (!string.IsNullOrEmpty(playStream.Token)){ @@ -1758,7 +1758,7 @@ public class CrunchyrollManager{ showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest); if (showRequestResponse.IsOk){ - CrunchyOldChapter chapterData = JsonConvert.DeserializeObject(showRequestResponse.ResponseContent); + CrunchyOldChapter chapterData = Helpers.Deserialize(showRequestResponse.ResponseContent,SettingsJsonSerializerSettings); DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index ff39bab..e69f0bb 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -379,12 +379,22 @@ public class History(){ } } + private CrSeriesBase? cachedSeries; + private async Task RefreshSeriesData(string seriesId, HistorySeries historySeries){ - var series = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true); - if (series?.Data != null){ - historySeries.SeriesDescription = series.Data.First().Description; - historySeries.ThumbnailImageUrl = GetSeriesThumbnail(series); - historySeries.SeriesTitle = series.Data.First().Title; + if (cachedSeries == null || (cachedSeries.Data != null && cachedSeries.Data.First().Id != seriesId)){ + cachedSeries = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true); + } else{ + return; + } + + if (cachedSeries?.Data != null){ + var series = cachedSeries.Data.First(); + historySeries.SeriesDescription = series.Description; + historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries); + historySeries.SeriesTitle = series.Title; + historySeries.HistorySeriesAvailableDubLang = series.AudioLocales; + historySeries.HistorySeriesAvailableSoftSubs = series.SubtitleLocales; } } @@ -758,13 +768,13 @@ public class History(){ return highestSimilarity < 0.8 ? null : closestMatch; } - private double CalculateSimilarity(string source, string target){ + public double CalculateSimilarity(string source, string target){ int distance = LevenshteinDistance(source, target); return 1.0 - (double)distance / Math.Max(source.Length, target.Length); } - public int LevenshteinDistance(string source, string target){ + private int LevenshteinDistance(string source, string target){ if (string.IsNullOrEmpty(source)){ return string.IsNullOrEmpty(target) ? 0 : target.Length; } diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index f7c0972..e66b7c0 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Text.RegularExpressions; using System.Threading.Tasks; using CRD.Downloader; +using CRD.Downloader.Crunchyroll; using CRD.Utils.Parser.Utils; using CRD.Utils.Structs; using Newtonsoft.Json; @@ -62,7 +63,7 @@ public class HlsDownloader{ try{ Console.WriteLine("Resume data found! Trying to resume..."); string resumeFileContent = File.ReadAllText($"{fn}.resume"); - var resumeData = JsonConvert.DeserializeObject(resumeFileContent); + var resumeData = Helpers.Deserialize(resumeFileContent, null); if (resumeData != null){ if (resumeData.Total == _data.M3U8Json?.Segments.Count && diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 9905ca7..c91ff51 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -9,6 +9,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Media.Imaging; +using CRD.Utils.JsonConv; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll.Music; using Newtonsoft.Json; @@ -16,15 +17,11 @@ using Newtonsoft.Json; namespace CRD.Utils; public class Helpers{ - /// - /// Deserializes a JSON string into a specified .NET type. - /// - /// The type of the object to deserialize to. - /// The JSON string to deserialize. - /// The settings for deserialization if null default settings will be used - /// The deserialized object of type T. public static T? Deserialize(string json, JsonSerializerSettings? serializerSettings){ try{ + serializerSettings ??= new JsonSerializerSettings(); + serializerSettings.Converters.Add(new UtcToLocalTimeConverter()); + return JsonConvert.DeserializeObject(json, serializerSettings); } catch (JsonException ex){ Console.Error.WriteLine($"Error deserializing JSON: {ex.Message}"); @@ -49,32 +46,12 @@ public class Helpers{ dialogue = Regex.Replace(dialogue, @"", "{\\i0}"); dialogue = Regex.Replace(dialogue, @"", "{\\u1}"); dialogue = Regex.Replace(dialogue, @"", "{\\u0}"); - + dialogue = Regex.Replace(dialogue, @"<[^>]+>", ""); // Remove any other HTML-like tags return dialogue; } - public static string ExtractDialogue(string[] lines, int startLine){ - var dialogueBuilder = new StringBuilder(); - - for (int i = startLine; i < lines.Length && !string.IsNullOrWhiteSpace(lines[i]); i++){ - if (!lines[i].Contains("-->") && !lines[i].StartsWith("STYLE")){ - string line = lines[i].Trim(); - // Remove HTML tags and keep the inner text - line = Regex.Replace(line, @"<[^>]+>", ""); - dialogueBuilder.Append(line + "\\N"); - } - } - - // Remove the last newline character - if (dialogueBuilder.Length > 0){ - dialogueBuilder.Length -= 2; // Remove the last "\N" - } - - return dialogueBuilder.ToString(); - } - public static void OpenUrl(string url){ try{ Process.Start(new ProcessStartInfo{ @@ -423,4 +400,5 @@ public class Helpers{ return languageGroups; } + } \ No newline at end of file diff --git a/CRD/Utils/JsonConv/UtcToLocalTimeConverter.cs b/CRD/Utils/JsonConv/UtcToLocalTimeConverter.cs new file mode 100644 index 0000000..cdf9174 --- /dev/null +++ b/CRD/Utils/JsonConv/UtcToLocalTimeConverter.cs @@ -0,0 +1,19 @@ +using System; +using Newtonsoft.Json; + +namespace CRD.Utils.JsonConv; + +public class UtcToLocalTimeConverter : JsonConverter{ + public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer){ + return reader.Value switch{ + null => DateTime.MinValue, + DateTime dateTime when dateTime.Kind == DateTimeKind.Utc => dateTime.ToLocalTime(), + DateTime dateTime => dateTime, + _ => throw new JsonSerializationException($"Unexpected token parsing date. Expected DateTime, got {reader.Value.GetType()}.") + }; + } + + public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer){ + writer.WriteValue(value); + } +} \ No newline at end of file diff --git a/CRD/Utils/Sonarr/Models/SonarrSeries.cs b/CRD/Utils/Sonarr/Models/SonarrSeries.cs index f14604e..b86032c 100644 --- a/CRD/Utils/Sonarr/Models/SonarrSeries.cs +++ b/CRD/Utils/Sonarr/Models/SonarrSeries.cs @@ -129,7 +129,10 @@ public class SonarrSeries{ /// The images. /// [JsonProperty("images")] - public List Images{ get; set; } + public List? Images{ get; set; } + + [JsonIgnore] + public string ImageUrl{ get; set; } /// /// Gets or sets the type of the series. diff --git a/CRD/Utils/Sonarr/SonarrClient.cs b/CRD/Utils/Sonarr/SonarrClient.cs index 4aeaa7b..9015099 100644 --- a/CRD/Utils/Sonarr/SonarrClient.cs +++ b/CRD/Utils/Sonarr/SonarrClient.cs @@ -74,7 +74,7 @@ public class SonarrClient{ apiUrl = $"http{(properties.UseSsl ? "s" : "")}://{(!string.IsNullOrEmpty(properties.Host) ? properties.Host : "localhost")}:{properties.Port}{(properties.UrlBase ?? "")}/api"; } } - + public async Task CheckSonarrSettings(){ SetApiUrl(); @@ -109,7 +109,7 @@ public class SonarrClient{ List series = []; try{ - series = JsonConvert.DeserializeObject>(json) ?? []; + series = Helpers.Deserialize>(json,null) ?? []; } catch (Exception e){ MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e); Console.Error.WriteLine("Sonarr GetSeries error \n" + e); @@ -124,7 +124,7 @@ public class SonarrClient{ List episodes = []; try{ - episodes = JsonConvert.DeserializeObject>(json) ?? []; + episodes = Helpers.Deserialize>(json,null) ?? []; } catch (Exception e){ MainWindow.Instance.ShowError("Sonarr GetEpisodes error \n" + e); Console.Error.WriteLine("Sonarr GetEpisodes error \n" + e); @@ -138,7 +138,7 @@ public class SonarrClient{ var json = await GetJson($"/v3/episode/id={episodeId}"); var episode = new SonarrEpisode(); try{ - episode = JsonConvert.DeserializeObject(json) ?? new SonarrEpisode(); + episode = Helpers.Deserialize(json,null) ?? new SonarrEpisode(); } catch (Exception e){ MainWindow.Instance.ShowError("Sonarr GetEpisode error \n" + e); Console.Error.WriteLine("Sonarr GetEpisode error \n" + e); diff --git a/CRD/Utils/Structs/Crunchyroll/StreamLimits.cs b/CRD/Utils/Structs/Crunchyroll/StreamLimits.cs index b9401d3..134fd49 100644 --- a/CRD/Utils/Structs/Crunchyroll/StreamLimits.cs +++ b/CRD/Utils/Structs/Crunchyroll/StreamLimits.cs @@ -14,7 +14,7 @@ public class StreamError{ public static StreamError? FromJson(string json){ try{ - return JsonConvert.DeserializeObject(json); + return Helpers.Deserialize(json,null); } catch (Exception e){ Console.Error.WriteLine(e); return null; diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs index 09fbdcd..502368c 100644 --- a/CRD/Utils/Structs/History/HistorySeries.cs +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -52,6 +52,12 @@ public class HistorySeries : INotifyPropertyChanged{ [JsonProperty("history_series_add_date")] public DateTime? HistorySeriesAddDate{ get; set; } + [JsonProperty("history_series_available_soft_subs")] + public List HistorySeriesAvailableSoftSubs{ get; set; } =[]; + + [JsonProperty("history_series_available_dub_lang")] + public List HistorySeriesAvailableDubLang{ get; set; } =[]; + [JsonProperty("history_series_soft_subs_override")] public List HistorySeriesSoftSubsOverride{ get; set; } =[]; diff --git a/CRD/Utils/Updater/Updater.cs b/CRD/Utils/Updater/Updater.cs index b121821..d40fc13 100644 --- a/CRD/Utils/Updater/Updater.cs +++ b/CRD/Utils/Updater/Updater.cs @@ -49,9 +49,9 @@ public class Updater : INotifyPropertyChanged{ public async Task CheckForUpdatesAsync(){ try{ using (var client = new HttpClient()){ - client.DefaultRequestHeaders.Add("User-Agent", "C# App"); // GitHub API requires a user agent + client.DefaultRequestHeaders.Add("User-Agent", "C# App"); var response = await client.GetStringAsync(apiEndpoint); - var releaseInfo = JsonConvert.DeserializeObject(response); + var releaseInfo = Helpers.Deserialize(response,null); var latestVersion = releaseInfo.tag_name; downloadUrl = releaseInfo.assets[0].browser_download_url; @@ -63,10 +63,10 @@ public class Updater : INotifyPropertyChanged{ if (latestVersion != currentVersion){ Console.WriteLine("Update available: " + latestVersion + " - Current Version: " + currentVersion); return true; - } else{ - Console.WriteLine("No updates available."); - return false; } + + Console.WriteLine("No updates available."); + return false; } } catch (Exception e){ Console.Error.WriteLine("Failed to get Update information"); diff --git a/CRD/ViewModels/AccountPageViewModel.cs b/CRD/ViewModels/AccountPageViewModel.cs index 84ae245..bcb25b4 100644 --- a/CRD/ViewModels/AccountPageViewModel.cs +++ b/CRD/ViewModels/AccountPageViewModel.cs @@ -111,7 +111,7 @@ public partial class AccountPageViewModel : ViewModelBase{ CloseButtonText = "Close" }; - var viewModel = new ContentDialogInputLoginViewModel(dialog, this); + var viewModel = new Utils.ContentDialogInputLoginViewModel(dialog, this); dialog.Content = new ContentDialogInputLoginView(){ DataContext = viewModel }; diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index e3cc3f1..ee9e04e 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -13,7 +13,10 @@ using CRD.Utils; using CRD.Utils.Sonarr; using CRD.Utils.Structs; using CRD.Utils.Structs.History; +using CRD.ViewModels.Utils; using CRD.Views; +using CRD.Views.Utils; +using FluentAvalonia.UI.Controls; using ReactiveUI; namespace CRD.ViewModels; @@ -27,25 +30,39 @@ public partial class SeriesPageViewModel : ViewModelBase{ [ObservableProperty] public static bool _sonarrAvailable; - + + [ObservableProperty] + public static bool _sonarrConnected; + private IStorageProvider? _storageProvider; - public SeriesPageViewModel(){ - + [ObservableProperty] + private string _availableDubs; - + [ObservableProperty] + private string _availableSubs; + + public SeriesPageViewModel(){ _selectedSeries = CrunchyrollManager.Instance.SelectedSeries; if (_selectedSeries.ThumbnailImage == null){ _selectedSeries.LoadImage(); } - if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){ - SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled; + if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){ + SonarrConnected = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled; + + if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){ + SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected; + } else{ + SonarrAvailable = false; + } } else{ - SonarrAvailable = false; + SonarrConnected = SonarrAvailable = false; } - + + AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang); + AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs); } [RelayCommand] @@ -79,6 +96,44 @@ public partial class SeriesPageViewModel : ViewModelBase{ _storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider)); } + [RelayCommand] + public async Task MatchSonarrSeries_Button(){ + var dialog = new ContentDialog(){ + Title = "Sonarr Matching", + PrimaryButtonText = "Save", + CloseButtonText = "Close", + FullSizeDesired = true + }; + + var viewModel = new ContentDialogSonarrMatchViewModel(dialog, SelectedSeries.SonarrSeriesId,SelectedSeries.SeriesTitle); + dialog.Content = new ContentDialogSonarrMatchView(){ + DataContext = viewModel + }; + + var dialogResult = await dialog.ShowAsync(); + + if (dialogResult == ContentDialogResult.Primary){ + SelectedSeries.SonarrSeriesId = viewModel.CurrentSonarrSeries.Id.ToString(); + SelectedSeries.SonarrTvDbId = viewModel.CurrentSonarrSeries.TvdbId.ToString(); + SelectedSeries.SonarrSlugTitle = viewModel.CurrentSonarrSeries.TitleSlug; + + if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){ + SonarrConnected = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled; + + if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){ + SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected; + } else{ + SonarrAvailable = false; + } + } else{ + SonarrConnected = SonarrAvailable = false; + } + + UpdateData(""); + } + + } + [RelayCommand] public async Task DownloadSeasonAll(HistorySeason season){ @@ -105,13 +160,16 @@ public partial class SeriesPageViewModel : ViewModelBase{ await Task.WhenAll(downloadTasks); } - + [RelayCommand] public async Task UpdateData(string? season){ await SelectedSeries.FetchData(season); SelectedSeries.Seasons.Refresh(); + AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang); + AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs); + // MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true)); } @@ -122,6 +180,7 @@ public partial class SeriesPageViewModel : ViewModelBase{ SelectedSeries.Seasons.Remove(objectToRemove); CfgManager.UpdateHistoryFile(); } + MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true)); } diff --git a/CRD/ViewModels/ContentDialogInputLoginViewModel.cs b/CRD/ViewModels/Utils/ContentDialogInputLoginViewModel.cs similarity index 95% rename from CRD/ViewModels/ContentDialogInputLoginViewModel.cs rename to CRD/ViewModels/Utils/ContentDialogInputLoginViewModel.cs index e3ec828..b71fc3e 100644 --- a/CRD/ViewModels/ContentDialogInputLoginViewModel.cs +++ b/CRD/ViewModels/Utils/ContentDialogInputLoginViewModel.cs @@ -1,12 +1,10 @@ using System; using CommunityToolkit.Mvvm.ComponentModel; -using CRD.Downloader; using CRD.Downloader.Crunchyroll; -using CRD.Utils; using CRD.Utils.Structs; using FluentAvalonia.UI.Controls; -namespace CRD.ViewModels; +namespace CRD.ViewModels.Utils; public partial class ContentDialogInputLoginViewModel : ViewModelBase{ private readonly ContentDialog dialog; diff --git a/CRD/ViewModels/Utils/ContentDialogSonarrMatchViewModel.cs b/CRD/ViewModels/Utils/ContentDialogSonarrMatchViewModel.cs new file mode 100644 index 0000000..5cb0908 --- /dev/null +++ b/CRD/ViewModels/Utils/ContentDialogSonarrMatchViewModel.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; +using CRD.Downloader.Crunchyroll; +using CRD.Utils; +using CRD.Utils.Sonarr; +using CRD.Utils.Sonarr.Models; +using DynamicData; +using FluentAvalonia.UI.Controls; + +namespace CRD.ViewModels.Utils; + +public partial class ContentDialogSonarrMatchViewModel : ViewModelBase{ + private readonly ContentDialog dialog; + + [ObservableProperty] + private SonarrSeries _currentSonarrSeries; + + [ObservableProperty] + private Bitmap? _currentSeriesImage; + + [ObservableProperty] + private SonarrSeries _selectedItem; + + [ObservableProperty] + private ObservableCollection _sonarrSeriesList = new(); + + public ContentDialogSonarrMatchViewModel(ContentDialog dialog, string? currentSonarrId, string? seriesTitle){ + if (dialog is null){ + throw new ArgumentNullException(nameof(dialog)); + } + + this.dialog = dialog; + dialog.Closed += DialogOnClosed; + dialog.PrimaryButtonClick += SaveButton; + + CurrentSonarrSeries = SonarrClient.Instance.SonarrSeries.Find(e => e.Id.ToString() == currentSonarrId) ?? new SonarrSeries(){ Title = "No series matched" }; + + SetImageUrl(CurrentSonarrSeries); + + LoadList(seriesTitle); + } + + private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){ + dialog.PrimaryButtonClick -= SaveButton; + + } + + private void LoadList(string? title){ + var list = PopulateSeriesList(title); + SonarrSeriesList.AddRange(list); + } + + private List PopulateSeriesList(string? title){ + var seriesList = SonarrClient.Instance.SonarrSeries.ToList(); + + + if (!string.IsNullOrEmpty(title)){ + seriesList.Sort((series1, series2) => { + double similarity1 = Helpers.CalculateCosineSimilarity(series1.Title.ToLower(), title.ToLower()); + double similarity2 = Helpers.CalculateCosineSimilarity(series2.Title.ToLower(), title.ToLower()); + + return similarity2.CompareTo(similarity1); + }); + } else{ + seriesList.Sort((series1, series2) => string.Compare(series1.Title, series2.Title, StringComparison.OrdinalIgnoreCase)); + } + + seriesList = seriesList.Take(20).ToList(); + + foreach (var sonarrSeries in seriesList){ + SetImageUrl(sonarrSeries); + } + + return seriesList; + } + + private void SetImageUrl(SonarrSeries sonarrSeries){ + var properties = CrunchyrollManager.Instance.CrunOptions.SonarrProperties; + if (properties == null || sonarrSeries.Images == null){ + return; + } + + var baseUrl = ""; + baseUrl = $"http{(properties.UseSsl ? "s" : "")}://{(!string.IsNullOrEmpty(properties.Host) ? properties.Host : "localhost")}:{properties.Port}{(properties.UrlBase ?? "")}"; + + sonarrSeries.ImageUrl = baseUrl + sonarrSeries.Images.Find(e => e.CoverType == SonarrCoverType.Poster)?.Url; + } + + + partial void OnSelectedItemChanged(SonarrSeries value){ + CurrentSonarrSeries = value; + } + + private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){ + dialog.Closed -= DialogOnClosed; + } +} \ No newline at end of file diff --git a/CRD/ViewModels/ContentDialogUpdateViewModel.cs b/CRD/ViewModels/Utils/ContentDialogUpdateViewModel.cs similarity index 93% rename from CRD/ViewModels/ContentDialogUpdateViewModel.cs rename to CRD/ViewModels/Utils/ContentDialogUpdateViewModel.cs index 8afe0c5..6c59cb1 100644 --- a/CRD/ViewModels/ContentDialogUpdateViewModel.cs +++ b/CRD/ViewModels/Utils/ContentDialogUpdateViewModel.cs @@ -1,12 +1,10 @@ using System; using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; -using CRD.Downloader; -using CRD.Utils.Structs; using CRD.Utils.Updater; using FluentAvalonia.UI.Controls; -namespace CRD.ViewModels; +namespace CRD.ViewModels.Utils; public partial class ContentDialogUpdateViewModel : ViewModelBase{ private readonly ContentDialog dialog; diff --git a/CRD/Views/MainWindow.axaml.cs b/CRD/Views/MainWindow.axaml.cs index cdb7f98..7fdafcf 100644 --- a/CRD/Views/MainWindow.axaml.cs +++ b/CRD/Views/MainWindow.axaml.cs @@ -9,6 +9,7 @@ using FluentAvalonia.Core; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Windowing; using ReactiveUI; +using ContentDialogUpdateViewModel = CRD.ViewModels.Utils.ContentDialogUpdateViewModel; namespace CRD.Views; diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml index 28f9fd7..d10759a 100644 --- a/CRD/Views/SeriesPageView.axaml +++ b/CRD/Views/SeriesPageView.axaml @@ -27,464 +27,494 @@ - - + + + + + + + + + + + - + - - - - - - + + + + + + + + - - - + + + + + - + - + - + - - - - - Edit - - - - - - - - - - + + + Edit + + - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + - - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CRD/Views/Utils/ContentDialogInputLoginView.axaml b/CRD/Views/Utils/ContentDialogInputLoginView.axaml index 9847e61..d343069 100644 --- a/CRD/Views/Utils/ContentDialogInputLoginView.axaml +++ b/CRD/Views/Utils/ContentDialogInputLoginView.axaml @@ -3,7 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:CRD.ViewModels" - x:DataType="vm:ContentDialogInputLoginViewModel" + xmlns:utils="clr-namespace:CRD.ViewModels.Utils" + x:DataType="utils:ContentDialogInputLoginViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="CRD.Views.Utils.ContentDialogInputLoginView"> diff --git a/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml new file mode 100644 index 0000000..629aabe --- /dev/null +++ b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs new file mode 100644 index 0000000..3ac06fc --- /dev/null +++ b/CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace CRD.Views.Utils; + +public partial class ContentDialogSonarrMatchView : UserControl{ + public ContentDialogSonarrMatchView(){ + InitializeComponent(); + } +} \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogUpdateView.axaml b/CRD/Views/Utils/ContentDialogUpdateView.axaml index 84e94f5..edeb066 100644 --- a/CRD/Views/Utils/ContentDialogUpdateView.axaml +++ b/CRD/Views/Utils/ContentDialogUpdateView.axaml @@ -3,7 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:CRD.ViewModels" - x:DataType="vm:ContentDialogUpdateViewModel" + xmlns:utils="clr-namespace:CRD.ViewModels.Utils" + x:DataType="utils:ContentDialogUpdateViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="CRD.Views.Utils.ContentDialogUpdateView">