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;
|
||||
|
||||
public class CrAuth(Crunchyroll crunInstance){
|
||||
public class CrAuth{
|
||||
|
||||
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
|
||||
|
||||
public async Task AuthAnonymous(){
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "grant_type", "client_id" },
|
||||
|
|
|
@ -12,7 +12,10 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace CRD.Downloader;
|
||||
|
||||
public class CrEpisode(Crunchyroll crunInstance){
|
||||
public class CrEpisode(){
|
||||
|
||||
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
|
||||
|
||||
public async Task<CrunchyEpisodeList?> ParseEpisodeById(string id,string locale){
|
||||
if (crunInstance.CmsToken?.Cms == null){
|
||||
Console.WriteLine("Missing CMS Access Token");
|
||||
|
|
|
@ -13,7 +13,10 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace CRD.Downloader;
|
||||
|
||||
public class CrSeries(Crunchyroll crunInstance){
|
||||
public class CrSeries(){
|
||||
|
||||
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
|
||||
|
||||
public async Task<List<CrunchyEpMeta>> DownloadFromSeriesId(string id, CrunchyMultiDownload data){
|
||||
var series = await ListSeriesId(id, "" ,data);
|
||||
|
||||
|
@ -353,7 +356,7 @@ public class CrSeries(Crunchyroll crunInstance){
|
|||
}
|
||||
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
|
||||
query["preferred_audio_language"] = "ja-JP";
|
||||
if (!string.IsNullOrEmpty(locale)){
|
||||
query["locale"] = Languages.Locale2language(locale).CrLocale;
|
||||
|
|
|
@ -17,6 +17,8 @@ using CRD.Utils.CustomList;
|
|||
using CRD.Utils.DRM;
|
||||
using CRD.Utils.HLS;
|
||||
using CRD.Utils.Muxing;
|
||||
using CRD.Utils.Sonarr;
|
||||
using CRD.Utils.Sonarr.Models;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.ViewModels;
|
||||
using CRD.Views;
|
||||
|
@ -62,6 +64,8 @@ public class Crunchyroll{
|
|||
Seasons =[]
|
||||
};
|
||||
|
||||
public List<SonarrSeries> SonarrSeries =[];
|
||||
|
||||
#endregion
|
||||
|
||||
public string DefaultLocale = "en";
|
||||
|
@ -101,10 +105,10 @@ public class Crunchyroll{
|
|||
public async Task Init(){
|
||||
_widevine = Widevine.Instance;
|
||||
|
||||
CrAuth = new CrAuth(Instance);
|
||||
CrEpisode = new CrEpisode(Instance);
|
||||
CrSeries = new CrSeries(Instance);
|
||||
CrHistory = new History(Instance);
|
||||
CrAuth = new CrAuth();
|
||||
CrEpisode = new CrEpisode();
|
||||
CrSeries = new CrSeries();
|
||||
CrHistory = new History();
|
||||
|
||||
Profile = new CrProfile{
|
||||
Username = "???",
|
||||
|
@ -146,6 +150,7 @@ public class Crunchyroll{
|
|||
CrunOptions.Theme = "System";
|
||||
CrunOptions.SelectedCalendarLanguage = "de";
|
||||
CrunOptions.DlVideoOnce = true;
|
||||
CrunOptions.UseNonDrmStreams = true;
|
||||
|
||||
CrunOptions.History = true;
|
||||
|
||||
|
@ -155,6 +160,8 @@ public class Crunchyroll{
|
|||
if (File.Exists(CfgManager.PathCrHistory)){
|
||||
HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(File.ReadAllText(CfgManager.PathCrHistory)) ??[];
|
||||
}
|
||||
|
||||
RefreshSonarr();
|
||||
}
|
||||
|
||||
|
||||
|
@ -173,16 +180,14 @@ public class Crunchyroll{
|
|||
};
|
||||
}
|
||||
|
||||
// public async void TestMethode(){
|
||||
// // One Pice - GRMG8ZQZR
|
||||
// // Studio - G9VHN9QWQ
|
||||
// var episodesMeta = await DownloadFromSeriesId("G9VHN9QWQ", new CrunchyMultiDownload(Crunchy.Instance.CrunOptions.dubLang, true));
|
||||
//
|
||||
//
|
||||
// foreach (var crunchyEpMeta in episodesMeta){
|
||||
// await DownloadEpisode(crunchyEpMeta, CrunOptions, false);
|
||||
// }
|
||||
// }
|
||||
public async void RefreshSonarr(){
|
||||
if (CrunOptions.SonarrProperties != null && !string.IsNullOrEmpty(CrunOptions.SonarrProperties.ApiKey)){
|
||||
SonarrClient.Instance.SetApiUrl();
|
||||
SonarrSeries = await SonarrClient.Instance.GetSeries();
|
||||
CrHistory.MatchHistorySeriesWithSonarr(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<CalendarWeek> GetCalendarForDate(string weeksMondayDate, bool forceUpdate){
|
||||
if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Sonarr;
|
||||
using CRD.Utils.Sonarr.Models;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Views;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -17,11 +16,13 @@ using ReactiveUI;
|
|||
|
||||
namespace CRD.Downloader;
|
||||
|
||||
public class History(Crunchyroll crunInstance){
|
||||
public class History(){
|
||||
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
|
||||
|
||||
public async Task UpdateSeries(string seriesId, string? seasonId){
|
||||
await crunInstance.CrAuth.RefreshToken(true);
|
||||
|
||||
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja");
|
||||
|
||||
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja");
|
||||
|
||||
if (parsedSeries == null){
|
||||
Console.WriteLine("Parse Data Invalid");
|
||||
|
@ -43,11 +44,12 @@ public class History(Crunchyroll crunInstance){
|
|||
if (sVersion.Guid != null){
|
||||
sId = sVersion.Guid;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId);
|
||||
UpdateWithSeasonData(seasonData);
|
||||
}
|
||||
|
@ -103,12 +105,22 @@ public class History(Crunchyroll crunInstance){
|
|||
if (historySeries != null){
|
||||
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == episode.SeasonId);
|
||||
|
||||
var series = await crunInstance.CrSeries.SeriesById(seriesId);
|
||||
if (series?.Data != null){
|
||||
historySeries.SeriesTitle = series.Data.First().Title;
|
||||
}
|
||||
|
||||
if (historySeason != null){
|
||||
historySeason.SeasonTitle = episode.SeasonTitle;
|
||||
historySeason.SeasonNum = episode.SeasonNumber + "";
|
||||
if (historySeason.EpisodesList.All(e => e.EpisodeId != episode.Id)){
|
||||
var newHistoryEpisode = new HistoryEpisode{
|
||||
EpisodeTitle = episode.Title,
|
||||
EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title,
|
||||
EpisodeDescription = episode.Description,
|
||||
EpisodeId = episode.Id,
|
||||
Episode = episode.Episode,
|
||||
EpisodeSeasonNum = episode.SeasonNumber + "",
|
||||
SpecialEpisode = !int.TryParse(episode.Episode, out _),
|
||||
};
|
||||
|
||||
historySeason.EpisodesList.Add(newHistoryEpisode);
|
||||
|
@ -122,6 +134,7 @@ public class History(Crunchyroll crunInstance){
|
|||
|
||||
historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList();
|
||||
}
|
||||
|
||||
historySeries.UpdateNewEpisodes();
|
||||
} else{
|
||||
var newHistorySeries = new HistorySeries{
|
||||
|
@ -136,6 +149,7 @@ public class History(Crunchyroll crunInstance){
|
|||
if (series?.Data != null){
|
||||
newHistorySeries.SeriesDescription = series.Data.First().Description;
|
||||
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
|
||||
newHistorySeries.SeriesTitle = series.Data.First().Title;
|
||||
}
|
||||
|
||||
newHistorySeries.Seasons.Add(newSeason);
|
||||
|
@ -148,6 +162,7 @@ public class History(Crunchyroll crunInstance){
|
|||
crunInstance.HistoryList.Add(item);
|
||||
}
|
||||
|
||||
MatchHistorySeriesWithSonarr(false);
|
||||
UpdateHistoryFile();
|
||||
}
|
||||
|
||||
|
@ -158,27 +173,38 @@ public class History(Crunchyroll crunInstance){
|
|||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||
if (historySeries != null){
|
||||
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == firstEpisode.SeasonId);
|
||||
var series = await crunInstance.CrSeries.SeriesById(seriesId);
|
||||
if (series?.Data != null){
|
||||
historySeries.SeriesTitle = series.Data.First().Title;
|
||||
}
|
||||
|
||||
if (historySeason != null){
|
||||
historySeason.SeasonTitle = firstEpisode.SeasonTitle;
|
||||
historySeason.SeasonNum = firstEpisode.SeasonNumber + "";
|
||||
foreach (var crunchyEpisode in seasonData.Data){
|
||||
|
||||
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id);
|
||||
|
||||
|
||||
if (historyEpisode == null){
|
||||
var newHistoryEpisode = new HistoryEpisode{
|
||||
EpisodeTitle = crunchyEpisode.Title,
|
||||
EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title,
|
||||
EpisodeDescription = crunchyEpisode.Description,
|
||||
EpisodeId = crunchyEpisode.Id,
|
||||
Episode = crunchyEpisode.Episode,
|
||||
EpisodeSeasonNum = crunchyEpisode.SeasonNumber + "",
|
||||
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
|
||||
};
|
||||
|
||||
historySeason.EpisodesList.Add(newHistoryEpisode);
|
||||
} else{
|
||||
//Update existing episode
|
||||
historyEpisode.EpisodeTitle = crunchyEpisode.Title;
|
||||
historyEpisode.EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title;
|
||||
historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _);
|
||||
historyEpisode.EpisodeDescription = crunchyEpisode.Description;
|
||||
historyEpisode.EpisodeId = crunchyEpisode.Id;
|
||||
historyEpisode.Episode = crunchyEpisode.Episode;
|
||||
historyEpisode.EpisodeSeasonNum = crunchyEpisode.SeasonNumber + "";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
historySeason.EpisodesList.Sort(new NumericStringPropertyComparer());
|
||||
|
@ -191,6 +217,7 @@ public class History(Crunchyroll crunInstance){
|
|||
|
||||
historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList();
|
||||
}
|
||||
|
||||
historySeries.UpdateNewEpisodes();
|
||||
} else{
|
||||
var newHistorySeries = new HistorySeries{
|
||||
|
@ -208,11 +235,12 @@ public class History(Crunchyroll crunInstance){
|
|||
if (series?.Data != null){
|
||||
newHistorySeries.SeriesDescription = series.Data.First().Description;
|
||||
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
|
||||
newHistorySeries.SeriesTitle = series.Data.First().Title;
|
||||
}
|
||||
|
||||
|
||||
newHistorySeries.Seasons.Add(newSeason);
|
||||
|
||||
|
||||
newHistorySeries.UpdateNewEpisodes();
|
||||
}
|
||||
}
|
||||
|
@ -223,6 +251,7 @@ public class History(Crunchyroll crunInstance){
|
|||
crunInstance.HistoryList.Add(item);
|
||||
}
|
||||
|
||||
MatchHistorySeriesWithSonarr(false);
|
||||
UpdateHistoryFile();
|
||||
}
|
||||
|
||||
|
@ -255,9 +284,11 @@ public class History(Crunchyroll crunInstance){
|
|||
|
||||
foreach (var crunchyEpisode in seasonData.Data!){
|
||||
var newHistoryEpisode = new HistoryEpisode{
|
||||
EpisodeTitle = crunchyEpisode.Title,
|
||||
EpisodeTitle = crunchyEpisode.Identifier.Contains("|M|") ? crunchyEpisode.SeasonTitle : crunchyEpisode.Title,
|
||||
EpisodeDescription = crunchyEpisode.Description,
|
||||
EpisodeId = crunchyEpisode.Id,
|
||||
Episode = crunchyEpisode.Episode,
|
||||
EpisodeSeasonNum = firstEpisode.SeasonNumber + "",
|
||||
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
|
||||
};
|
||||
|
||||
|
@ -276,9 +307,11 @@ public class History(Crunchyroll crunInstance){
|
|||
};
|
||||
|
||||
var newHistoryEpisode = new HistoryEpisode{
|
||||
EpisodeTitle = episode.Title,
|
||||
EpisodeTitle = episode.Identifier.Contains("|M|") ? episode.SeasonTitle : episode.Title,
|
||||
EpisodeDescription = episode.Description,
|
||||
EpisodeId = episode.Id,
|
||||
Episode = episode.Episode,
|
||||
EpisodeSeasonNum = episode.SeasonNumber + "",
|
||||
SpecialEpisode = !int.TryParse(episode.Episode, out _),
|
||||
};
|
||||
|
||||
|
@ -287,6 +320,170 @@ public class History(Crunchyroll crunInstance){
|
|||
|
||||
return newSeason;
|
||||
}
|
||||
|
||||
public void MatchHistorySeriesWithSonarr(bool updateAll){
|
||||
foreach (var historySeries in crunInstance.HistoryList){
|
||||
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle);
|
||||
if (sonarrSeries != null){
|
||||
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
|
||||
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
|
||||
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){
|
||||
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
|
||||
|
||||
List<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>{
|
||||
|
@ -294,7 +491,7 @@ public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
|
|||
if (int.TryParse(x.Episode, out int xInt) && int.TryParse(y.Episode, out int yInt)){
|
||||
return xInt.CompareTo(yInt);
|
||||
}
|
||||
|
||||
|
||||
// Fall back to string comparison if not parseable as integers
|
||||
return String.Compare(x.Episode, y.Episode, StringComparison.Ordinal);
|
||||
}
|
||||
|
@ -307,6 +504,15 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
[JsonProperty("series_id")]
|
||||
public string? SeriesId{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_series_id")]
|
||||
public string? SonarrSeriesId{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_tvdb_id")]
|
||||
public string? SonarrTvDbId{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_slug_title")]
|
||||
public string? SonarrSlugTitle{ get; set; }
|
||||
|
||||
[JsonProperty("series_description")]
|
||||
public string? SeriesDescription{ get; set; }
|
||||
|
||||
|
@ -346,18 +552,16 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
// Iterate over the Seasons list from the end to the beginning
|
||||
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
|
||||
|
||||
if (Seasons[i].SpecialSeason == true){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Iterate over the Episodes from the end to the beginning
|
||||
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
|
||||
|
||||
if (Seasons[i].EpisodesList[j].SpecialEpisode){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!Seasons[i].EpisodesList[j].WasDownloaded){
|
||||
count++;
|
||||
} else{
|
||||
|
@ -365,27 +569,26 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
NewEpisodes = count;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
|
||||
}
|
||||
|
||||
|
||||
public async Task AddNewMissingToDownloads(){
|
||||
bool foundWatched = false;
|
||||
|
||||
// Iterate over the Seasons list from the end to the beginning
|
||||
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
|
||||
|
||||
if (Seasons[i].SpecialSeason == true){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Iterate over the Episodes from the end to the beginning
|
||||
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
|
||||
|
||||
if (Seasons[i].EpisodesList[j].SpecialEpisode){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!Seasons[i].EpisodesList[j].WasDownloaded){
|
||||
//ADD to download queue
|
||||
await Seasons[i].EpisodesList[j].DownloadEpisode();
|
||||
|
@ -410,9 +613,10 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
|
||||
[JsonProperty("season_cr_season_number")]
|
||||
public string? SeasonNum{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("season_special_season")]
|
||||
public bool? SpecialSeason{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
|
||||
|
||||
|
@ -450,15 +654,36 @@ public partial class HistoryEpisode : INotifyPropertyChanged{
|
|||
|
||||
[JsonProperty("episode_cr_episode_number")]
|
||||
public string? Episode{ get; set; }
|
||||
|
||||
[JsonProperty("episode_cr_episode_description")]
|
||||
public string? EpisodeDescription{ get; set; }
|
||||
|
||||
[JsonProperty("episode_cr_season_number")]
|
||||
public string? EpisodeSeasonNum{ get; set; }
|
||||
|
||||
[JsonProperty("episode_was_downloaded")]
|
||||
public bool WasDownloaded{ get; set; }
|
||||
|
||||
[JsonProperty("episode_special_episode")]
|
||||
public bool SpecialEpisode{ get; set; }
|
||||
public bool SpecialEpisode{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_episode_id")]
|
||||
public string? SonarrEpisodeId{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_has_file")]
|
||||
public bool SonarrHasFile{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_episode_number")]
|
||||
public string? SonarrEpisodeNumber{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_season_number")]
|
||||
public string? SonarrSeasonNumber{ get; set; }
|
||||
|
||||
[JsonProperty("sonarr_absolut_number")]
|
||||
public string? SonarrAbsolutNumber{ get; set; }
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
|
||||
public void ToggleWasDownloaded(){
|
||||
WasDownloaded = !WasDownloaded;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
|
||||
|
@ -466,6 +691,5 @@ public partial class HistoryEpisode : INotifyPropertyChanged{
|
|||
|
||||
public async Task DownloadEpisode(){
|
||||
await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang);
|
||||
|
||||
}
|
||||
}
|
|
@ -8,25 +8,62 @@ namespace CRD.Utils;
|
|||
[DataContract]
|
||||
[JsonConverter(typeof(LocaleConverter))]
|
||||
public enum Locale{
|
||||
[EnumMember(Value = "")] DefaulT,
|
||||
[EnumMember(Value = "un")] Unknown,
|
||||
[EnumMember(Value = "en-US")] EnUs,
|
||||
[EnumMember(Value = "es-LA")] EsLa,
|
||||
[EnumMember(Value = "es-419")] Es419,
|
||||
[EnumMember(Value = "es-ES")] EsEs,
|
||||
[EnumMember(Value = "pt-BR")] PtBr,
|
||||
[EnumMember(Value = "fr-FR")] FrFr,
|
||||
[EnumMember(Value = "de-DE")] DeDe,
|
||||
[EnumMember(Value = "ar-ME")] ArMe,
|
||||
[EnumMember(Value = "ar-SA")] ArSa,
|
||||
[EnumMember(Value = "it-IT")] ItIt,
|
||||
[EnumMember(Value = "ru-RU")] RuRu,
|
||||
[EnumMember(Value = "tr-TR")] TrTr,
|
||||
[EnumMember(Value = "hi-IN")] HiIn,
|
||||
[EnumMember(Value = "zh-CN")] ZhCn,
|
||||
[EnumMember(Value = "ko-KR")] KoKr,
|
||||
[EnumMember(Value = "ja-JP")] JaJp,
|
||||
[EnumMember(Value = "id-ID")] IdId,
|
||||
[EnumMember(Value = "")]
|
||||
DefaulT,
|
||||
|
||||
[EnumMember(Value = "un")]
|
||||
Unknown,
|
||||
|
||||
[EnumMember(Value = "en-US")]
|
||||
EnUs,
|
||||
|
||||
[EnumMember(Value = "es-LA")]
|
||||
EsLa,
|
||||
|
||||
[EnumMember(Value = "es-419")]
|
||||
Es419,
|
||||
|
||||
[EnumMember(Value = "es-ES")]
|
||||
EsEs,
|
||||
|
||||
[EnumMember(Value = "pt-BR")]
|
||||
PtBr,
|
||||
|
||||
[EnumMember(Value = "fr-FR")]
|
||||
FrFr,
|
||||
|
||||
[EnumMember(Value = "de-DE")]
|
||||
DeDe,
|
||||
|
||||
[EnumMember(Value = "ar-ME")]
|
||||
ArMe,
|
||||
|
||||
[EnumMember(Value = "ar-SA")]
|
||||
ArSa,
|
||||
|
||||
[EnumMember(Value = "it-IT")]
|
||||
ItIt,
|
||||
|
||||
[EnumMember(Value = "ru-RU")]
|
||||
RuRu,
|
||||
|
||||
[EnumMember(Value = "tr-TR")]
|
||||
TrTr,
|
||||
|
||||
[EnumMember(Value = "hi-IN")]
|
||||
HiIn,
|
||||
|
||||
[EnumMember(Value = "zh-CN")]
|
||||
ZhCn,
|
||||
|
||||
[EnumMember(Value = "ko-KR")]
|
||||
KoKr,
|
||||
|
||||
[EnumMember(Value = "ja-JP")]
|
||||
JaJp,
|
||||
|
||||
[EnumMember(Value = "id-ID")]
|
||||
IdId,
|
||||
}
|
||||
|
||||
public static class EnumExtensions{
|
||||
|
@ -49,34 +86,67 @@ public static class EnumExtensions{
|
|||
|
||||
[DataContract]
|
||||
public enum ChannelId{
|
||||
[EnumMember(Value = "crunchyroll")] Crunchyroll,
|
||||
[EnumMember(Value = "crunchyroll")]
|
||||
Crunchyroll,
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public enum ImageType{
|
||||
[EnumMember(Value = "poster_tall")] PosterTall,
|
||||
[EnumMember(Value = "poster_tall")]
|
||||
PosterTall,
|
||||
|
||||
[EnumMember(Value = "poster_wide")] PosterWide,
|
||||
[EnumMember(Value = "poster_wide")]
|
||||
PosterWide,
|
||||
|
||||
[EnumMember(Value = "promo_image")] PromoImage,
|
||||
[EnumMember(Value = "promo_image")]
|
||||
PromoImage,
|
||||
|
||||
[EnumMember(Value = "thumbnail")] Thumbnail,
|
||||
[EnumMember(Value = "thumbnail")]
|
||||
Thumbnail,
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public enum MaturityRating{
|
||||
[EnumMember(Value = "TV-14")] Tv14,
|
||||
[EnumMember(Value = "TV-14")]
|
||||
Tv14,
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public enum MediaType{
|
||||
[EnumMember(Value = "episode")] Episode,
|
||||
[EnumMember(Value = "episode")]
|
||||
Episode,
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public enum DownloadMediaType{
|
||||
[EnumMember(Value = "Video")] Video,
|
||||
[EnumMember(Value = "Audio")] Audio,
|
||||
[EnumMember(Value = "Chapters")] Chapters,
|
||||
[EnumMember(Value = "Subtitle")] Subtitle,
|
||||
}
|
||||
[EnumMember(Value = "Video")]
|
||||
Video,
|
||||
|
||||
[EnumMember(Value = "Audio")]
|
||||
Audio,
|
||||
|
||||
[EnumMember(Value = "Chapters")]
|
||||
Chapters,
|
||||
|
||||
[EnumMember(Value = "Subtitle")]
|
||||
Subtitle,
|
||||
}
|
||||
|
||||
public enum SonarrCoverType{
|
||||
Banner,
|
||||
FanArt,
|
||||
Poster,
|
||||
ClearLogo,
|
||||
}
|
||||
|
||||
public enum SonarrSeriesType{
|
||||
Anime,
|
||||
Standard,
|
||||
Daily
|
||||
}
|
||||
|
||||
public enum SonarrStatus{
|
||||
Continuing,
|
||||
Upcoming,
|
||||
Ended
|
||||
};
|
|
@ -142,6 +142,7 @@ public class CfgManager{
|
|||
Crunchyroll.Instance.CrunOptions.AccentColor = loadedOptions.AccentColor;
|
||||
Crunchyroll.Instance.CrunOptions.History = loadedOptions.History;
|
||||
Crunchyroll.Instance.CrunOptions.UseNonDrmStreams = loadedOptions.UseNonDrmStreams;
|
||||
Crunchyroll.Instance.CrunOptions.SonarrProperties = loadedOptions.SonarrProperties;
|
||||
}
|
||||
|
||||
private static object fileLock = new object();
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -14,9 +16,9 @@ public class Helpers{
|
|||
/// <param name="json">The JSON string to deserialize.</param>
|
||||
/// <param name="serializerSettings">The settings for deserialization if null default settings will be used</param>
|
||||
/// <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{
|
||||
return JsonConvert.DeserializeObject<T>(json,serializerSettings);
|
||||
return JsonConvert.DeserializeObject<T>(json, serializerSettings);
|
||||
} catch (JsonException ex){
|
||||
Console.WriteLine($"Error deserializing JSON: {ex.Message}");
|
||||
throw;
|
||||
|
@ -77,4 +79,42 @@ public class Helpers{
|
|||
return (IsOk: isSuccess, ErrorCode: process.ExitCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static double CalculateCosineSimilarity(string text1, string text2){
|
||||
var vector1 = ComputeWordFrequency(text1);
|
||||
var vector2 = ComputeWordFrequency(text2);
|
||||
|
||||
return CosineSimilarity(vector1, vector2);
|
||||
}
|
||||
|
||||
private static Dictionary<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 CRD.Utils.Sonarr;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace CRD.Utils.Structs;
|
||||
|
@ -115,4 +116,7 @@ public class CrDownloadOptions{
|
|||
[YamlMember(Alias = "user_non_drm_streams", ApplyNamingConventions = false)]
|
||||
public bool UseNonDrmStreams{ get; set; }
|
||||
|
||||
[YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)]
|
||||
public SonarrProperties? SonarrProperties{ get; set; }
|
||||
|
||||
}
|
|
@ -39,7 +39,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
private CrunchySeriesList? currentSeriesList;
|
||||
|
||||
public AddDownloadPageViewModel(){
|
||||
// Items.Add(new ItemModel("", "Test", "22:33", "Test", "S1", "E1", 1, new List<string>()));
|
||||
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,55 +1,93 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Sonarr;
|
||||
using CRD.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace CRD.ViewModels;
|
||||
|
||||
public partial class SeriesPageViewModel : ViewModelBase{
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
public HistorySeries _selectedSeries;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
public static bool _editMode;
|
||||
|
||||
[ObservableProperty]
|
||||
public static bool _sonarrAvailable;
|
||||
|
||||
public SeriesPageViewModel(){
|
||||
_selectedSeries = Crunchyroll.Instance.SelectedSeries;
|
||||
|
||||
|
||||
if (_selectedSeries.ThumbnailImage == null){
|
||||
_selectedSeries.LoadImage();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){
|
||||
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0;
|
||||
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(true,SelectedSeries);
|
||||
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
|
||||
} else{
|
||||
SonarrAvailable = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenSonarrPage(){
|
||||
var sonarrProp = Crunchyroll.Instance.CrunOptions.SonarrProperties;
|
||||
|
||||
if (sonarrProp == null) return;
|
||||
|
||||
OpenUrl($"http{(sonarrProp.UseSsl ? "s" : "")}://{sonarrProp.Host}:{sonarrProp.Port}{(sonarrProp.UrlBase ?? "")}/series/{SelectedSeries.SonarrSlugTitle}");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenCrPage(){
|
||||
|
||||
OpenUrl($"https://www.crunchyroll.com/series/{SelectedSeries.SeriesId}");
|
||||
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task UpdateData(string? season){
|
||||
await SelectedSeries.FetchData(season);
|
||||
|
||||
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel),false,true));
|
||||
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public void RemoveSeason(string? season){
|
||||
|
||||
HistorySeason? objectToRemove = SelectedSeries.Seasons.Find(se => se.SeasonId == season) ?? null;
|
||||
if (objectToRemove != null) {
|
||||
if (objectToRemove != null){
|
||||
SelectedSeries.Seasons.Remove(objectToRemove);
|
||||
}
|
||||
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
|
||||
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel),false,true));
|
||||
|
||||
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
|
||||
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public void NavBack(){
|
||||
SelectedSeries.UpdateNewEpisodes();
|
||||
MessageBus.Current.SendMessage(new NavigationMessage(null,true,false));
|
||||
MessageBus.Current.SendMessage(new NavigationMessage(null, true, false));
|
||||
}
|
||||
|
||||
|
||||
private void OpenUrl(string url){
|
||||
try{
|
||||
Process.Start(new ProcessStartInfo{
|
||||
FileName = url,
|
||||
UseShellExecute = true
|
||||
});
|
||||
} catch (Exception e){
|
||||
Console.WriteLine($"An error occurred: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,7 @@ using Avalonia.Styling;
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CRD.Downloader;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Sonarr;
|
||||
using CRD.Utils.Structs;
|
||||
using FluentAvalonia.Styling;
|
||||
|
||||
|
@ -35,7 +36,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _history;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _useNonDrmEndpoint = true;
|
||||
|
||||
|
@ -87,6 +88,18 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private Color _customAccentColor = Colors.SlateBlue;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _sonarrHost = "localhost";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _sonarrPort = "8989";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _sonarrApiKey = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _sonarrUseSsl = false;
|
||||
|
||||
public ObservableCollection<Color> PredefinedColors{ get; } = new(){
|
||||
Color.FromRgb(255, 185, 0),
|
||||
Color.FromRgb(255, 140, 0),
|
||||
|
@ -193,10 +206,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
CrDownloadOptions options = Crunchyroll.Instance.CrunOptions;
|
||||
|
||||
|
||||
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null;
|
||||
SelectedHSLang = hsLang ?? HardSubLangList[0];
|
||||
|
||||
|
||||
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
|
||||
|
||||
SelectedSubLang.Clear();
|
||||
|
@ -213,6 +226,14 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
|
||||
UpdateSubAndDubString();
|
||||
|
||||
var props = options.SonarrProperties;
|
||||
|
||||
if (props != null){
|
||||
SonarrUseSsl = props.UseSsl;
|
||||
SonarrHost = props.Host + "";
|
||||
SonarrPort = props.Port + "";
|
||||
SonarrApiKey = props.ApiKey + "";
|
||||
}
|
||||
|
||||
UseNonDrmEndpoint = options.UseNonDrmStreams;
|
||||
DownloadVideo = !options.Novids;
|
||||
|
@ -292,6 +313,17 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
|
||||
Crunchyroll.Instance.CrunOptions.History = History;
|
||||
|
||||
var props = new SonarrProperties();
|
||||
|
||||
props.UseSsl = SonarrUseSsl;
|
||||
props.Host = SonarrHost;
|
||||
props.Port = Convert.ToInt32(SonarrPort);
|
||||
props.ApiKey = SonarrApiKey;
|
||||
|
||||
Crunchyroll.Instance.CrunOptions.SonarrProperties = props;
|
||||
|
||||
Crunchyroll.Instance.RefreshSonarr();
|
||||
|
||||
//TODO - Mux Options
|
||||
|
||||
CfgManager.WriteSettingsToFile();
|
||||
|
@ -370,8 +402,6 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
|
||||
UpdateSettings();
|
||||
}
|
||||
|
@ -416,7 +446,27 @@ public partial class SettingsPageViewModel : ViewModelBase{
|
|||
UpdateSettings();
|
||||
}
|
||||
|
||||
partial void OnUseNonDrmEndpointChanged(bool value){
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
partial void OnHistoryChanged(bool value){
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
partial void OnSonarrHostChanged(string value){
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
partial void OnSonarrPortChanged(string value){
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
partial void OnSonarrApiKeyChanged(string value){
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
partial void OnSonarrUseSslChanged(bool value){
|
||||
UpdateSettings();
|
||||
}
|
||||
}
|
|
@ -40,9 +40,36 @@
|
|||
|
||||
<TextBlock Grid.Row="0" FontSize="50" Text="{Binding SelectedSeries.SeriesTitle}"></TextBlock>
|
||||
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock>
|
||||
<StackPanel Grid.Row="3" 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 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>
|
||||
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 0 10">Edit</ToggleButton>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
@ -80,6 +107,20 @@
|
|||
|
||||
<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"
|
||||
BorderThickness="0" CornerRadius="50"
|
||||
IsVisible="{Binding !WasDownloaded}"
|
||||
|
|
|
@ -236,6 +236,40 @@
|
|||
|
||||
</controls:SettingsExpander.Footer>
|
||||
</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"
|
||||
IconSource="DarkTheme"
|
||||
|
|
Loading…
Reference in New Issue