Add - Added Sonarr support

This commit is contained in:
Elwador 2024-05-26 00:02:45 +02:00
parent 461b626e79
commit 095054697d
24 changed files with 1272 additions and 106 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
CRD/Assets/sonarr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -12,7 +12,10 @@ using YamlDotNet.Core.Tokens;
namespace CRD.Downloader; namespace CRD.Downloader;
public class CrAuth(Crunchyroll crunInstance){ public class CrAuth{
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
public async Task AuthAnonymous(){ public async Task AuthAnonymous(){
var formData = new Dictionary<string, string>{ var formData = new Dictionary<string, string>{
{ "grant_type", "client_id" }, { "grant_type", "client_id" },

View File

@ -12,7 +12,10 @@ using Newtonsoft.Json;
namespace CRD.Downloader; namespace CRD.Downloader;
public class CrEpisode(Crunchyroll crunInstance){ public class CrEpisode(){
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
public async Task<CrunchyEpisodeList?> ParseEpisodeById(string id,string locale){ public async Task<CrunchyEpisodeList?> ParseEpisodeById(string id,string locale){
if (crunInstance.CmsToken?.Cms == null){ if (crunInstance.CmsToken?.Cms == null){
Console.WriteLine("Missing CMS Access Token"); Console.WriteLine("Missing CMS Access Token");

View File

@ -13,7 +13,10 @@ using Newtonsoft.Json;
namespace CRD.Downloader; namespace CRD.Downloader;
public class CrSeries(Crunchyroll crunInstance){ public class CrSeries(){
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
public async Task<List<CrunchyEpMeta>> DownloadFromSeriesId(string id, CrunchyMultiDownload data){ public async Task<List<CrunchyEpMeta>> DownloadFromSeriesId(string id, CrunchyMultiDownload data){
var series = await ListSeriesId(id, "" ,data); var series = await ListSeriesId(id, "" ,data);
@ -353,7 +356,7 @@ public class CrSeries(Crunchyroll crunInstance){
} }
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
query["preferred_audio_language"] = "ja-JP"; query["preferred_audio_language"] = "ja-JP";
if (!string.IsNullOrEmpty(locale)){ if (!string.IsNullOrEmpty(locale)){
query["locale"] = Languages.Locale2language(locale).CrLocale; query["locale"] = Languages.Locale2language(locale).CrLocale;

View File

@ -17,6 +17,8 @@ using CRD.Utils.CustomList;
using CRD.Utils.DRM; using CRD.Utils.DRM;
using CRD.Utils.HLS; using CRD.Utils.HLS;
using CRD.Utils.Muxing; using CRD.Utils.Muxing;
using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.ViewModels; using CRD.ViewModels;
using CRD.Views; using CRD.Views;
@ -62,6 +64,8 @@ public class Crunchyroll{
Seasons =[] Seasons =[]
}; };
public List<SonarrSeries> SonarrSeries =[];
#endregion #endregion
public string DefaultLocale = "en"; public string DefaultLocale = "en";
@ -101,10 +105,10 @@ public class Crunchyroll{
public async Task Init(){ public async Task Init(){
_widevine = Widevine.Instance; _widevine = Widevine.Instance;
CrAuth = new CrAuth(Instance); CrAuth = new CrAuth();
CrEpisode = new CrEpisode(Instance); CrEpisode = new CrEpisode();
CrSeries = new CrSeries(Instance); CrSeries = new CrSeries();
CrHistory = new History(Instance); CrHistory = new History();
Profile = new CrProfile{ Profile = new CrProfile{
Username = "???", Username = "???",
@ -146,6 +150,7 @@ public class Crunchyroll{
CrunOptions.Theme = "System"; CrunOptions.Theme = "System";
CrunOptions.SelectedCalendarLanguage = "de"; CrunOptions.SelectedCalendarLanguage = "de";
CrunOptions.DlVideoOnce = true; CrunOptions.DlVideoOnce = true;
CrunOptions.UseNonDrmStreams = true;
CrunOptions.History = true; CrunOptions.History = true;
@ -155,6 +160,8 @@ public class Crunchyroll{
if (File.Exists(CfgManager.PathCrHistory)){ if (File.Exists(CfgManager.PathCrHistory)){
HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(File.ReadAllText(CfgManager.PathCrHistory)) ??[]; HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(File.ReadAllText(CfgManager.PathCrHistory)) ??[];
} }
RefreshSonarr();
} }
@ -173,16 +180,14 @@ public class Crunchyroll{
}; };
} }
// public async void TestMethode(){ public async void RefreshSonarr(){
// // One Pice - GRMG8ZQZR if (CrunOptions.SonarrProperties != null && !string.IsNullOrEmpty(CrunOptions.SonarrProperties.ApiKey)){
// // Studio - G9VHN9QWQ SonarrClient.Instance.SetApiUrl();
// var episodesMeta = await DownloadFromSeriesId("G9VHN9QWQ", new CrunchyMultiDownload(Crunchy.Instance.CrunOptions.dubLang, true)); SonarrSeries = await SonarrClient.Instance.GetSeries();
// CrHistory.MatchHistorySeriesWithSonarr(true);
// }
// foreach (var crunchyEpMeta in episodesMeta){ }
// await DownloadEpisode(crunchyEpMeta, CrunOptions, false);
// }
// }
public async Task<CalendarWeek> GetCalendarForDate(string weeksMondayDate, bool forceUpdate){ public async Task<CalendarWeek> GetCalendarForDate(string weeksMondayDate, bool forceUpdate){
if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){ if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){

View File

@ -1,15 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Views; using CRD.Views;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -17,11 +16,13 @@ using ReactiveUI;
namespace CRD.Downloader; 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){ public async Task UpdateSeries(string seriesId, string? seasonId){
await crunInstance.CrAuth.RefreshToken(true); await crunInstance.CrAuth.RefreshToken(true);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja"); CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja");
if (parsedSeries == null){ if (parsedSeries == null){
Console.WriteLine("Parse Data Invalid"); Console.WriteLine("Parse Data Invalid");
@ -43,11 +44,12 @@ public class History(Crunchyroll crunInstance){
if (sVersion.Guid != null){ if (sVersion.Guid != null){
sId = sVersion.Guid; sId = sVersion.Guid;
} }
break; break;
} }
} }
} }
var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId); var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId);
UpdateWithSeasonData(seasonData); UpdateWithSeasonData(seasonData);
} }
@ -103,12 +105,22 @@ public class History(Crunchyroll crunInstance){
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == episode.SeasonId); 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){ if (historySeason != null){
historySeason.SeasonTitle = episode.SeasonTitle;
historySeason.SeasonNum = episode.SeasonNumber + "";
if (historySeason.EpisodesList.All(e => e.EpisodeId != episode.Id)){ if (historySeason.EpisodesList.All(e => e.EpisodeId != episode.Id)){
var newHistoryEpisode = new HistoryEpisode{ var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = episode.Title, EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title,
EpisodeDescription = episode.Description,
EpisodeId = episode.Id, EpisodeId = episode.Id,
Episode = episode.Episode, Episode = episode.Episode,
EpisodeSeasonNum = episode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(episode.Episode, out _),
}; };
historySeason.EpisodesList.Add(newHistoryEpisode); 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.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList();
} }
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} else{ } else{
var newHistorySeries = new HistorySeries{ var newHistorySeries = new HistorySeries{
@ -136,6 +149,7 @@ public class History(Crunchyroll crunInstance){
if (series?.Data != null){ if (series?.Data != null){
newHistorySeries.SeriesDescription = series.Data.First().Description; newHistorySeries.SeriesDescription = series.Data.First().Description;
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series); newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
newHistorySeries.SeriesTitle = series.Data.First().Title;
} }
newHistorySeries.Seasons.Add(newSeason); newHistorySeries.Seasons.Add(newSeason);
@ -148,6 +162,7 @@ public class History(Crunchyroll crunInstance){
crunInstance.HistoryList.Add(item); crunInstance.HistoryList.Add(item);
} }
MatchHistorySeriesWithSonarr(false);
UpdateHistoryFile(); UpdateHistoryFile();
} }
@ -158,27 +173,38 @@ public class History(Crunchyroll crunInstance){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == firstEpisode.SeasonId); 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){ if (historySeason != null){
historySeason.SeasonTitle = firstEpisode.SeasonTitle;
historySeason.SeasonNum = firstEpisode.SeasonNumber + "";
foreach (var crunchyEpisode in seasonData.Data){ foreach (var crunchyEpisode in seasonData.Data){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id); var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id);
if (historyEpisode == null){ if (historyEpisode == null){
var newHistoryEpisode = new HistoryEpisode{ var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = crunchyEpisode.Title, EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title,
EpisodeDescription = crunchyEpisode.Description,
EpisodeId = crunchyEpisode.Id, EpisodeId = crunchyEpisode.Id,
Episode = crunchyEpisode.Episode, Episode = crunchyEpisode.Episode,
EpisodeSeasonNum = crunchyEpisode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
}; };
historySeason.EpisodesList.Add(newHistoryEpisode); historySeason.EpisodesList.Add(newHistoryEpisode);
} else{ } else{
//Update existing episode //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.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()); 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.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList();
} }
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} else{ } else{
var newHistorySeries = new HistorySeries{ var newHistorySeries = new HistorySeries{
@ -208,11 +235,12 @@ public class History(Crunchyroll crunInstance){
if (series?.Data != null){ if (series?.Data != null){
newHistorySeries.SeriesDescription = series.Data.First().Description; newHistorySeries.SeriesDescription = series.Data.First().Description;
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series); newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
newHistorySeries.SeriesTitle = series.Data.First().Title;
} }
newHistorySeries.Seasons.Add(newSeason); newHistorySeries.Seasons.Add(newSeason);
newHistorySeries.UpdateNewEpisodes(); newHistorySeries.UpdateNewEpisodes();
} }
} }
@ -223,6 +251,7 @@ public class History(Crunchyroll crunInstance){
crunInstance.HistoryList.Add(item); crunInstance.HistoryList.Add(item);
} }
MatchHistorySeriesWithSonarr(false);
UpdateHistoryFile(); UpdateHistoryFile();
} }
@ -255,9 +284,11 @@ public class History(Crunchyroll crunInstance){
foreach (var crunchyEpisode in seasonData.Data!){ foreach (var crunchyEpisode in seasonData.Data!){
var newHistoryEpisode = new HistoryEpisode{ var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = crunchyEpisode.Title, EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title,
EpisodeDescription = crunchyEpisode.Description,
EpisodeId = crunchyEpisode.Id, EpisodeId = crunchyEpisode.Id,
Episode = crunchyEpisode.Episode, Episode = crunchyEpisode.Episode,
EpisodeSeasonNum = firstEpisode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
}; };
@ -276,9 +307,11 @@ public class History(Crunchyroll crunInstance){
}; };
var newHistoryEpisode = new HistoryEpisode{ var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = episode.Title, EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title,
EpisodeDescription = episode.Description,
EpisodeId = episode.Id, EpisodeId = episode.Id,
Episode = episode.Episode, Episode = episode.Episode,
EpisodeSeasonNum = episode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(episode.Episode, out _), SpecialEpisode = !int.TryParse(episode.Episode, out _),
}; };
@ -287,6 +320,170 @@ public class History(Crunchyroll crunInstance){
return newSeason; 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<HistoryEpisode> allHistoryEpisodes =[];
foreach (var historySeriesSeason in historySeries.Seasons){
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
}
List<HistoryEpisode> 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<SonarrEpisode> 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<HistoryEpisode>{ public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
@ -294,7 +491,7 @@ public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
if (int.TryParse(x.Episode, out int xInt) && int.TryParse(y.Episode, out int yInt)){ if (int.TryParse(x.Episode, out int xInt) && int.TryParse(y.Episode, out int yInt)){
return xInt.CompareTo(yInt); return xInt.CompareTo(yInt);
} }
// Fall back to string comparison if not parseable as integers // Fall back to string comparison if not parseable as integers
return String.Compare(x.Episode, y.Episode, StringComparison.Ordinal); return String.Compare(x.Episode, y.Episode, StringComparison.Ordinal);
} }
@ -307,6 +504,15 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_id")] [JsonProperty("series_id")]
public string? SeriesId{ get; set; } 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")] [JsonProperty("series_description")]
public string? SeriesDescription{ get; set; } public string? SeriesDescription{ get; set; }
@ -346,18 +552,16 @@ public class HistorySeries : INotifyPropertyChanged{
// Iterate over the Seasons list from the end to the beginning // Iterate over the Seasons list from the end to the beginning
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){ for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
if (Seasons[i].SpecialSeason == true){ if (Seasons[i].SpecialSeason == true){
continue; continue;
} }
// Iterate over the Episodes from the end to the beginning // Iterate over the Episodes from the end to the beginning
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){ for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
if (Seasons[i].EpisodesList[j].SpecialEpisode){ if (Seasons[i].EpisodesList[j].SpecialEpisode){
continue; continue;
} }
if (!Seasons[i].EpisodesList[j].WasDownloaded){ if (!Seasons[i].EpisodesList[j].WasDownloaded){
count++; count++;
} else{ } else{
@ -365,27 +569,26 @@ public class HistorySeries : INotifyPropertyChanged{
} }
} }
} }
NewEpisodes = count; NewEpisodes = count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
} }
public async Task AddNewMissingToDownloads(){ public async Task AddNewMissingToDownloads(){
bool foundWatched = false; bool foundWatched = false;
// Iterate over the Seasons list from the end to the beginning // Iterate over the Seasons list from the end to the beginning
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){ for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
if (Seasons[i].SpecialSeason == true){ if (Seasons[i].SpecialSeason == true){
continue; continue;
} }
// Iterate over the Episodes from the end to the beginning // Iterate over the Episodes from the end to the beginning
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){ for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
if (Seasons[i].EpisodesList[j].SpecialEpisode){ if (Seasons[i].EpisodesList[j].SpecialEpisode){
continue; continue;
} }
if (!Seasons[i].EpisodesList[j].WasDownloaded){ if (!Seasons[i].EpisodesList[j].WasDownloaded){
//ADD to download queue //ADD to download queue
await Seasons[i].EpisodesList[j].DownloadEpisode(); await Seasons[i].EpisodesList[j].DownloadEpisode();
@ -410,9 +613,10 @@ public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("season_cr_season_number")] [JsonProperty("season_cr_season_number")]
public string? SeasonNum{ get; set; } public string? SeasonNum{ get; set; }
[JsonProperty("season_special_season")] [JsonProperty("season_special_season")]
public bool? SpecialSeason{ get; set; } public bool? SpecialSeason{ get; set; }
[JsonIgnore] [JsonIgnore]
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}"; public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
@ -450,15 +654,36 @@ public partial class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_cr_episode_number")] [JsonProperty("episode_cr_episode_number")]
public string? Episode{ get; set; } 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")] [JsonProperty("episode_was_downloaded")]
public bool WasDownloaded{ get; set; } public bool WasDownloaded{ get; set; }
[JsonProperty("episode_special_episode")] [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 event PropertyChangedEventHandler? PropertyChanged;
public void ToggleWasDownloaded(){ public void ToggleWasDownloaded(){
WasDownloaded = !WasDownloaded; WasDownloaded = !WasDownloaded;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
@ -466,6 +691,5 @@ public partial class HistoryEpisode : INotifyPropertyChanged{
public async Task DownloadEpisode(){ public async Task DownloadEpisode(){
await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang); await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang);
} }
} }

View File

@ -8,25 +8,62 @@ namespace CRD.Utils;
[DataContract] [DataContract]
[JsonConverter(typeof(LocaleConverter))] [JsonConverter(typeof(LocaleConverter))]
public enum Locale{ public enum Locale{
[EnumMember(Value = "")] DefaulT, [EnumMember(Value = "")]
[EnumMember(Value = "un")] Unknown, DefaulT,
[EnumMember(Value = "en-US")] EnUs,
[EnumMember(Value = "es-LA")] EsLa, [EnumMember(Value = "un")]
[EnumMember(Value = "es-419")] Es419, Unknown,
[EnumMember(Value = "es-ES")] EsEs,
[EnumMember(Value = "pt-BR")] PtBr, [EnumMember(Value = "en-US")]
[EnumMember(Value = "fr-FR")] FrFr, EnUs,
[EnumMember(Value = "de-DE")] DeDe,
[EnumMember(Value = "ar-ME")] ArMe, [EnumMember(Value = "es-LA")]
[EnumMember(Value = "ar-SA")] ArSa, EsLa,
[EnumMember(Value = "it-IT")] ItIt,
[EnumMember(Value = "ru-RU")] RuRu, [EnumMember(Value = "es-419")]
[EnumMember(Value = "tr-TR")] TrTr, Es419,
[EnumMember(Value = "hi-IN")] HiIn,
[EnumMember(Value = "zh-CN")] ZhCn, [EnumMember(Value = "es-ES")]
[EnumMember(Value = "ko-KR")] KoKr, EsEs,
[EnumMember(Value = "ja-JP")] JaJp,
[EnumMember(Value = "id-ID")] IdId, [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{ public static class EnumExtensions{
@ -49,34 +86,67 @@ public static class EnumExtensions{
[DataContract] [DataContract]
public enum ChannelId{ public enum ChannelId{
[EnumMember(Value = "crunchyroll")] Crunchyroll, [EnumMember(Value = "crunchyroll")]
Crunchyroll,
} }
[DataContract] [DataContract]
public enum ImageType{ 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] [DataContract]
public enum MaturityRating{ public enum MaturityRating{
[EnumMember(Value = "TV-14")] Tv14, [EnumMember(Value = "TV-14")]
Tv14,
} }
[DataContract] [DataContract]
public enum MediaType{ public enum MediaType{
[EnumMember(Value = "episode")] Episode, [EnumMember(Value = "episode")]
Episode,
} }
[DataContract] [DataContract]
public enum DownloadMediaType{ public enum DownloadMediaType{
[EnumMember(Value = "Video")] Video, [EnumMember(Value = "Video")]
[EnumMember(Value = "Audio")] Audio, Video,
[EnumMember(Value = "Chapters")] Chapters,
[EnumMember(Value = "Subtitle")] Subtitle, [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
};

View File

@ -142,6 +142,7 @@ public class CfgManager{
Crunchyroll.Instance.CrunOptions.AccentColor = loadedOptions.AccentColor; Crunchyroll.Instance.CrunOptions.AccentColor = loadedOptions.AccentColor;
Crunchyroll.Instance.CrunOptions.History = loadedOptions.History; Crunchyroll.Instance.CrunOptions.History = loadedOptions.History;
Crunchyroll.Instance.CrunOptions.UseNonDrmStreams = loadedOptions.UseNonDrmStreams; Crunchyroll.Instance.CrunOptions.UseNonDrmStreams = loadedOptions.UseNonDrmStreams;
Crunchyroll.Instance.CrunOptions.SonarrProperties = loadedOptions.SonarrProperties;
} }
private static object fileLock = new object(); private static object fileLock = new object();

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -14,9 +16,9 @@ public class Helpers{
/// <param name="json">The JSON string to deserialize.</param> /// <param name="json">The JSON string to deserialize.</param>
/// <param name="serializerSettings">The settings for deserialization if null default settings will be used</param> /// <param name="serializerSettings">The settings for deserialization if null default settings will be used</param>
/// <returns>The deserialized object of type T.</returns> /// <returns>The deserialized object of type T.</returns>
public static T? Deserialize<T>(string json,JsonSerializerSettings? serializerSettings){ public static T? Deserialize<T>(string json, JsonSerializerSettings? serializerSettings){
try{ try{
return JsonConvert.DeserializeObject<T>(json,serializerSettings); return JsonConvert.DeserializeObject<T>(json, serializerSettings);
} catch (JsonException ex){ } catch (JsonException ex){
Console.WriteLine($"Error deserializing JSON: {ex.Message}"); Console.WriteLine($"Error deserializing JSON: {ex.Message}");
throw; throw;
@ -77,4 +79,42 @@ public class Helpers{
return (IsOk: isSuccess, ErrorCode: process.ExitCode); 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<string, double> ComputeWordFrequency(string text){
var wordFrequency = new Dictionary<string, double>();
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<string, double> vector1, Dictionary<string, double> 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);
}
} }

View File

@ -0,0 +1,141 @@
using System;
using Newtonsoft.Json;
namespace CRD.Utils.Sonarr.Models;
public class SonarrEpisode{
/// <summary>
/// Gets or sets the series identifier.
/// </summary>
/// <value>
/// The series identifier.
/// </value>
[JsonProperty("seriesId")]
public int SeriesId{ get; set; }
/// <summary>
/// Gets or sets the episode file identifier.
/// </summary>
/// <value>
/// The episode file identifier.
/// </value>
[JsonProperty("episodeFileId")]
public int EpisodeFileId{ get; set; }
/// <summary>
/// Gets or sets the season number.
/// </summary>
/// <value>
/// The season number.
/// </value>
[JsonProperty("seasonNumber")]
public int SeasonNumber{ get; set; }
/// <summary>
/// Gets or sets the episode number.
/// </summary>
/// <value>
/// The episode number.
/// </value>
[JsonProperty("episodeNumber")]
public int EpisodeNumber{ get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
/// <value>
/// The title.
/// </value>
[JsonProperty("title")]
public string Title{ get; set; }
/// <summary>
/// Gets or sets the air date.
/// </summary>
/// <value>
/// The air date.
/// </value>
[JsonProperty("airDate")]
public DateTimeOffset AirDate{ get; set; }
/// <summary>
/// Gets or sets the air date UTC.
/// </summary>
/// <value>
/// The air date UTC.
/// </value>
[JsonProperty("airDateUtc")]
public DateTimeOffset AirDateUtc{ get; set; }
/// <summary>
/// Gets or sets the overview.
/// </summary>
/// <value>
/// The overview.
/// </value>
[JsonProperty("overview")]
public string Overview{ get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance has file.
/// </summary>
/// <value>
/// <c>true</c> if this instance has file; otherwise, <c>false</c>.
/// </value>
[JsonProperty("hasFile")]
public bool HasFile{ get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="Episode"/> is monitored.
/// </summary>
/// <value>
/// <c>true</c> if monitored; otherwise, <c>false</c>.
/// </value>
[JsonProperty("monitored")]
public bool Monitored{ get; set; }
/// <summary>
/// Gets or sets the scene episode number.
/// </summary>
/// <value>
/// The scene episode number.
/// </value>
[JsonProperty("sceneEpisodeNumber")]
public int SceneEpisodeNumber{ get; set; }
/// <summary>
/// Gets or sets the scene season number.
/// </summary>
/// <value>
/// The scene season number.
/// </value>
[JsonProperty("sceneSeasonNumber")]
public int SceneSeasonNumber{ get; set; }
/// <summary>
/// Gets or sets the tv database episode identifier.
/// </summary>
/// <value>
/// The tv database episode identifier.
/// </value>
[JsonProperty("tvDbEpisodeId")]
public int TvDbEpisodeId{ get; set; }
/// <summary>
/// Gets or sets the absolute episode number.
/// </summary>
/// <value>
/// The absolute episode number.
/// </value>
[JsonProperty("absoluteEpisodeNumber")]
public int AbsoluteEpisodeNumber{ get; set; }
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>
/// The identifier.
/// </value>
[JsonProperty("id")]
public int Id{ get; set; }
}

View File

@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace CRD.Utils.Sonarr.Models;
public class SonarrImage{
/// <summary>
/// Gets or sets the type of the cover.
/// </summary>
/// <value>
/// The type of the cover.
/// </value>
[JsonProperty("coverType")] public SonarrCoverType CoverType { get; set; }
/// <summary>
/// Gets or sets the URL.
/// </summary>
/// <value>
/// The URL.
/// </value>
[JsonProperty("url")] public string Url { get; set; }
}

View File

@ -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; }
}

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CRD.Utils.Sonarr.Models;
public class SonarrSeason{
/// <summary>
/// Gets or sets the season number.
/// </summary>
/// <value>
/// The season number.
/// </value>
[JsonProperty("seasonNumber")] public int SeasonNumber { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="Season"/> is monitored.
/// </summary>
/// <value>
/// <c>true</c> if monitored; otherwise, <c>false</c>.
/// </value>
[JsonProperty("monitored")] public bool Monitored { get; set; }
/// <summary>
/// Gets or sets the statistics.
/// </summary>
/// <value>
/// The statistics.
/// </value>
[JsonProperty("statistics")] public SonarrStatistics Statistics { get; set; }
/// <summary>
/// Gets or sets the images.
/// </summary>
/// <value>
/// The images.
/// </value>
[JsonProperty("images")] public List<SonarrImage> Images { get; set; }
}

View File

@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CRD.Utils.Sonarr.Models;
public class SonarrSeries{
/// <summary>
/// Gets or sets the TVDB identifier.
/// </summary>
/// <value>
/// The TVDB identifier.
/// </value>
[JsonProperty("tvdbId")]
public int TvdbId{ get; set; }
/// <summary>
/// Gets or sets the tv rage identifier.
/// </summary>
/// <value>
/// The tv rage identifier.
/// </value>
[JsonProperty("tvRageId")]
public long TvRageId{ get; set; }
/// <summary>
/// Gets or sets the imdb identifier.
/// </summary>
/// <value>
/// The imdb identifier.
/// </value>
[JsonProperty("imdbId")]
public string ImdbId{ get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
/// <value>
/// The title.
/// </value>
[JsonProperty("title")]
public string Title{ get; set; }
/// <summary>
/// Gets or sets the clean title.
/// </summary>
/// <value>
/// The clean title.
/// </value>
[JsonProperty("cleanTitle")]
public string CleanTitle{ get; set; }
/// <summary>
/// Gets or sets the status.
/// </summary>
/// <value>
/// The status.
/// </value>
[JsonProperty("status")]
public SonarrStatus Status{ get; set; }
/// <summary>
/// Gets or sets the overview.
/// </summary>
/// <value>
/// The overview.
/// </value>
[JsonProperty("overview")]
public string Overview{ get; set; }
/// <summary>
/// Gets or sets the air time.
/// </summary>
/// <value>
/// The air time.
/// </value>
[JsonProperty("airTime")]
public string AirTime{ get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="Series"/> is monitored.
/// </summary>
/// <value>
/// <c>true</c> if monitored; otherwise, <c>false</c>.
/// </value>
[JsonProperty("monitored")]
public bool Monitored{ get; set; }
/// <summary>
/// Gets or sets the quality profile identifier.
/// </summary>
/// <value>
/// The quality profile identifier.
/// </value>
[JsonProperty("qualityProfileId")]
public long QualityProfileId{ get; set; }
/// <summary>
/// Gets or sets a value indicating whether [season folder].
/// </summary>
/// <value>
/// <c>true</c> if [season folder]; otherwise, <c>false</c>.
/// </value>
[JsonProperty("seasonFolder")]
public bool SeasonFolder{ get; set; }
/// <summary>
/// Gets or sets the last information synchronize.
/// </summary>
/// <value>
/// The last information synchronize.
/// </value>
[JsonProperty("lastInfoSync")]
public DateTimeOffset LastInfoSync{ get; set; }
/// <summary>
/// Gets or sets the runtime.
/// </summary>
/// <value>
/// The runtime.
/// </value>
[JsonProperty("runtime")]
public long Runtime{ get; set; }
/// <summary>
/// Gets or sets the images.
/// </summary>
/// <value>
/// The images.
/// </value>
[JsonProperty("images")]
public List<SonarrImage> Images{ get; set; }
/// <summary>
/// Gets or sets the type of the series.
/// </summary>
/// <value>
/// The type of the series.
/// </value>
[JsonProperty("seriesType")]
public SonarrSeriesType SeriesType{ get; set; }
/// <summary>
/// Gets or sets the network.
/// </summary>
/// <value>
/// The network.
/// </value>
[JsonProperty("network")]
public string Network{ get; set; }
/// <summary>
/// Gets or sets a value indicating whether [use scene numbering].
/// </summary>
/// <value>
/// <c>true</c> if [use scene numbering]; otherwise, <c>false</c>.
/// </value>
[JsonProperty("useSceneNumbering")]
public bool UseSceneNumbering{ get; set; }
/// <summary>
/// Gets or sets the title slug.
/// </summary>
/// <value>
/// The title slug.
/// </value>
[JsonProperty("titleSlug")]
public string TitleSlug{ get; set; }
/// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>
/// The path.
/// </value>
[JsonProperty("path")]
public string Path{ get; set; }
/// <summary>
/// Gets or sets the year.
/// </summary>
/// <value>
/// The year.
/// </value>
[JsonProperty("year")]
public int Year{ get; set; }
/// <summary>
/// Gets or sets the first aired.
/// </summary>
/// <value>
/// The first aired.
/// </value>
[JsonProperty("firstAired")]
public DateTimeOffset FirstAired{ get; set; }
/// <summary>
/// Gets or sets the quality profile.
/// </summary>
/// <value>
/// The quality profile.
/// </value>
[JsonProperty("qualityProfile")]
public SonarrQualityProfile QualityProfile{ get; set; }
/// <summary>
/// Gets or sets the seasons.
/// </summary>
/// <value>
/// The seasons.
/// </value>
[JsonProperty("seasons")]
public List<SonarrSeason> Seasons{ get; set; }
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>
/// The identifier.
/// </value>
[JsonProperty("id")]
public int Id{ get; set; }
}

View File

@ -0,0 +1,60 @@
using System;
using Newtonsoft.Json;
namespace CRD.Utils.Sonarr.Models;
public class SonarrStatistics{
/// <summary>
/// Gets or sets the previous airing.
/// </summary>
/// <value>
/// The previous airing.
/// </value>
[JsonProperty("previousAiring")]
public DateTimeOffset PreviousAiring{ get; set; }
/// <summary>
/// Gets or sets the episode file count.
/// </summary>
/// <value>
/// The episode file count.
/// </value>
[JsonProperty("episodeFileCount")]
public int EpisodeFileCount{ get; set; }
/// <summary>
/// Gets or sets the episode count.
/// </summary>
/// <value>
/// The episode count.
/// </value>
[JsonProperty("episodeCount")]
public int EpisodeCount{ get; set; }
/// <summary>
/// Gets or sets the total episode count.
/// </summary>
/// <value>
/// The total episode count.
/// </value>
[JsonProperty("totalEpisodeCount")]
public int TotalEpisodeCount{ get; set; }
/// <summary>
/// Gets or sets the size on disk.
/// </summary>
/// <value>
/// The size on disk.
/// </value>
[JsonProperty("sizeOnDisk")]
public long SizeOnDisk{ get; set; }
/// <summary>
/// Gets or sets the percent of episodes.
/// </summary>
/// <value>
/// The percent of episodes.
/// </value>
[JsonProperty("percentOfEpisodes")]
public double PercentOfEpisodes{ get; set; }
}

View File

@ -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<List<SonarrSeries>> GetSeries(){
var json = await GetJson($"/v3/series{(true ? $"?includeSeasonImages={true}" : "")}");
List<SonarrSeries> series = [];
try{
series = JsonConvert.DeserializeObject<List<SonarrSeries>>(json) ?? [];
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e);
Console.WriteLine(e);
}
return series;
}
public async Task<List<SonarrEpisode>> GetEpisodes(int seriesId){
var json = await GetJson($"/v3/episode?seriesId={seriesId}");
List<SonarrEpisode> episodes = [];
try{
episodes = JsonConvert.DeserializeObject<List<SonarrEpisode>>(json) ?? [];
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e);
Console.WriteLine(e);
}
return episodes;
}
public async Task<SonarrEpisode> GetEpisode(int episodeId){
var json = await GetJson($"/v3/episode/id={episodeId}");
var episode = new SonarrEpisode();
try{
episode = JsonConvert.DeserializeObject<SonarrEpisode>(json) ?? new SonarrEpisode();
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e);
Console.WriteLine(e);
}
return episode;
}
private async Task<string> 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; }
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using CRD.Utils.Sonarr;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs;
@ -115,4 +116,7 @@ public class CrDownloadOptions{
[YamlMember(Alias = "user_non_drm_streams", ApplyNamingConventions = false)] [YamlMember(Alias = "user_non_drm_streams", ApplyNamingConventions = false)]
public bool UseNonDrmStreams{ get; set; } public bool UseNonDrmStreams{ get; set; }
[YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)]
public SonarrProperties? SonarrProperties{ get; set; }
} }

View File

@ -39,7 +39,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private CrunchySeriesList? currentSeriesList; private CrunchySeriesList? currentSeriesList;
public AddDownloadPageViewModel(){ public AddDownloadPageViewModel(){
// Items.Add(new ItemModel("", "Test", "22:33", "Test", "S1", "E1", 1, new List<string>()));
SelectedItems.CollectionChanged += OnSelectedItemsChanged; SelectedItems.CollectionChanged += OnSelectedItemsChanged;
} }

View File

@ -1,55 +1,93 @@
using System; using System;
using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Views; using CRD.Views;
using ReactiveUI; using ReactiveUI;
namespace CRD.ViewModels; namespace CRD.ViewModels;
public partial class SeriesPageViewModel : ViewModelBase{ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
public HistorySeries _selectedSeries; public HistorySeries _selectedSeries;
[ObservableProperty] [ObservableProperty]
public static bool _editMode; public static bool _editMode;
[ObservableProperty]
public static bool _sonarrAvailable;
public SeriesPageViewModel(){ public SeriesPageViewModel(){
_selectedSeries = Crunchyroll.Instance.SelectedSeries; _selectedSeries = Crunchyroll.Instance.SelectedSeries;
if (_selectedSeries.ThumbnailImage == null){ if (_selectedSeries.ThumbnailImage == null){
_selectedSeries.LoadImage(); _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] [RelayCommand]
public async Task UpdateData(string? season){ public async Task UpdateData(string? season){
await SelectedSeries.FetchData(season); await SelectedSeries.FetchData(season);
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel),false,true)); MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
} }
[RelayCommand] [RelayCommand]
public void RemoveSeason(string? season){ public void RemoveSeason(string? season){
HistorySeason? objectToRemove = SelectedSeries.Seasons.Find(se => se.SeasonId == season) ?? null; HistorySeason? objectToRemove = SelectedSeries.Seasons.Find(se => se.SeasonId == season) ?? null;
if (objectToRemove != null) { if (objectToRemove != null){
SelectedSeries.Seasons.Remove(objectToRemove); 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] [RelayCommand]
public void NavBack(){ public void NavBack(){
SelectedSeries.UpdateNewEpisodes(); 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}");
}
} }
} }

View File

@ -12,6 +12,7 @@ using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader; using CRD.Downloader;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using FluentAvalonia.Styling; using FluentAvalonia.Styling;
@ -35,7 +36,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _history; private bool _history;
[ObservableProperty] [ObservableProperty]
private bool _useNonDrmEndpoint = true; private bool _useNonDrmEndpoint = true;
@ -87,6 +88,18 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private Color _customAccentColor = Colors.SlateBlue; 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<Color> PredefinedColors{ get; } = new(){ public ObservableCollection<Color> PredefinedColors{ get; } = new(){
Color.FromRgb(255, 185, 0), Color.FromRgb(255, 185, 0),
Color.FromRgb(255, 140, 0), Color.FromRgb(255, 140, 0),
@ -193,10 +206,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
} }
CrDownloadOptions options = Crunchyroll.Instance.CrunOptions; CrDownloadOptions options = Crunchyroll.Instance.CrunOptions;
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null; ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null;
SelectedHSLang = hsLang ?? HardSubLangList[0]; SelectedHSLang = hsLang ?? HardSubLangList[0];
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList(); var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
SelectedSubLang.Clear(); SelectedSubLang.Clear();
@ -213,6 +226,14 @@ public partial class SettingsPageViewModel : ViewModelBase{
UpdateSubAndDubString(); UpdateSubAndDubString();
var props = options.SonarrProperties;
if (props != null){
SonarrUseSsl = props.UseSsl;
SonarrHost = props.Host + "";
SonarrPort = props.Port + "";
SonarrApiKey = props.ApiKey + "";
}
UseNonDrmEndpoint = options.UseNonDrmStreams; UseNonDrmEndpoint = options.UseNonDrmStreams;
DownloadVideo = !options.Novids; DownloadVideo = !options.Novids;
@ -292,6 +313,17 @@ public partial class SettingsPageViewModel : ViewModelBase{
Crunchyroll.Instance.CrunOptions.History = History; 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 //TODO - Mux Options
CfgManager.WriteSettingsToFile(); CfgManager.WriteSettingsToFile();
@ -370,8 +402,6 @@ public partial class SettingsPageViewModel : ViewModelBase{
} }
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings(); UpdateSettings();
} }
@ -416,7 +446,27 @@ public partial class SettingsPageViewModel : ViewModelBase{
UpdateSettings(); UpdateSettings();
} }
partial void OnUseNonDrmEndpointChanged(bool value){
UpdateSettings();
}
partial void OnHistoryChanged(bool value){ partial void OnHistoryChanged(bool value){
UpdateSettings(); 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();
}
} }

View File

@ -40,9 +40,36 @@
<TextBlock Grid.Row="0" FontSize="50" Text="{Binding SelectedSeries.SeriesTitle}"></TextBlock> <TextBlock Grid.Row="0" FontSize="50" Text="{Binding SelectedSeries.SeriesTitle}"></TextBlock>
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock> <TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock>
<StackPanel Grid.Row="3" Orientation="Horizontal"> <StackPanel Grid.Row="3" Orientation="Vertical">
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Fetch Series</Button>
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 0 10">Edit</ToggleButton> <StackPanel Orientation="Horizontal" Margin="0 10 10 10">
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
Command="{Binding OpenCrPage}">
<Grid>
<controls:ImageIcon Source="../Assets/crunchy_icon_round.png" Width="30" Height="30" />
</Grid>
</Button>
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
IsVisible="{Binding SonarrAvailable}"
Command="{Binding OpenSonarrPage}">
<Grid>
<controls:ImageIcon Source="../Assets/sonarr.png" Width="30" Height="30" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Fetch Series</Button>
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 0 10">Edit</ToggleButton>
</StackPanel>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -80,6 +107,20 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0"
IsVisible="{Binding $parent[ItemsControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}" Source="../Assets/sonarr.png" Width="25"
Height="25" />
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}" Source="../Assets/sonarr_inactive.png" Width="25"
Height="25" />
</StackPanel>
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent" <Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50" BorderThickness="0" CornerRadius="50"
IsVisible="{Binding !WasDownloaded}" IsVisible="{Binding !WasDownloaded}"

View File

@ -236,6 +236,40 @@
</controls:SettingsExpander.Footer> </controls:SettingsExpander.Footer>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsExpander Header="Sonarr Settings"
IconSource="Globe"
Description="Adjust sonarr settings"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Host">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrHost}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Port">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrPort}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="API Key">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrApiKey}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use SSL">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SonarrUseSsl}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="App Theme" <controls:SettingsExpander Header="App Theme"
IconSource="DarkTheme" IconSource="DarkTheme"