Add - Added Sonarr support
This commit is contained in:
parent
461b626e79
commit
095054697d
Binary file not shown.
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -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" },
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)){
|
||||||
|
|
|
@ -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,7 +16,9 @@ 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);
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ public class History(Crunchyroll crunInstance){
|
||||||
if (sVersion.Guid != null){
|
if (sVersion.Guid != null){
|
||||||
sId = sVersion.Guid;
|
sId = sVersion.Guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,28 +173,39 @@ 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());
|
||||||
} else{
|
} else{
|
||||||
|
@ -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,6 +235,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>{
|
||||||
|
@ -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,14 +552,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -365,6 +569,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NewEpisodes = count;
|
NewEpisodes = count;
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
|
||||||
}
|
}
|
||||||
|
@ -374,14 +579,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -413,6 +616,7 @@ public class HistorySeason : INotifyPropertyChanged{
|
||||||
|
|
||||||
[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}";
|
||||||
|
|
||||||
|
@ -451,12 +655,33 @@ 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(){
|
||||||
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
};
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,57 @@
|
||||||
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]
|
||||||
|
@ -35,21 +63,31 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
||||||
|
|
||||||
[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);
|
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
|
||||||
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -40,11 +40,38 @@
|
||||||
|
|
||||||
<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">
|
||||||
|
|
||||||
|
<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>
|
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Fetch Series</Button>
|
||||||
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 0 10">Edit</ToggleButton>
|
<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}"
|
||||||
|
|
|
@ -237,6 +237,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"
|
||||||
Description="Change the current app theme">
|
Description="Change the current app theme">
|
||||||
|
|
Loading…
Reference in New Issue