From 6aa10cb2c20a92fbdd024de778795df55368edba Mon Sep 17 00:00:00 2001
From: Elwador <75888166+Elwador@users.noreply.github.com>
Date: Thu, 15 Aug 2024 02:27:49 +0200
Subject: [PATCH] Add - Added a button to manually match Sonarr series Add -
Available dubs/subs overall to history series - not all dubs/subs are
available for every season/episode Fix - Subscription end date was in UTC,
causing the program to not recognize an active premium subscription
https://github.com/Crunchy-DL/Crunchy-Downloader/issues/74
---
CRD/App.axaml | 7 +
CRD/Downloader/CalendarManager.cs | 23 +-
CRD/Downloader/Crunchyroll/CRAuth.cs | 2 +-
.../Crunchyroll/CrunchyrollManager.cs | 6 +-
CRD/Downloader/History.cs | 24 +-
CRD/Utils/HLS/HLSDownloader.cs | 3 +-
CRD/Utils/Helpers.cs | 34 +-
CRD/Utils/JsonConv/UtcToLocalTimeConverter.cs | 19 +
CRD/Utils/Sonarr/Models/SonarrSeries.cs | 5 +-
CRD/Utils/Sonarr/SonarrClient.cs | 8 +-
CRD/Utils/Structs/Crunchyroll/StreamLimits.cs | 2 +-
CRD/Utils/Structs/History/HistorySeries.cs | 6 +
CRD/Utils/Updater/Updater.cs | 10 +-
CRD/ViewModels/AccountPageViewModel.cs | 2 +-
CRD/ViewModels/SeriesPageViewModel.cs | 77 +-
.../ContentDialogInputLoginViewModel.cs | 4 +-
.../ContentDialogSonarrMatchViewModel.cs | 101 ++
.../ContentDialogUpdateViewModel.cs | 4 +-
CRD/Views/MainWindow.axaml.cs | 1 +
CRD/Views/SeriesPageView.axaml | 892 +++++++++---------
.../Utils/ContentDialogInputLoginView.axaml | 3 +-
.../Utils/ContentDialogSonarrMatchView.axaml | 104 ++
.../ContentDialogSonarrMatchView.axaml.cs | 11 +
CRD/Views/Utils/ContentDialogUpdateView.axaml | 3 +-
24 files changed, 843 insertions(+), 508 deletions(-)
create mode 100644 CRD/Utils/JsonConv/UtcToLocalTimeConverter.cs
rename CRD/ViewModels/{ => Utils}/ContentDialogInputLoginViewModel.cs (95%)
create mode 100644 CRD/ViewModels/Utils/ContentDialogSonarrMatchViewModel.cs
rename CRD/ViewModels/{ => Utils}/ContentDialogUpdateViewModel.cs (93%)
create mode 100644 CRD/Views/Utils/ContentDialogSonarrMatchView.axaml
create mode 100644 CRD/Views/Utils/ContentDialogSonarrMatchView.axaml.cs
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">