diff --git a/CRD/Assets/crunchy_icon_round.png b/CRD/Assets/crunchy_icon_round.png new file mode 100644 index 0000000..76afd46 Binary files /dev/null and b/CRD/Assets/crunchy_icon_round.png differ diff --git a/CRD/Assets/sonarr.png b/CRD/Assets/sonarr.png new file mode 100644 index 0000000..dc7ac68 Binary files /dev/null and b/CRD/Assets/sonarr.png differ diff --git a/CRD/Assets/sonarr_inactive.png b/CRD/Assets/sonarr_inactive.png new file mode 100644 index 0000000..24c8b3d Binary files /dev/null and b/CRD/Assets/sonarr_inactive.png differ diff --git a/CRD/Downloader/CRAuth.cs b/CRD/Downloader/CRAuth.cs index 244551b..4202951 100644 --- a/CRD/Downloader/CRAuth.cs +++ b/CRD/Downloader/CRAuth.cs @@ -12,7 +12,10 @@ using YamlDotNet.Core.Tokens; namespace CRD.Downloader; -public class CrAuth(Crunchyroll crunInstance){ +public class CrAuth{ + + private readonly Crunchyroll crunInstance = Crunchyroll.Instance; + public async Task AuthAnonymous(){ var formData = new Dictionary{ { "grant_type", "client_id" }, diff --git a/CRD/Downloader/CrEpisode.cs b/CRD/Downloader/CrEpisode.cs index fe5897c..7e4d098 100644 --- a/CRD/Downloader/CrEpisode.cs +++ b/CRD/Downloader/CrEpisode.cs @@ -12,7 +12,10 @@ using Newtonsoft.Json; namespace CRD.Downloader; -public class CrEpisode(Crunchyroll crunInstance){ +public class CrEpisode(){ + + private readonly Crunchyroll crunInstance = Crunchyroll.Instance; + public async Task ParseEpisodeById(string id,string locale){ if (crunInstance.CmsToken?.Cms == null){ Console.WriteLine("Missing CMS Access Token"); diff --git a/CRD/Downloader/CrSeries.cs b/CRD/Downloader/CrSeries.cs index 82aabb5..f6f34b9 100644 --- a/CRD/Downloader/CrSeries.cs +++ b/CRD/Downloader/CrSeries.cs @@ -13,7 +13,10 @@ using Newtonsoft.Json; namespace CRD.Downloader; -public class CrSeries(Crunchyroll crunInstance){ +public class CrSeries(){ + + private readonly Crunchyroll crunInstance = Crunchyroll.Instance; + public async Task> DownloadFromSeriesId(string id, CrunchyMultiDownload data){ var series = await ListSeriesId(id, "" ,data); @@ -353,7 +356,7 @@ public class CrSeries(Crunchyroll crunInstance){ } NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); - + query["preferred_audio_language"] = "ja-JP"; if (!string.IsNullOrEmpty(locale)){ query["locale"] = Languages.Locale2language(locale).CrLocale; diff --git a/CRD/Downloader/Crunchyroll.cs b/CRD/Downloader/Crunchyroll.cs index 0511b61..42e610f 100644 --- a/CRD/Downloader/Crunchyroll.cs +++ b/CRD/Downloader/Crunchyroll.cs @@ -17,6 +17,8 @@ using CRD.Utils.CustomList; using CRD.Utils.DRM; using CRD.Utils.HLS; using CRD.Utils.Muxing; +using CRD.Utils.Sonarr; +using CRD.Utils.Sonarr.Models; using CRD.Utils.Structs; using CRD.ViewModels; using CRD.Views; @@ -62,6 +64,8 @@ public class Crunchyroll{ Seasons =[] }; + public List SonarrSeries =[]; + #endregion public string DefaultLocale = "en"; @@ -101,10 +105,10 @@ public class Crunchyroll{ public async Task Init(){ _widevine = Widevine.Instance; - CrAuth = new CrAuth(Instance); - CrEpisode = new CrEpisode(Instance); - CrSeries = new CrSeries(Instance); - CrHistory = new History(Instance); + CrAuth = new CrAuth(); + CrEpisode = new CrEpisode(); + CrSeries = new CrSeries(); + CrHistory = new History(); Profile = new CrProfile{ Username = "???", @@ -146,6 +150,7 @@ public class Crunchyroll{ CrunOptions.Theme = "System"; CrunOptions.SelectedCalendarLanguage = "de"; CrunOptions.DlVideoOnce = true; + CrunOptions.UseNonDrmStreams = true; CrunOptions.History = true; @@ -155,6 +160,8 @@ public class Crunchyroll{ if (File.Exists(CfgManager.PathCrHistory)){ HistoryList = JsonConvert.DeserializeObject>(File.ReadAllText(CfgManager.PathCrHistory)) ??[]; } + + RefreshSonarr(); } @@ -173,16 +180,14 @@ public class Crunchyroll{ }; } - // public async void TestMethode(){ - // // One Pice - GRMG8ZQZR - // // Studio - G9VHN9QWQ - // var episodesMeta = await DownloadFromSeriesId("G9VHN9QWQ", new CrunchyMultiDownload(Crunchy.Instance.CrunOptions.dubLang, true)); - // - // - // foreach (var crunchyEpMeta in episodesMeta){ - // await DownloadEpisode(crunchyEpMeta, CrunOptions, false); - // } - // } + public async void RefreshSonarr(){ + if (CrunOptions.SonarrProperties != null && !string.IsNullOrEmpty(CrunOptions.SonarrProperties.ApiKey)){ + SonarrClient.Instance.SetApiUrl(); + SonarrSeries = await SonarrClient.Instance.GetSeries(); + CrHistory.MatchHistorySeriesWithSonarr(true); + } + } + public async Task GetCalendarForDate(string weeksMondayDate, bool forceUpdate){ if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){ diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index fbf91a3..df2fedb 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; using CRD.Utils; +using CRD.Utils.Sonarr; +using CRD.Utils.Sonarr.Models; using CRD.Utils.Structs; using CRD.Views; using Newtonsoft.Json; @@ -17,11 +16,13 @@ using ReactiveUI; namespace CRD.Downloader; -public class History(Crunchyroll crunInstance){ +public class History(){ + private readonly Crunchyroll crunInstance = Crunchyroll.Instance; + 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"); if (parsedSeries == null){ Console.WriteLine("Parse Data Invalid"); @@ -43,11 +44,12 @@ public class History(Crunchyroll crunInstance){ if (sVersion.Guid != null){ sId = sVersion.Guid; } + break; } } } - + var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId); UpdateWithSeasonData(seasonData); } @@ -103,12 +105,22 @@ public class History(Crunchyroll crunInstance){ if (historySeries != null){ var historySeason = historySeries.Seasons.Find(s => s.SeasonId == episode.SeasonId); + var series = await crunInstance.CrSeries.SeriesById(seriesId); + if (series?.Data != null){ + historySeries.SeriesTitle = series.Data.First().Title; + } + if (historySeason != null){ + historySeason.SeasonTitle = episode.SeasonTitle; + historySeason.SeasonNum = episode.SeasonNumber + ""; if (historySeason.EpisodesList.All(e => e.EpisodeId != episode.Id)){ var newHistoryEpisode = new HistoryEpisode{ - EpisodeTitle = episode.Title, + EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title, + EpisodeDescription = episode.Description, EpisodeId = episode.Id, Episode = episode.Episode, + EpisodeSeasonNum = episode.SeasonNumber + "", + SpecialEpisode = !int.TryParse(episode.Episode, out _), }; historySeason.EpisodesList.Add(newHistoryEpisode); @@ -122,6 +134,7 @@ public class History(Crunchyroll crunInstance){ historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList(); } + historySeries.UpdateNewEpisodes(); } else{ var newHistorySeries = new HistorySeries{ @@ -136,6 +149,7 @@ public class History(Crunchyroll crunInstance){ if (series?.Data != null){ newHistorySeries.SeriesDescription = series.Data.First().Description; newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series); + newHistorySeries.SeriesTitle = series.Data.First().Title; } newHistorySeries.Seasons.Add(newSeason); @@ -148,6 +162,7 @@ public class History(Crunchyroll crunInstance){ crunInstance.HistoryList.Add(item); } + MatchHistorySeriesWithSonarr(false); UpdateHistoryFile(); } @@ -158,27 +173,38 @@ public class History(Crunchyroll crunInstance){ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); if (historySeries != null){ var historySeason = historySeries.Seasons.Find(s => s.SeasonId == firstEpisode.SeasonId); + var series = await crunInstance.CrSeries.SeriesById(seriesId); + if (series?.Data != null){ + historySeries.SeriesTitle = series.Data.First().Title; + } if (historySeason != null){ + historySeason.SeasonTitle = firstEpisode.SeasonTitle; + historySeason.SeasonNum = firstEpisode.SeasonNumber + ""; foreach (var crunchyEpisode in seasonData.Data){ - var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id); - + if (historyEpisode == null){ var newHistoryEpisode = new HistoryEpisode{ - EpisodeTitle = crunchyEpisode.Title, + EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title, + EpisodeDescription = crunchyEpisode.Description, EpisodeId = crunchyEpisode.Id, Episode = crunchyEpisode.Episode, + EpisodeSeasonNum = crunchyEpisode.SeasonNumber + "", SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), }; historySeason.EpisodesList.Add(newHistoryEpisode); } else{ //Update existing episode - historyEpisode.EpisodeTitle = crunchyEpisode.Title; + historyEpisode.EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title; historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _); + historyEpisode.EpisodeDescription = crunchyEpisode.Description; + historyEpisode.EpisodeId = crunchyEpisode.Id; + historyEpisode.Episode = crunchyEpisode.Episode; + historyEpisode.EpisodeSeasonNum = crunchyEpisode.SeasonNumber + ""; + } - } historySeason.EpisodesList.Sort(new NumericStringPropertyComparer()); @@ -191,6 +217,7 @@ public class History(Crunchyroll crunInstance){ historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList(); } + historySeries.UpdateNewEpisodes(); } else{ var newHistorySeries = new HistorySeries{ @@ -208,11 +235,12 @@ public class History(Crunchyroll crunInstance){ if (series?.Data != null){ newHistorySeries.SeriesDescription = series.Data.First().Description; newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series); + newHistorySeries.SeriesTitle = series.Data.First().Title; } newHistorySeries.Seasons.Add(newSeason); - + newHistorySeries.UpdateNewEpisodes(); } } @@ -223,6 +251,7 @@ public class History(Crunchyroll crunInstance){ crunInstance.HistoryList.Add(item); } + MatchHistorySeriesWithSonarr(false); UpdateHistoryFile(); } @@ -255,9 +284,11 @@ public class History(Crunchyroll crunInstance){ foreach (var crunchyEpisode in seasonData.Data!){ var newHistoryEpisode = new HistoryEpisode{ - EpisodeTitle = crunchyEpisode.Title, + EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title, + EpisodeDescription = crunchyEpisode.Description, EpisodeId = crunchyEpisode.Id, Episode = crunchyEpisode.Episode, + EpisodeSeasonNum = firstEpisode.SeasonNumber + "", SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), }; @@ -276,9 +307,11 @@ public class History(Crunchyroll crunInstance){ }; var newHistoryEpisode = new HistoryEpisode{ - EpisodeTitle = episode.Title, + EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title, + EpisodeDescription = episode.Description, EpisodeId = episode.Id, Episode = episode.Episode, + EpisodeSeasonNum = episode.SeasonNumber + "", SpecialEpisode = !int.TryParse(episode.Episode, out _), }; @@ -287,6 +320,170 @@ public class History(Crunchyroll crunInstance){ return newSeason; } + + public void MatchHistorySeriesWithSonarr(bool updateAll){ + foreach (var historySeries in crunInstance.HistoryList){ + if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ + var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle); + if (sonarrSeries != null){ + historySeries.SonarrSeriesId = sonarrSeries.Id + ""; + historySeries.SonarrTvDbId = sonarrSeries.TvdbId + ""; + historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug; + } + } + } + } + + public async void MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){ + if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ + var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); + + List allHistoryEpisodes =[]; + + foreach (var historySeriesSeason in historySeries.Seasons){ + allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList); + } + + List failedEpisodes =[]; + + foreach (var historyEpisode in allHistoryEpisodes){ + if (updateAll || string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){ + var episode = FindClosestMatchEpisodes(episodes, historyEpisode.EpisodeTitle); + if (episode != null){ + historyEpisode.SonarrEpisodeId = episode.Id + ""; + historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + ""; + historyEpisode.SonarrHasFile = episode.HasFile; + historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; + historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; + episodes.Remove(episode); + } else{ + failedEpisodes.Add(historyEpisode); + } + } + } + + foreach (var historyEpisode in failedEpisodes){ + var episode = episodes.Find(ele => ele.EpisodeNumber + "" == historyEpisode.Episode && ele.SeasonNumber + "" == historyEpisode.EpisodeSeasonNum); + if (episode != null){ + historyEpisode.SonarrEpisodeId = episode.Id + ""; + historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + ""; + historyEpisode.SonarrHasFile = episode.HasFile; + historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; + historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; + episodes.Remove(episode); + } else{ + var episode1 = episodes.Find(ele => !string.IsNullOrEmpty(historyEpisode.EpisodeDescription) && !string.IsNullOrEmpty(ele.Overview) && Helpers.CalculateCosineSimilarity(ele.Overview, historyEpisode.EpisodeDescription) > 0.8); + + if (episode1 != null){ + historyEpisode.SonarrEpisodeId = episode1.Id + ""; + historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + ""; + historyEpisode.SonarrHasFile = episode1.HasFile; + historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + ""; + historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + ""; + episodes.Remove(episode1); + } else{ + var episode2 = episodes.Find(ele => ele.AbsoluteEpisodeNumber + "" == historyEpisode.Episode); + if (episode2 != null){ + historyEpisode.SonarrEpisodeId = episode2.Id + ""; + historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + ""; + historyEpisode.SonarrHasFile = episode2.HasFile; + historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + ""; + historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + ""; + episodes.Remove(episode2); + } else{ + Console.WriteLine("Could not match episode to sonarr episode"); + } + } + } + + } + + + + } + } + + private SonarrSeries? FindClosestMatch(string title){ + SonarrSeries? closestMatch = null; + double highestSimilarity = 0.0; + + Parallel.ForEach(crunInstance.SonarrSeries, series => { + double similarity = CalculateSimilarity(series.Title, title); + if (similarity > highestSimilarity){ + highestSimilarity = similarity; + closestMatch = series; + } + }); + + return highestSimilarity < 0.8 ? null : closestMatch; + } + + public SonarrEpisode? FindClosestMatchEpisodes(List episodeList, string title){ + SonarrEpisode? closestMatch = null; + double highestSimilarity = 0.0; + object lockObject = new object(); // To synchronize access to shared variables + + Parallel.ForEach(episodeList, episode => { + double similarity = CalculateSimilarity(episode.Title, title); + lock (lockObject) // Ensure thread-safe access to shared variables + { + if (similarity > highestSimilarity){ + highestSimilarity = similarity; + closestMatch = episode; + } + } + }); + + return highestSimilarity < 0.8 ? null : closestMatch; + } + + private double CalculateSimilarity(string source, string target){ + int distance = LevenshteinDistance(source, target); + return 1.0 - (double)distance / Math.Max(source.Length, target.Length); + } + + private int LevenshteinDistance(string source, string target){ + if (string.IsNullOrEmpty(source)){ + return string.IsNullOrEmpty(target) ? 0 : target.Length; + } + + if (string.IsNullOrEmpty(target)){ + return source.Length; + } + + int n = source.Length; + int m = target.Length; + + // Create two work arrays of integer distances. + int[] previousDistances = new int[m + 1]; + int[] currentDistances = new int[m + 1]; + + // Initialize the previous distance array. + for (int j = 0; j <= m; j++){ + previousDistances[j] = j; + } + + for (int i = 1; i <= n; i++){ + // Initialize the current distance array. + currentDistances[0] = i; + + for (int j = 1; j <= m; j++){ + int cost = (target[j - 1] == source[i - 1]) ? 0 : 1; + + currentDistances[j] = Math.Min( + Math.Min(currentDistances[j - 1] + 1, previousDistances[j] + 1), + previousDistances[j - 1] + cost); + } + + // Swap the arrays for the next iteration. + var temp = previousDistances; + previousDistances = currentDistances; + currentDistances = temp; + } + + // The final distance is in the previous distance array. + return previousDistances[m]; + } } public class NumericStringPropertyComparer : IComparer{ @@ -294,7 +491,7 @@ public class NumericStringPropertyComparer : IComparer{ if (int.TryParse(x.Episode, out int xInt) && int.TryParse(y.Episode, out int yInt)){ return xInt.CompareTo(yInt); } - + // Fall back to string comparison if not parseable as integers return String.Compare(x.Episode, y.Episode, StringComparison.Ordinal); } @@ -307,6 +504,15 @@ public class HistorySeries : INotifyPropertyChanged{ [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; } @@ -346,18 +552,16 @@ public class HistorySeries : INotifyPropertyChanged{ // 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{ @@ -365,27 +569,26 @@ public class HistorySeries : INotifyPropertyChanged{ } } } + NewEpisodes = count; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); } - + 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(); @@ -410,9 +613,10 @@ public class HistorySeason : INotifyPropertyChanged{ [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}"; @@ -450,15 +654,36 @@ public partial class HistoryEpisode : INotifyPropertyChanged{ [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; } + 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))); @@ -466,6 +691,5 @@ public partial class HistoryEpisode : INotifyPropertyChanged{ 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 a2671f3..e18a745 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -8,25 +8,62 @@ namespace CRD.Utils; [DataContract] [JsonConverter(typeof(LocaleConverter))] public enum Locale{ - [EnumMember(Value = "")] DefaulT, - [EnumMember(Value = "un")] Unknown, - [EnumMember(Value = "en-US")] EnUs, - [EnumMember(Value = "es-LA")] EsLa, - [EnumMember(Value = "es-419")] Es419, - [EnumMember(Value = "es-ES")] EsEs, - [EnumMember(Value = "pt-BR")] PtBr, - [EnumMember(Value = "fr-FR")] FrFr, - [EnumMember(Value = "de-DE")] DeDe, - [EnumMember(Value = "ar-ME")] ArMe, - [EnumMember(Value = "ar-SA")] ArSa, - [EnumMember(Value = "it-IT")] ItIt, - [EnumMember(Value = "ru-RU")] RuRu, - [EnumMember(Value = "tr-TR")] TrTr, - [EnumMember(Value = "hi-IN")] HiIn, - [EnumMember(Value = "zh-CN")] ZhCn, - [EnumMember(Value = "ko-KR")] KoKr, - [EnumMember(Value = "ja-JP")] JaJp, - [EnumMember(Value = "id-ID")] IdId, + [EnumMember(Value = "")] + DefaulT, + + [EnumMember(Value = "un")] + Unknown, + + [EnumMember(Value = "en-US")] + EnUs, + + [EnumMember(Value = "es-LA")] + EsLa, + + [EnumMember(Value = "es-419")] + Es419, + + [EnumMember(Value = "es-ES")] + EsEs, + + [EnumMember(Value = "pt-BR")] + PtBr, + + [EnumMember(Value = "fr-FR")] + FrFr, + + [EnumMember(Value = "de-DE")] + DeDe, + + [EnumMember(Value = "ar-ME")] + ArMe, + + [EnumMember(Value = "ar-SA")] + ArSa, + + [EnumMember(Value = "it-IT")] + ItIt, + + [EnumMember(Value = "ru-RU")] + RuRu, + + [EnumMember(Value = "tr-TR")] + TrTr, + + [EnumMember(Value = "hi-IN")] + HiIn, + + [EnumMember(Value = "zh-CN")] + ZhCn, + + [EnumMember(Value = "ko-KR")] + KoKr, + + [EnumMember(Value = "ja-JP")] + JaJp, + + [EnumMember(Value = "id-ID")] + IdId, } public static class EnumExtensions{ @@ -49,34 +86,67 @@ public static class EnumExtensions{ [DataContract] public enum ChannelId{ - [EnumMember(Value = "crunchyroll")] Crunchyroll, + [EnumMember(Value = "crunchyroll")] + Crunchyroll, } [DataContract] public enum ImageType{ - [EnumMember(Value = "poster_tall")] PosterTall, + [EnumMember(Value = "poster_tall")] + PosterTall, - [EnumMember(Value = "poster_wide")] PosterWide, + [EnumMember(Value = "poster_wide")] + PosterWide, - [EnumMember(Value = "promo_image")] PromoImage, + [EnumMember(Value = "promo_image")] + PromoImage, - [EnumMember(Value = "thumbnail")] Thumbnail, + [EnumMember(Value = "thumbnail")] + Thumbnail, } [DataContract] public enum MaturityRating{ - [EnumMember(Value = "TV-14")] Tv14, + [EnumMember(Value = "TV-14")] + Tv14, } [DataContract] public enum MediaType{ - [EnumMember(Value = "episode")] Episode, + [EnumMember(Value = "episode")] + Episode, } [DataContract] public enum DownloadMediaType{ - [EnumMember(Value = "Video")] Video, - [EnumMember(Value = "Audio")] Audio, - [EnumMember(Value = "Chapters")] Chapters, - [EnumMember(Value = "Subtitle")] Subtitle, -} \ No newline at end of file + [EnumMember(Value = "Video")] + Video, + + [EnumMember(Value = "Audio")] + Audio, + + [EnumMember(Value = "Chapters")] + Chapters, + + [EnumMember(Value = "Subtitle")] + Subtitle, +} + +public enum SonarrCoverType{ + Banner, + FanArt, + Poster, + ClearLogo, +} + +public enum SonarrSeriesType{ + Anime, + Standard, + Daily +} + +public enum SonarrStatus{ + Continuing, + Upcoming, + Ended +}; \ No newline at end of file diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index d7ef619..1c25349 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -142,6 +142,7 @@ public class CfgManager{ Crunchyroll.Instance.CrunOptions.AccentColor = loadedOptions.AccentColor; Crunchyroll.Instance.CrunOptions.History = loadedOptions.History; Crunchyroll.Instance.CrunOptions.UseNonDrmStreams = loadedOptions.UseNonDrmStreams; + Crunchyroll.Instance.CrunOptions.SonarrProperties = loadedOptions.SonarrProperties; } private static object fileLock = new object(); diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index edfd4bc..f71ac05 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.Serialization; using System.Threading.Tasks; using Newtonsoft.Json; @@ -14,9 +16,9 @@ public class Helpers{ /// 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){ + public static T? Deserialize(string json, JsonSerializerSettings? serializerSettings){ try{ - return JsonConvert.DeserializeObject(json,serializerSettings); + return JsonConvert.DeserializeObject(json, serializerSettings); } catch (JsonException ex){ Console.WriteLine($"Error deserializing JSON: {ex.Message}"); throw; @@ -77,4 +79,42 @@ public class Helpers{ return (IsOk: isSuccess, ErrorCode: process.ExitCode); } } + + public static double CalculateCosineSimilarity(string text1, string text2){ + var vector1 = ComputeWordFrequency(text1); + var vector2 = ComputeWordFrequency(text2); + + return CosineSimilarity(vector1, vector2); + } + + private static Dictionary ComputeWordFrequency(string text){ + var wordFrequency = new Dictionary(); + var words = text.Split(new[]{ ' ', ',', '.', ';', ':', '-', '_', '\'' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var word in words){ + var lowerWord = word.ToLower(); + if (!wordFrequency.ContainsKey(lowerWord)){ + wordFrequency[lowerWord] = 0; + } + + wordFrequency[lowerWord]++; + } + + return wordFrequency; + } + + private static double CosineSimilarity(Dictionary vector1, Dictionary vector2){ + var intersection = vector1.Keys.Intersect(vector2.Keys); + + double dotProduct = intersection.Sum(term => vector1[term] * vector2[term]); + double normA = Math.Sqrt(vector1.Values.Sum(val => val * val)); + double normB = Math.Sqrt(vector2.Values.Sum(val => val * val)); + + if (normA == 0 || normB == 0){ + // If either vector has zero length, return 0 similarity. + return 0; + } + + return dotProduct / (normA * normB); + } } \ No newline at end of file diff --git a/CRD/Utils/Sonarr/Models/SonarrEpisode.cs b/CRD/Utils/Sonarr/Models/SonarrEpisode.cs new file mode 100644 index 0000000..da60c98 --- /dev/null +++ b/CRD/Utils/Sonarr/Models/SonarrEpisode.cs @@ -0,0 +1,141 @@ +using System; +using Newtonsoft.Json; + +namespace CRD.Utils.Sonarr.Models; + +public class SonarrEpisode{ + /// + /// Gets or sets the series identifier. + /// + /// + /// The series identifier. + /// + [JsonProperty("seriesId")] + public int SeriesId{ get; set; } + + /// + /// Gets or sets the episode file identifier. + /// + /// + /// The episode file identifier. + /// + [JsonProperty("episodeFileId")] + public int EpisodeFileId{ get; set; } + + /// + /// Gets or sets the season number. + /// + /// + /// The season number. + /// + [JsonProperty("seasonNumber")] + public int SeasonNumber{ get; set; } + + /// + /// Gets or sets the episode number. + /// + /// + /// The episode number. + /// + [JsonProperty("episodeNumber")] + public int EpisodeNumber{ get; set; } + + /// + /// Gets or sets the title. + /// + /// + /// The title. + /// + [JsonProperty("title")] + public string Title{ get; set; } + + /// + /// Gets or sets the air date. + /// + /// + /// The air date. + /// + [JsonProperty("airDate")] + public DateTimeOffset AirDate{ get; set; } + + /// + /// Gets or sets the air date UTC. + /// + /// + /// The air date UTC. + /// + [JsonProperty("airDateUtc")] + public DateTimeOffset AirDateUtc{ get; set; } + + /// + /// Gets or sets the overview. + /// + /// + /// The overview. + /// + [JsonProperty("overview")] + public string Overview{ get; set; } + + /// + /// Gets or sets a value indicating whether this instance has file. + /// + /// + /// true if this instance has file; otherwise, false. + /// + [JsonProperty("hasFile")] + public bool HasFile{ get; set; } + + /// + /// Gets or sets a value indicating whether this is monitored. + /// + /// + /// true if monitored; otherwise, false. + /// + [JsonProperty("monitored")] + public bool Monitored{ get; set; } + + /// + /// Gets or sets the scene episode number. + /// + /// + /// The scene episode number. + /// + [JsonProperty("sceneEpisodeNumber")] + public int SceneEpisodeNumber{ get; set; } + + /// + /// Gets or sets the scene season number. + /// + /// + /// The scene season number. + /// + [JsonProperty("sceneSeasonNumber")] + public int SceneSeasonNumber{ get; set; } + + /// + /// Gets or sets the tv database episode identifier. + /// + /// + /// The tv database episode identifier. + /// + [JsonProperty("tvDbEpisodeId")] + public int TvDbEpisodeId{ get; set; } + + /// + /// Gets or sets the absolute episode number. + /// + /// + /// The absolute episode number. + /// + [JsonProperty("absoluteEpisodeNumber")] + public int AbsoluteEpisodeNumber{ get; set; } + + /// + /// Gets or sets the identifier. + /// + /// + /// The identifier. + /// + [JsonProperty("id")] + public int Id{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Sonarr/Models/SonarrImage.cs b/CRD/Utils/Sonarr/Models/SonarrImage.cs new file mode 100644 index 0000000..0a6b7f9 --- /dev/null +++ b/CRD/Utils/Sonarr/Models/SonarrImage.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace CRD.Utils.Sonarr.Models; + +public class SonarrImage{ + /// + /// Gets or sets the type of the cover. + /// + /// + /// The type of the cover. + /// + [JsonProperty("coverType")] public SonarrCoverType CoverType { get; set; } + + /// + /// Gets or sets the URL. + /// + /// + /// The URL. + /// + [JsonProperty("url")] public string Url { get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Sonarr/Models/SonarrQualityProfile.cs b/CRD/Utils/Sonarr/Models/SonarrQualityProfile.cs new file mode 100644 index 0000000..429ea26 --- /dev/null +++ b/CRD/Utils/Sonarr/Models/SonarrQualityProfile.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using YamlDotNet.Core.Tokens; + +namespace CRD.Utils.Sonarr.Models; + +public class SonarrQualityProfile{ + + [JsonProperty("value")] + public Value Value{ get; set; } + + + [JsonProperty("isLoaded")] + public bool IsLoaded{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Sonarr/Models/SonarrSeason.cs b/CRD/Utils/Sonarr/Models/SonarrSeason.cs new file mode 100644 index 0000000..1de64b0 --- /dev/null +++ b/CRD/Utils/Sonarr/Models/SonarrSeason.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CRD.Utils.Sonarr.Models; + +public class SonarrSeason{ + /// + /// Gets or sets the season number. + /// + /// + /// The season number. + /// + [JsonProperty("seasonNumber")] public int SeasonNumber { get; set; } + + /// + /// Gets or sets a value indicating whether this is monitored. + /// + /// + /// true if monitored; otherwise, false. + /// + [JsonProperty("monitored")] public bool Monitored { get; set; } + + /// + /// Gets or sets the statistics. + /// + /// + /// The statistics. + /// + [JsonProperty("statistics")] public SonarrStatistics Statistics { get; set; } + + /// + /// Gets or sets the images. + /// + /// + /// The images. + /// + [JsonProperty("images")] public List Images { get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Sonarr/Models/SonarrSeries.cs b/CRD/Utils/Sonarr/Models/SonarrSeries.cs new file mode 100644 index 0000000..f14604e --- /dev/null +++ b/CRD/Utils/Sonarr/Models/SonarrSeries.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CRD.Utils.Sonarr.Models; + +public class SonarrSeries{ + /// + /// Gets or sets the TVDB identifier. + /// + /// + /// The TVDB identifier. + /// + [JsonProperty("tvdbId")] + public int TvdbId{ get; set; } + + /// + /// Gets or sets the tv rage identifier. + /// + /// + /// The tv rage identifier. + /// + [JsonProperty("tvRageId")] + public long TvRageId{ get; set; } + + /// + /// Gets or sets the imdb identifier. + /// + /// + /// The imdb identifier. + /// + [JsonProperty("imdbId")] + public string ImdbId{ get; set; } + + /// + /// Gets or sets the title. + /// + /// + /// The title. + /// + [JsonProperty("title")] + public string Title{ get; set; } + + /// + /// Gets or sets the clean title. + /// + /// + /// The clean title. + /// + [JsonProperty("cleanTitle")] + public string CleanTitle{ get; set; } + + /// + /// Gets or sets the status. + /// + /// + /// The status. + /// + [JsonProperty("status")] + public SonarrStatus Status{ get; set; } + + /// + /// Gets or sets the overview. + /// + /// + /// The overview. + /// + [JsonProperty("overview")] + public string Overview{ get; set; } + + /// + /// Gets or sets the air time. + /// + /// + /// The air time. + /// + [JsonProperty("airTime")] + public string AirTime{ get; set; } + + /// + /// Gets or sets a value indicating whether this is monitored. + /// + /// + /// true if monitored; otherwise, false. + /// + [JsonProperty("monitored")] + public bool Monitored{ get; set; } + + /// + /// Gets or sets the quality profile identifier. + /// + /// + /// The quality profile identifier. + /// + [JsonProperty("qualityProfileId")] + public long QualityProfileId{ get; set; } + + /// + /// Gets or sets a value indicating whether [season folder]. + /// + /// + /// true if [season folder]; otherwise, false. + /// + [JsonProperty("seasonFolder")] + public bool SeasonFolder{ get; set; } + + /// + /// Gets or sets the last information synchronize. + /// + /// + /// The last information synchronize. + /// + [JsonProperty("lastInfoSync")] + public DateTimeOffset LastInfoSync{ get; set; } + + /// + /// Gets or sets the runtime. + /// + /// + /// The runtime. + /// + [JsonProperty("runtime")] + public long Runtime{ get; set; } + + /// + /// Gets or sets the images. + /// + /// + /// The images. + /// + [JsonProperty("images")] + public List Images{ get; set; } + + /// + /// Gets or sets the type of the series. + /// + /// + /// The type of the series. + /// + [JsonProperty("seriesType")] + public SonarrSeriesType SeriesType{ get; set; } + + /// + /// Gets or sets the network. + /// + /// + /// The network. + /// + [JsonProperty("network")] + public string Network{ get; set; } + + /// + /// Gets or sets a value indicating whether [use scene numbering]. + /// + /// + /// true if [use scene numbering]; otherwise, false. + /// + [JsonProperty("useSceneNumbering")] + public bool UseSceneNumbering{ get; set; } + + /// + /// Gets or sets the title slug. + /// + /// + /// The title slug. + /// + [JsonProperty("titleSlug")] + public string TitleSlug{ get; set; } + + /// + /// Gets or sets the path. + /// + /// + /// The path. + /// + [JsonProperty("path")] + public string Path{ get; set; } + + /// + /// Gets or sets the year. + /// + /// + /// The year. + /// + [JsonProperty("year")] + public int Year{ get; set; } + + /// + /// Gets or sets the first aired. + /// + /// + /// The first aired. + /// + [JsonProperty("firstAired")] + public DateTimeOffset FirstAired{ get; set; } + + /// + /// Gets or sets the quality profile. + /// + /// + /// The quality profile. + /// + [JsonProperty("qualityProfile")] + public SonarrQualityProfile QualityProfile{ get; set; } + + /// + /// Gets or sets the seasons. + /// + /// + /// The seasons. + /// + [JsonProperty("seasons")] + public List Seasons{ get; set; } + + /// + /// Gets or sets the identifier. + /// + /// + /// The identifier. + /// + [JsonProperty("id")] + public int Id{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Sonarr/Models/SonarrStatistics.cs b/CRD/Utils/Sonarr/Models/SonarrStatistics.cs new file mode 100644 index 0000000..108bd4a --- /dev/null +++ b/CRD/Utils/Sonarr/Models/SonarrStatistics.cs @@ -0,0 +1,60 @@ +using System; +using Newtonsoft.Json; + +namespace CRD.Utils.Sonarr.Models; + +public class SonarrStatistics{ + /// + /// Gets or sets the previous airing. + /// + /// + /// The previous airing. + /// + [JsonProperty("previousAiring")] + public DateTimeOffset PreviousAiring{ get; set; } + + /// + /// Gets or sets the episode file count. + /// + /// + /// The episode file count. + /// + [JsonProperty("episodeFileCount")] + public int EpisodeFileCount{ get; set; } + + /// + /// Gets or sets the episode count. + /// + /// + /// The episode count. + /// + [JsonProperty("episodeCount")] + public int EpisodeCount{ get; set; } + + /// + /// Gets or sets the total episode count. + /// + /// + /// The total episode count. + /// + [JsonProperty("totalEpisodeCount")] + public int TotalEpisodeCount{ get; set; } + + /// + /// Gets or sets the size on disk. + /// + /// + /// The size on disk. + /// + [JsonProperty("sizeOnDisk")] + public long SizeOnDisk{ get; set; } + + /// + /// Gets or sets the percent of episodes. + /// + /// + /// The percent of episodes. + /// + [JsonProperty("percentOfEpisodes")] + public double PercentOfEpisodes{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Sonarr/SonarrClient.cs b/CRD/Utils/Sonarr/SonarrClient.cs new file mode 100644 index 0000000..3e8496b --- /dev/null +++ b/CRD/Utils/Sonarr/SonarrClient.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using CRD.Downloader; +using CRD.Utils.Sonarr.Models; +using CRD.Views; +using Newtonsoft.Json; + +namespace CRD.Utils.Sonarr; + +public class SonarrClient{ + private string apiUrl; + + private HttpClient httpClient; + + private SonarrProperties properties; + + #region Singelton + + private static SonarrClient? _instance; + private static readonly object Padlock = new(); + + public static SonarrClient Instance{ + get{ + if (_instance == null){ + lock (Padlock){ + if (_instance == null){ + _instance = new SonarrClient(); + } + } + } + + return _instance; + } + } + + #endregion + + public SonarrClient(){ + httpClient = new HttpClient(); + } + + public void SetApiUrl(){ + if (Crunchyroll.Instance.CrunOptions.SonarrProperties != null) properties = Crunchyroll.Instance.CrunOptions.SonarrProperties; + + if (properties != null){ + apiUrl = $"http{(properties.UseSsl ? "s" : "")}://{properties.Host}:{properties.Port}{(properties.UrlBase ?? "")}/api"; + } + } + + public async Task> GetSeries(){ + var json = await GetJson($"/v3/series{(true ? $"?includeSeasonImages={true}" : "")}"); + + List series = []; + + try{ + series = JsonConvert.DeserializeObject>(json) ?? []; + } catch (Exception e){ + MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e); + Console.WriteLine(e); + } + + return series; + } + + public async Task> GetEpisodes(int seriesId){ + var json = await GetJson($"/v3/episode?seriesId={seriesId}"); + + List episodes = []; + + try{ + episodes = JsonConvert.DeserializeObject>(json) ?? []; + } catch (Exception e){ + MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e); + Console.WriteLine(e); + } + + return episodes; + } + + + public async Task GetEpisode(int episodeId){ + var json = await GetJson($"/v3/episode/id={episodeId}"); + var episode = new SonarrEpisode(); + try{ + episode = JsonConvert.DeserializeObject(json) ?? new SonarrEpisode(); + } catch (Exception e){ + MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e); + Console.WriteLine(e); + } + + return episode; + } + + private async Task GetJson(string endpointUrl){ + Debug.WriteLine($"[DEBUG] [SonarrClient.PostJson] Endpoint URL: '{endpointUrl}'"); + + var request = CreateRequestMessage($"{apiUrl}{endpointUrl}", HttpMethod.Get); + HttpResponseMessage response; + var content = string.Empty; + + try{ + response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + content = await response.Content.ReadAsStringAsync(); + + } catch (Exception ex){ + Debug.WriteLine($"[ERROR] [SonarrClient.GetJson] Endpoint URL: '{endpointUrl}', {ex}"); + } + + + if (!string.IsNullOrEmpty(content)) // Convert response to UTF8 + content = Encoding.UTF8.GetString(Encoding.Default.GetBytes(content)); + + return content; + } + + public HttpRequestMessage CreateRequestMessage(string uri, HttpMethod requestMethod, [Optional] NameValueCollection query){ + UriBuilder uriBuilder = new UriBuilder(uri); + + if (query != null){ + uriBuilder.Query = query.ToString(); + } + + var request = new HttpRequestMessage(requestMethod, uriBuilder.ToString()); + + request.Headers.Add("X-Api-Key", properties.ApiKey); + + request.Headers.UserAgent.ParseAdd($"{Assembly.GetExecutingAssembly().GetName().Name.Replace(" ", ".")}.v{Assembly.GetExecutingAssembly().GetName().Version}"); + + + return request; + } + +} + + + +public class SonarrProperties(){ + public string? Host{ get; set; } + public int Port{ get; set; } + public string? ApiKey{ get; set; } + public bool UseSsl{ get; set; } + + public string? UrlBase{ get; set; } +} \ No newline at end of file diff --git a/CRD/Utils/Structs/CrDownloadOptions.cs b/CRD/Utils/Structs/CrDownloadOptions.cs index f28978a..05eac33 100644 --- a/CRD/Utils/Structs/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/CrDownloadOptions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using CRD.Utils.Sonarr; using YamlDotNet.Serialization; namespace CRD.Utils.Structs; @@ -115,4 +116,7 @@ public class CrDownloadOptions{ [YamlMember(Alias = "user_non_drm_streams", ApplyNamingConventions = false)] public bool UseNonDrmStreams{ get; set; } + [YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)] + public SonarrProperties? SonarrProperties{ get; set; } + } \ No newline at end of file diff --git a/CRD/ViewModels/AddDownloadPageViewModel.cs b/CRD/ViewModels/AddDownloadPageViewModel.cs index ca3101a..5b693b2 100644 --- a/CRD/ViewModels/AddDownloadPageViewModel.cs +++ b/CRD/ViewModels/AddDownloadPageViewModel.cs @@ -39,7 +39,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{ private CrunchySeriesList? currentSeriesList; public AddDownloadPageViewModel(){ - // Items.Add(new ItemModel("", "Test", "22:33", "Test", "S1", "E1", 1, new List())); SelectedItems.CollectionChanged += OnSelectedItemsChanged; } diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index 561214b..e228e46 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -1,55 +1,93 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader; using CRD.Utils; +using CRD.Utils.Sonarr; using CRD.Views; using ReactiveUI; namespace CRD.ViewModels; public partial class SeriesPageViewModel : ViewModelBase{ - - [ObservableProperty] public HistorySeries _selectedSeries; - + [ObservableProperty] public static bool _editMode; + [ObservableProperty] + public static bool _sonarrAvailable; + public SeriesPageViewModel(){ _selectedSeries = Crunchyroll.Instance.SelectedSeries; - + 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{ + SonarrAvailable = false; + } + } - + + [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); - MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel),false,true)); + MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true)); } - + [RelayCommand] public void RemoveSeason(string? season){ - HistorySeason? objectToRemove = SelectedSeries.Seasons.Find(se => se.SeasonId == season) ?? null; - if (objectToRemove != null) { + if (objectToRemove != null){ SelectedSeries.Seasons.Remove(objectToRemove); } - CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); - MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel),false,true)); + + CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); + MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true)); } - - - + + [RelayCommand] public void NavBack(){ SelectedSeries.UpdateNewEpisodes(); - MessageBus.Current.SendMessage(new NavigationMessage(null,true,false)); + MessageBus.Current.SendMessage(new NavigationMessage(null, true, false)); + } + + + private void OpenUrl(string url){ + try{ + Process.Start(new ProcessStartInfo{ + FileName = url, + UseShellExecute = true + }); + } catch (Exception e){ + Console.WriteLine($"An error occurred: {e.Message}"); + } } - } \ No newline at end of file diff --git a/CRD/ViewModels/SettingsPageViewModel.cs b/CRD/ViewModels/SettingsPageViewModel.cs index 027abef..2f721ec 100644 --- a/CRD/ViewModels/SettingsPageViewModel.cs +++ b/CRD/ViewModels/SettingsPageViewModel.cs @@ -12,6 +12,7 @@ using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; using CRD.Downloader; using CRD.Utils; +using CRD.Utils.Sonarr; using CRD.Utils.Structs; using FluentAvalonia.Styling; @@ -35,7 +36,7 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private bool _history; - + [ObservableProperty] private bool _useNonDrmEndpoint = true; @@ -87,6 +88,18 @@ public partial class SettingsPageViewModel : ViewModelBase{ [ObservableProperty] private Color _customAccentColor = Colors.SlateBlue; + [ObservableProperty] + private string _sonarrHost = "localhost"; + + [ObservableProperty] + private string _sonarrPort = "8989"; + + [ObservableProperty] + private string _sonarrApiKey = ""; + + [ObservableProperty] + private bool _sonarrUseSsl = false; + public ObservableCollection PredefinedColors{ get; } = new(){ Color.FromRgb(255, 185, 0), Color.FromRgb(255, 140, 0), @@ -193,10 +206,10 @@ public partial class SettingsPageViewModel : ViewModelBase{ } CrDownloadOptions options = Crunchyroll.Instance.CrunOptions; - + ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null; SelectedHSLang = hsLang ?? HardSubLangList[0]; - + var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList(); SelectedSubLang.Clear(); @@ -213,6 +226,14 @@ public partial class SettingsPageViewModel : ViewModelBase{ UpdateSubAndDubString(); + var props = options.SonarrProperties; + + if (props != null){ + SonarrUseSsl = props.UseSsl; + SonarrHost = props.Host + ""; + SonarrPort = props.Port + ""; + SonarrApiKey = props.ApiKey + ""; + } UseNonDrmEndpoint = options.UseNonDrmStreams; DownloadVideo = !options.Novids; @@ -292,6 +313,17 @@ public partial class SettingsPageViewModel : ViewModelBase{ Crunchyroll.Instance.CrunOptions.History = History; + var props = new SonarrProperties(); + + props.UseSsl = SonarrUseSsl; + props.Host = SonarrHost; + props.Port = Convert.ToInt32(SonarrPort); + props.ApiKey = SonarrApiKey; + + Crunchyroll.Instance.CrunOptions.SonarrProperties = props; + + Crunchyroll.Instance.RefreshSonarr(); + //TODO - Mux Options CfgManager.WriteSettingsToFile(); @@ -370,8 +402,6 @@ public partial class SettingsPageViewModel : ViewModelBase{ } - - private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ UpdateSettings(); } @@ -416,7 +446,27 @@ public partial class SettingsPageViewModel : ViewModelBase{ UpdateSettings(); } + partial void OnUseNonDrmEndpointChanged(bool value){ + UpdateSettings(); + } + partial void OnHistoryChanged(bool value){ UpdateSettings(); } + + partial void OnSonarrHostChanged(string value){ + UpdateSettings(); + } + + partial void OnSonarrPortChanged(string value){ + UpdateSettings(); + } + + partial void OnSonarrApiKeyChanged(string value){ + UpdateSettings(); + } + + partial void OnSonarrUseSslChanged(bool value){ + UpdateSettings(); + } } \ No newline at end of file diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml index 9da0a8d..b940b51 100644 --- a/CRD/Views/SeriesPageView.axaml +++ b/CRD/Views/SeriesPageView.axaml @@ -40,9 +40,36 @@ - - - Edit + + + + + + + + + + + + + + Edit + + + @@ -80,6 +107,20 @@ + + + + + + + + + + +