Add - Added setting to use sonarr numbering instead of crunchyroll numbering

Chg - History "Refresh All" now shows in more detail what is being updated
Chg - Calendar design changes - highlight "Premiere" episodes
Fix - Crash caused by using sonarr
Fix - Memory leak caused by progress bar
Fix - Sometimes it downloaded Hardsub because it didn't know the language
This commit is contained in:
Elwador 2024-06-11 23:58:44 +02:00
parent 9e975062dc
commit 7b021940c3
19 changed files with 546 additions and 309 deletions

View File

@ -4,16 +4,14 @@
xmlns:sty="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia" xmlns:sty="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
x:Class="CRD.App" x:Class="CRD.App"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates> <Application.DataTemplates>
<crd:ViewLocator/> <crd:ViewLocator />
</Application.DataTemplates> </Application.DataTemplates>
<Application.Styles> <Application.Styles>
<sty:FluentAvaloniaTheme/> <sty:FluentAvaloniaTheme PreferSystemTheme="True" PreferUserAccentColor="True"/>
<StyleInclude Source="avares://CRD/Styling/ControlsGalleryStyles.axaml" /> <StyleInclude Source="avares://CRD/Styling/ControlsGalleryStyles.axaml" />
<StyleInclude Source="avares://CRD/Assets/Icons.axaml"></StyleInclude> <StyleInclude Source="avares://CRD/Assets/Icons.axaml"></StyleInclude>
</Application.Styles> </Application.Styles>

View File

@ -47,7 +47,7 @@ public class CrEpisode(){
} }
public CrunchySeriesList EpisodeData(CrunchyEpisodeList dlEpisodes){ public async Task<CrunchySeriesList> EpisodeData(CrunchyEpisodeList dlEpisodes){
bool serieshasversions = true; bool serieshasversions = true;
Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>(); Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
@ -56,7 +56,7 @@ public class CrEpisode(){
foreach (var episode in dlEpisodes.Data){ foreach (var episode in dlEpisodes.Data){
if (crunInstance.CrunOptions.History){ if (crunInstance.CrunOptions.History){
crunInstance.CrHistory.UpdateWithEpisode(episode); await crunInstance.CrHistory.UpdateWithEpisode(episode);
} }
// Prepare the episode array // Prepare the episode array

View File

@ -164,7 +164,6 @@ public class Crunchyroll{
RefreshSonarr(); RefreshSonarr();
} }
calendarLanguage = new(){ calendarLanguage = new(){
{ "en-us", "https://www.crunchyroll.com/simulcastcalendar" }, { "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
{ "es", "https://www.crunchyroll.com/es/simulcastcalendar" }, { "es", "https://www.crunchyroll.com/es/simulcastcalendar" },
@ -181,8 +180,8 @@ public class Crunchyroll{
} }
public async void RefreshSonarr(){ public async void RefreshSonarr(){
if (CrunOptions.SonarrProperties != null && !string.IsNullOrEmpty(CrunOptions.SonarrProperties.ApiKey)){ await SonarrClient.Instance.CheckSonarrSettings();
SonarrClient.Instance.SetApiUrl(); if (CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
SonarrSeries = await SonarrClient.Instance.GetSeries(); SonarrSeries = await SonarrClient.Instance.GetSeries();
CrHistory.MatchHistorySeriesWithSonarr(true); CrHistory.MatchHistorySeriesWithSonarr(true);
} }
@ -221,8 +220,6 @@ public class Crunchyroll{
var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim(); var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim();
// Console.WriteLine($"Day: {dayName}, Date: {date}");
CalendarDay calDay = new CalendarDay(); CalendarDay calDay = new CalendarDay();
calDay.CalendarEpisodes = new List<CalendarEpisode>(); calDay.CalendarEpisodes = new List<CalendarEpisode>();
@ -242,14 +239,10 @@ public class Crunchyroll{
var episodeLink = episode.SelectSingleNode(".//a[contains(@class, 'available-episode-link')]")?.GetAttributeValue("href", "No link"); var episodeLink = episode.SelectSingleNode(".//a[contains(@class, 'available-episode-link')]")?.GetAttributeValue("href", "No link");
var thumbnailUrl = episode.SelectSingleNode(".//img[contains(@class, 'thumbnail')]")?.GetAttributeValue("src", "No image"); var thumbnailUrl = episode.SelectSingleNode(".//img[contains(@class, 'thumbnail')]")?.GetAttributeValue("src", "No image");
var isPremiumOnly = episode.SelectSingleNode(".//svg[contains(@class, 'premium-flag')]") != null; var isPremiumOnly = episode.SelectSingleNode(".//svg[contains(@class, 'premium-flag')]") != null;
var isPremiere = episode.SelectSingleNode(".//div[contains(@class, 'premiere-flag')]") != null;
var seasonName = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim(); var seasonName = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim();
var episodeNumber = episode.SelectSingleNode(".//meta[contains(@itemprop, 'episodeNumber')]")?.GetAttributeValue("content", "?"); var episodeNumber = episode.SelectSingleNode(".//meta[contains(@itemprop, 'episodeNumber')]")?.GetAttributeValue("content", "?");
// Console.WriteLine($" Time: {episodeTime} (Has Passed: {hasPassed}), Episode: {episodeName}");
// Console.WriteLine($" Season Link: {seasonLink}");
// Console.WriteLine($" Episode Link: {episodeLink}");
// Console.WriteLine($" Thumbnail URL: {thumbnailUrl}");
CalendarEpisode calEpisode = new CalendarEpisode(); CalendarEpisode calEpisode = new CalendarEpisode();
calEpisode.DateTime = episodeTime; calEpisode.DateTime = episodeTime;
@ -259,6 +252,7 @@ public class Crunchyroll{
calEpisode.EpisodeUrl = episodeLink; calEpisode.EpisodeUrl = episodeLink;
calEpisode.ThumbnailUrl = thumbnailUrl; calEpisode.ThumbnailUrl = thumbnailUrl;
calEpisode.IsPremiumOnly = isPremiumOnly; calEpisode.IsPremiumOnly = isPremiumOnly;
calEpisode.IsPremiere = isPremiere;
calEpisode.SeasonName = seasonName; calEpisode.SeasonName = seasonName;
calEpisode.EpisodeNumber = episodeNumber; calEpisode.EpisodeNumber = episodeNumber;
@ -267,7 +261,6 @@ public class Crunchyroll{
} }
week.CalendarDays.Add(calDay); week.CalendarDays.Add(calDay);
// Console.WriteLine();
} }
} else{ } else{
Console.WriteLine("No days found in the HTML document."); Console.WriteLine("No days found in the HTML document.");
@ -291,10 +284,19 @@ public class Crunchyroll{
return; return;
} }
var sList = CrEpisode.EpisodeData((CrunchyEpisodeList)episodeL); var sList = await CrEpisode.EpisodeData((CrunchyEpisodeList)episodeL);
var selected = CrEpisode.EpisodeMeta(sList.Data, dubLang); var selected = CrEpisode.EpisodeMeta(sList.Data, dubLang);
var metas = selected.Values.ToList(); var metas = selected.Values.ToList();
foreach (var crunchyEpMeta in metas){ foreach (var crunchyEpMeta in metas){
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (historyEpisode != null){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
}
}
Queue.Add(crunchyEpMeta); Queue.Add(crunchyEpMeta);
} }
@ -310,6 +312,14 @@ public class Crunchyroll{
foreach (var crunchyEpMeta in selected.Values.ToList()){ foreach (var crunchyEpMeta in selected.Values.ToList()){
if (crunchyEpMeta.Data?.First().Playback != null){ if (crunchyEpMeta.Data?.First().Playback != null){
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
var historyEpisode = CrHistory.GetHistoryEpisode(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (historyEpisode != null){
crunchyEpMeta.EpisodeNumber = historyEpisode.SonarrEpisodeNumber;
crunchyEpMeta.Season = historyEpisode.SonarrSeasonNumber;
}
}
Queue.Add(crunchyEpMeta); Queue.Add(crunchyEpMeta);
} else{ } else{
failed = true; failed = true;
@ -559,10 +569,11 @@ public class Crunchyroll{
}; };
} }
bool dlFailed = false; bool dlFailed = false;
bool dlVideoOnce = false; bool dlVideoOnce = false;
if (data.Data != null) if (data.Data != null){
foreach (CrunchyEpMetaData epMeta in data.Data){ foreach (CrunchyEpMetaData epMeta in data.Data){
Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}"); Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}");
@ -1065,6 +1076,7 @@ public class Crunchyroll{
if (File.Exists($"{tsFile}.video.m4s")){ if (File.Exists($"{tsFile}.video.m4s")){
File.Delete($"{tsFile}.video.m4s"); File.Delete($"{tsFile}.video.m4s");
} }
File.Move($"{tempTsFile}.video.m4s", $"{tsFile}.video.m4s"); File.Move($"{tempTsFile}.video.m4s", $"{tsFile}.video.m4s");
} catch (IOException ex){ } catch (IOException ex){
Console.WriteLine($"An error occurred: {ex.Message}"); Console.WriteLine($"An error occurred: {ex.Message}");
@ -1111,6 +1123,7 @@ public class Crunchyroll{
if (File.Exists($"{tsFile}.audio.m4s")){ if (File.Exists($"{tsFile}.audio.m4s")){
File.Delete($"{tsFile}.audio.m4s"); File.Delete($"{tsFile}.audio.m4s");
} }
File.Move($"{tempTsFile}.audio.m4s", $"{tsFile}.audio.m4s"); File.Move($"{tempTsFile}.audio.m4s", $"{tsFile}.audio.m4s");
} catch (IOException ex){ } catch (IOException ex){
Console.WriteLine($"An error occurred: {ex.Message}"); Console.WriteLine($"An error occurred: {ex.Message}");
@ -1226,7 +1239,7 @@ public class Crunchyroll{
await Task.Delay(options.Waittime); await Task.Delay(options.Waittime);
} }
}
// variables.Add(new Variable("height", quality == 0 ? plQuality.Last().RESOLUTION.Height : plQuality[quality - 1].RESOLUTION.Height, false)); // variables.Add(new Variable("height", quality == 0 ? plQuality.Last().RESOLUTION.Height : plQuality[quality - 1].RESOLUTION.Height, false));
// variables.Add(new Variable("width", quality == 0 ? plQuality.Last().RESOLUTION.Width : plQuality[quality - 1].RESOLUTION.Width, false)); // variables.Add(new Variable("width", quality == 0 ? plQuality.Last().RESOLUTION.Width : plQuality[quality - 1].RESOLUTION.Width, false));
@ -1476,7 +1489,7 @@ public class Crunchyroll{
var stream = hardsub.Value; var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url, Url = stream.Url,
HardsubLocale = Helpers.ConvertStringToLocale(stream.Hlang) HardsubLocale = stream.Hlang
}; };
} }
@ -1552,7 +1565,7 @@ public class Crunchyroll{
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest); playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
if (playbackRequestResponse.IsOk){ if (playbackRequestResponse.IsOk){
// temppbData = Helpers.Deserialize<PlaybackData>(playbackRequestResponse22.ResponseContent, SettingsJsonSerializerSettings) ?? // var temppbData2 = Helpers.Deserialize<PlaybackData>(playbackRequestResponse22.ResponseContent, SettingsJsonSerializerSettings) ??
// new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() }; // new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
temppbData = new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() }; temppbData = new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
@ -1569,7 +1582,7 @@ public class Crunchyroll{
var stream = hardsub.Value; var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url, Url = stream.Url,
HardsubLocale = Helpers.ConvertStringToLocale(stream.Hlang) HardsubLocale = stream.Hlang
}; };
} }
@ -1578,7 +1591,7 @@ public class Crunchyroll{
HardsubLocale = Locale.DefaulT HardsubLocale = Locale.DefaulT
}; };
if (temppbData.Data != null) temppbData.Data[0]["drm_adaptive_dash"] = derivedPlayCrunchyStreams; if (temppbData.Data != null) temppbData.Data[0]["drm_adaptive_switch_dash"] = derivedPlayCrunchyStreams;
temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List<string>{ playStream.Bifs }, MediaId = mediaId }; temppbData.Meta = new PlaybackMeta(){ AudioLocale = playStream.AudioLocale, Versions = playStream.Versions, Bifs = new List<string>{ playStream.Bifs }, MediaId = mediaId };
temppbData.Meta.Subtitles = new Subtitles(); temppbData.Meta.Subtitles = new Subtitles();

View File

@ -80,8 +80,27 @@ public class History(){
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1)); MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
} }
public HistoryEpisode? GetHistoryEpisode(string? seriesId, string? seasonId, string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
public async void UpdateWithEpisode(CrunchyEpisode episodeParam){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId);
if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historyEpisode != null){
return historyEpisode;
}
}
}
return null;
}
public async Task UpdateWithEpisode(CrunchyEpisode episodeParam){
var episode = episodeParam; var episode = episodeParam;
if (episode.Versions != null){ if (episode.Versions != null){
@ -137,23 +156,23 @@ public class History(){
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} else{ } else{
var newHistorySeries = new HistorySeries{ historySeries = new HistorySeries{
SeriesTitle = episode.SeriesTitle, SeriesTitle = episode.SeriesTitle,
SeriesId = episode.SeriesId, SeriesId = episode.SeriesId,
Seasons =[], Seasons =[],
}; };
crunInstance.HistoryList.Add(newHistorySeries); crunInstance.HistoryList.Add(historySeries);
var newSeason = NewHistorySeason(episode); var newSeason = NewHistorySeason(episode);
var series = await crunInstance.CrSeries.SeriesById(seriesId); var series = await crunInstance.CrSeries.SeriesById(seriesId);
if (series?.Data != null){ if (series?.Data != null){
newHistorySeries.SeriesDescription = series.Data.First().Description; historySeries.SeriesDescription = series.Data.First().Description;
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series); historySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
newHistorySeries.SeriesTitle = series.Data.First().Title; historySeries.SeriesTitle = series.Data.First().Title;
} }
newHistorySeries.Seasons.Add(newSeason); historySeries.Seasons.Add(newSeason);
newHistorySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} }
var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList(); var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList();
@ -163,10 +182,11 @@ public class History(){
} }
MatchHistorySeriesWithSonarr(false); MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false,historySeries);
UpdateHistoryFile(); UpdateHistoryFile();
} }
public async void UpdateWithSeasonData(CrunchyEpisodeList seasonData){ public async Task UpdateWithSeasonData(CrunchyEpisodeList seasonData){
if (seasonData.Data != null){ if (seasonData.Data != null){
var firstEpisode = seasonData.Data.First(); var firstEpisode = seasonData.Data.First();
var seriesId = firstEpisode.SeriesId; var seriesId = firstEpisode.SeriesId;
@ -203,7 +223,6 @@ public class History(){
historyEpisode.EpisodeId = crunchyEpisode.Id; historyEpisode.EpisodeId = crunchyEpisode.Id;
historyEpisode.Episode = crunchyEpisode.Episode; historyEpisode.Episode = crunchyEpisode.Episode;
historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + ""; historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + "";
} }
} }
@ -220,12 +239,12 @@ public class History(){
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} else{ } else{
var newHistorySeries = new HistorySeries{ historySeries = new HistorySeries{
SeriesTitle = firstEpisode.SeriesTitle, SeriesTitle = firstEpisode.SeriesTitle,
SeriesId = firstEpisode.SeriesId, SeriesId = firstEpisode.SeriesId,
Seasons =[], Seasons =[],
}; };
crunInstance.HistoryList.Add(newHistorySeries); crunInstance.HistoryList.Add(historySeries);
var newSeason = NewHistorySeason(seasonData, firstEpisode); var newSeason = NewHistorySeason(seasonData, firstEpisode);
@ -233,26 +252,26 @@ public class History(){
var series = await crunInstance.CrSeries.SeriesById(seriesId); var series = await crunInstance.CrSeries.SeriesById(seriesId);
if (series?.Data != null){ if (series?.Data != null){
newHistorySeries.SeriesDescription = series.Data.First().Description; historySeries.SeriesDescription = series.Data.First().Description;
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series); historySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
newHistorySeries.SeriesTitle = series.Data.First().Title; historySeries.SeriesTitle = series.Data.First().Title;
} }
newHistorySeries.Seasons.Add(newSeason); historySeries.Seasons.Add(newSeason);
newHistorySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
}
var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList();
crunInstance.HistoryList.Clear();
foreach (var item in sortedList){
crunInstance.HistoryList.Add(item);
} }
}
var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList(); MatchHistorySeriesWithSonarr(false);
crunInstance.HistoryList.Clear(); await MatchHistoryEpisodesWithSonarr(false,historySeries);
foreach (var item in sortedList){ UpdateHistoryFile();
crunInstance.HistoryList.Add(item);
} }
MatchHistorySeriesWithSonarr(false);
UpdateHistoryFile();
} }
private string GetSeriesThumbnail(CrSeriesBase series){ private string GetSeriesThumbnail(CrSeriesBase series){
@ -322,6 +341,11 @@ public class History(){
} }
public void MatchHistorySeriesWithSonarr(bool updateAll){ public void MatchHistorySeriesWithSonarr(bool updateAll){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
}
foreach (var historySeries in crunInstance.HistoryList){ foreach (var historySeries in crunInstance.HistoryList){
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle); var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle);
@ -334,7 +358,11 @@ public class History(){
} }
} }
public async void MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){ public async Task MatchHistoryEpisodesWithSonarr(bool updateAll, HistorySeries historySeries){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
}
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
@ -372,7 +400,8 @@ public class History(){
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
episodes.Remove(episode); episodes.Remove(episode);
} else{ } else{
var episode1 = episodes.Find(ele => !string.IsNullOrEmpty(historyEpisode.EpisodeDescription) && !string.IsNullOrEmpty(ele.Overview) && Helpers.CalculateCosineSimilarity(ele.Overview, historyEpisode.EpisodeDescription) > 0.8); var episode1 = episodes.Find(ele =>
!string.IsNullOrEmpty(historyEpisode.EpisodeDescription) && !string.IsNullOrEmpty(ele.Overview) && Helpers.CalculateCosineSimilarity(ele.Overview, historyEpisode.EpisodeDescription) > 0.8);
if (episode1 != null){ if (episode1 != null){
historyEpisode.SonarrEpisodeId = episode1.Id + ""; historyEpisode.SonarrEpisodeId = episode1.Id + "";
@ -395,11 +424,7 @@ public class History(){
} }
} }
} }
} }
} }
} }
@ -442,7 +467,8 @@ public class History(){
return 1.0 - (double)distance / Math.Max(source.Length, target.Length); return 1.0 - (double)distance / Math.Max(source.Length, target.Length);
} }
private int LevenshteinDistance(string source, string target){
public int LevenshteinDistance(string source, string target){
if (string.IsNullOrEmpty(source)){ if (string.IsNullOrEmpty(source)){
return string.IsNullOrEmpty(target) ? 0 : target.Length; return string.IsNullOrEmpty(target) ? 0 : target.Length;
} }
@ -454,35 +480,32 @@ public class History(){
int n = source.Length; int n = source.Length;
int m = target.Length; int m = target.Length;
// Create two work arrays of integer distances. // Use a single array for distances.
int[] previousDistances = new int[m + 1]; int[] distances = new int[m + 1];
int[] currentDistances = new int[m + 1];
// Initialize the previous distance array. // Initialize the distance array.
for (int j = 0; j <= m; j++){ for (int j = 0; j <= m; j++){
previousDistances[j] = j; distances[j] = j;
} }
for (int i = 1; i <= n; i++){ for (int i = 1; i <= n; i++){
// Initialize the current distance array. int previousDiagonal = distances[0];
currentDistances[0] = i; distances[0] = i;
for (int j = 1; j <= m; j++){ for (int j = 1; j <= m; j++){
int previousDistance = distances[j];
int cost = (target[j - 1] == source[i - 1]) ? 0 : 1; int cost = (target[j - 1] == source[i - 1]) ? 0 : 1;
currentDistances[j] = Math.Min( distances[j] = Math.Min(
Math.Min(currentDistances[j - 1] + 1, previousDistances[j] + 1), Math.Min(distances[j - 1] + 1, distances[j] + 1),
previousDistances[j - 1] + cost); previousDiagonal + cost);
}
// Swap the arrays for the next iteration. previousDiagonal = previousDistance;
var temp = previousDistances; }
previousDistances = currentDistances;
currentDistances = temp;
} }
// The final distance is in the previous distance array. // The final distance is in the last cell.
return previousDistances[m]; return distances[m];
} }
} }
@ -530,6 +553,9 @@ public class HistorySeries : INotifyPropertyChanged{
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
[JsonIgnore]
public bool FetchingData{ get; set; }
public async Task LoadImage(){ public async Task LoadImage(){
try{ try{
using (var client = new HttpClient()){ using (var client = new HttpClient()){
@ -574,6 +600,11 @@ public class HistorySeries : INotifyPropertyChanged{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
} }
public void SetFetchingData(){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
}
public async Task AddNewMissingToDownloads(){ public async Task AddNewMissingToDownloads(){
bool foundWatched = false; bool foundWatched = false;
@ -600,7 +631,12 @@ public class HistorySeries : INotifyPropertyChanged{
} }
public async Task FetchData(string? seasonId){ public async Task FetchData(string? seasonId){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId); await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId);
FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false,this);
} }
} }

View File

@ -149,9 +149,6 @@ public class CfgManager{
public static void WriteJsonToFile(string pathToFile, object obj){ public static void WriteJsonToFile(string pathToFile, object obj){
try{ try{
// Serialize the object to a JSON string.
var jsonString = JsonConvert.SerializeObject(obj, Formatting.Indented);
// Check if the directory exists; if not, create it. // Check if the directory exists; if not, create it.
string directoryPath = Path.GetDirectoryName(pathToFile); string directoryPath = Path.GetDirectoryName(pathToFile);
if (!Directory.Exists(directoryPath)){ if (!Directory.Exists(directoryPath)){
@ -159,8 +156,13 @@ public class CfgManager{
} }
lock (fileLock){ lock (fileLock){
// Write the JSON string to file. Creates the file if it does not exist. // Write the JSON string to file using a streaming approach.
File.WriteAllText(pathToFile, jsonString); using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write))
using (var streamWriter = new StreamWriter(fileStream))
using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){
var serializer = new JsonSerializer();
serializer.Serialize(jsonWriter, obj);
}
} }
} catch (Exception ex){ } catch (Exception ex){
Console.WriteLine($"An error occurred: {ex.Message}"); Console.WriteLine($"An error occurred: {ex.Message}");

View File

@ -421,7 +421,7 @@ public class HlsDownloader{
try{ try{
response = await HttpClientReq.Instance.GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseContentRead); response = await HttpClientReq.Instance.GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseContentRead);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync(); return await ReadContentAsByteArrayAsync(response.Content);
} catch (HttpRequestException ex){ } catch (HttpRequestException ex){
// Log retry attempts // Log retry attempts
string partType = isKey ? "Key" : "Part"; string partType = isKey ? "Key" : "Part";
@ -437,6 +437,14 @@ public class HlsDownloader{
return null; // Should not reach here return null; // Should not reach here
} }
private async Task<byte[]> ReadContentAsByteArrayAsync(HttpContent content){
using (var memoryStream = new MemoryStream())
using (var contentStream = await content.ReadAsStreamAsync()){
await contentStream.CopyToAsync(memoryStream, 81920);
return memoryStream.ToArray();
}
}
private HttpRequestMessage CloneHttpRequestMessage(HttpRequestMessage originalRequest){ private HttpRequestMessage CloneHttpRequestMessage(HttpRequestMessage originalRequest){
var clone = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri){ var clone = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri){
Content = originalRequest.Content?.Clone(), Content = originalRequest.Content?.Clone(),

View File

@ -38,7 +38,11 @@ public class Helpers{
} }
} }
return Locale.DefaulT; // Return default if not found if (string.IsNullOrEmpty(value)){
return Locale.DefaulT;
}
return Locale.Unknown; // Return default if not found
} }
public static string GenerateSessionId(){ public static string GenerateSessionId(){
@ -88,22 +92,44 @@ public class Helpers{
return CosineSimilarity(vector1, vector2); return CosineSimilarity(vector1, vector2);
} }
private static Dictionary<string, double> ComputeWordFrequency(string text){ private static readonly char[] Delimiters ={ ' ', ',', '.', ';', ':', '-', '_', '\'' };
var wordFrequency = new Dictionary<string, double>();
var words = text.Split(new[]{ ' ', ',', '.', ';', ':', '-', '_', '\'' }, StringSplitOptions.RemoveEmptyEntries); public static Dictionary<string, double> ComputeWordFrequency(string text){
var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
var words = SplitText(text);
foreach (var word in words){ foreach (var word in words){
var lowerWord = word.ToLower(); if (wordFrequency.TryGetValue(word, out double count)){
if (!wordFrequency.ContainsKey(lowerWord)){ wordFrequency[word] = count + 1;
wordFrequency[lowerWord] = 0; } else{
wordFrequency[word] = 1;
} }
wordFrequency[lowerWord]++;
} }
return wordFrequency; return wordFrequency;
} }
private static List<string> SplitText(string text){
var words = new List<string>();
int start = 0;
for (int i = 0; i < text.Length; i++){
if (Array.IndexOf(Delimiters, text[i]) >= 0){
if (i > start){
words.Add(text.Substring(start, i - start));
}
start = i + 1;
}
}
if (start < text.Length){
words.Add(text.Substring(start));
}
return words;
}
private static double CosineSimilarity(Dictionary<string, double> vector1, Dictionary<string, double> vector2){ private static double CosineSimilarity(Dictionary<string, double> vector1, Dictionary<string, double> vector2){
var intersection = vector1.Keys.Intersect(vector2.Keys); var intersection = vector1.Keys.Intersect(vector2.Keys);

View File

@ -16,7 +16,7 @@ using Newtonsoft.Json;
namespace CRD.Utils.Sonarr; namespace CRD.Utils.Sonarr;
public class SonarrClient{ public class SonarrClient{
private string apiUrl; private string? apiUrl;
private HttpClient httpClient; private HttpClient httpClient;
@ -50,11 +50,39 @@ public class SonarrClient{
public void SetApiUrl(){ public void SetApiUrl(){
if (Crunchyroll.Instance.CrunOptions.SonarrProperties != null) properties = Crunchyroll.Instance.CrunOptions.SonarrProperties; if (Crunchyroll.Instance.CrunOptions.SonarrProperties != null) properties = Crunchyroll.Instance.CrunOptions.SonarrProperties;
if (properties != null){ if (properties != null ){
apiUrl = $"http{(properties.UseSsl ? "s" : "")}://{properties.Host}:{properties.Port}{(properties.UrlBase ?? "")}/api"; apiUrl = $"http{(properties.UseSsl ? "s" : "")}://{(!string.IsNullOrEmpty(properties.Host) ? properties.Host : "localhost")}:{properties.Port}{(properties.UrlBase ?? "")}/api";
} }
} }
public async Task CheckSonarrSettings(){
SetApiUrl();
if (Crunchyroll.Instance.CrunOptions.SonarrProperties != null){
Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled = false;
} else{
Crunchyroll.Instance.CrunOptions.SonarrProperties = new SonarrProperties(){SonarrEnabled = false};
return;
}
Debug.WriteLine($"[DEBUG] [SonarrClient.CheckSonarrSettings] Endpoint URL: '{apiUrl}'");
var request = CreateRequestMessage($"{apiUrl}", HttpMethod.Get);
HttpResponseMessage response;
try{
response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
if (Crunchyroll.Instance.CrunOptions.SonarrProperties != null) Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled = true;
} catch (Exception ex){
Debug.WriteLine($"[ERROR] [SonarrClient.GetJson] Endpoint URL: '{apiUrl}', {ex}");
if (Crunchyroll.Instance.CrunOptions.SonarrProperties != null) Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled = false;
}
}
public async Task<List<SonarrSeries>> GetSeries(){ public async Task<List<SonarrSeries>> GetSeries(){
var json = await GetJson($"/v3/series{(true ? $"?includeSeasonImages={true}" : "")}"); var json = await GetJson($"/v3/series{(true ? $"?includeSeasonImages={true}" : "")}");
@ -151,4 +179,7 @@ public class SonarrProperties(){
public bool UseSsl{ get; set; } public bool UseSsl{ get; set; }
public string? UrlBase{ get; set; } public string? UrlBase{ get; set; }
public bool UseSonarrNumbering{ get; set; }
public bool SonarrEnabled{ get; set; }
} }

View File

@ -7,8 +7,6 @@ using System.Threading.Tasks;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Views;
using ReactiveUI;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs;
@ -36,6 +34,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
public string? EpisodeNumber{ get; set; } public string? EpisodeNumber{ get; set; }
public bool IsPremiumOnly{ get; set; } public bool IsPremiumOnly{ get; set; }
public bool IsPremiere{ get; set; }
public string? SeasonName{ get; set; } public string? SeasonName{ get; set; }

View File

@ -25,7 +25,7 @@ public class Caption{
} }
public class HardSub{ public class HardSub{
public string? Hlang{ get; set; } public Locale? Hlang{ get; set; }
public string? Url{ get; set; } public string? Url{ get; set; }
public string? Quality{ get; set; } public string? Quality{ get; set; }
} }

View File

@ -11,24 +11,55 @@ public struct CrunchyEpisodeList{
} }
public struct CrunchyEpisode{ public struct CrunchyEpisode{
[JsonProperty("next_episode_id")] public string NextEpisodeId{ get; set; } [JsonProperty("next_episode_id")]
[JsonProperty("series_id")] public string SeriesId{ get; set; } public string NextEpisodeId{ get; set; }
[JsonProperty("season_number")] public int SeasonNumber{ get; set; }
[JsonProperty("next_episode_title")] public string NextEpisodeTitle{ get; set; } [JsonProperty("series_id")]
[JsonProperty("availability_notes")] public string AvailabilityNotes{ get; set; } public string SeriesId{ get; set; }
[JsonProperty("duration_ms")] public int DurationMs{ get; set; }
[JsonProperty("series_slug_title")] public string SeriesSlugTitle{ get; set; } [JsonProperty("season_number")]
[JsonProperty("series_title")] public string SeriesTitle{ get; set; } public int SeasonNumber{ get; set; }
[JsonProperty("is_dubbed")] public bool IsDubbed{ get; set; }
public List<EpisodeVersion>? Versions{ get; set; } // Assume Version is defined elsewhere. [JsonProperty("next_episode_title")]
public string NextEpisodeTitle{ get; set; }
[JsonProperty("availability_notes")]
public string AvailabilityNotes{ get; set; }
[JsonProperty("duration_ms")]
public int DurationMs{ get; set; }
[JsonProperty("series_slug_title")]
public string SeriesSlugTitle{ get; set; }
[JsonProperty("series_title")]
public string SeriesTitle{ get; set; }
[JsonProperty("is_dubbed")]
public bool IsDubbed{ get; set; }
public List<EpisodeVersion>? Versions{ get; set; }
public string Identifier{ get; set; } public string Identifier{ get; set; }
[JsonProperty("sequence_number")] public float SequenceNumber{ get; set; }
[JsonProperty("eligible_region")] public string EligibleRegion{ get; set; } [JsonProperty("sequence_number")]
[JsonProperty("availability_starts")] public DateTime? AvailabilityStarts{ get; set; } public float SequenceNumber{ get; set; }
public Images? Images{ get; set; } // Assume Images is a struct or class you've defined elsewhere.
[JsonProperty("season_id")] public string SeasonId{ get; set; } [JsonProperty("eligible_region")]
[JsonProperty("seo_title")] public string SeoTitle{ get; set; } public string EligibleRegion{ get; set; }
[JsonProperty("is_premium_only")] public bool IsPremiumOnly{ get; set; }
[JsonProperty("availability_starts")]
public DateTime? AvailabilityStarts{ get; set; }
public Images? Images{ get; set; }
[JsonProperty("season_id")]
public string SeasonId{ get; set; }
[JsonProperty("seo_title")]
public string SeoTitle{ get; set; }
[JsonProperty("is_premium_only")]
public bool IsPremiumOnly{ get; set; }
[JsonProperty("extended_maturity_rating")] [JsonProperty("extended_maturity_rating")]
public Dictionary<string, object> ExtendedMaturityRating{ get; set; } public Dictionary<string, object> ExtendedMaturityRating{ get; set; }
@ -41,104 +72,119 @@ public struct CrunchyEpisode{
[JsonProperty("premium_available_date")] [JsonProperty("premium_available_date")]
public DateTime? PremiumAvailableDate{ get; set; } public DateTime? PremiumAvailableDate{ get; set; }
[JsonProperty("season_title")] public string SeasonTitle{ get; set; } [JsonProperty("season_title")]
[JsonProperty("seo_description")] public string SeoDescription{ get; set; } public string SeasonTitle{ get; set; }
[JsonProperty("seo_description")]
public string SeoDescription{ get; set; }
[JsonProperty("audio_locale")]
public string AudioLocale{ get; set; }
[JsonProperty("audio_locale")] public string AudioLocale{ get; set; }
public string Id{ get; set; } public string Id{ get; set; }
[JsonProperty("media_type")] public MediaType? MediaType{ get; set; } // MediaType should be an enum you define based on possible values.
[JsonProperty("availability_ends")] public DateTime? AvailabilityEnds{ get; set; } [JsonProperty("media_type")]
[JsonProperty("free_available_date")] public DateTime? FreeAvailableDate{ get; set; } public MediaType? MediaType{ get; set; }
[JsonProperty("availability_ends")]
public DateTime? AvailabilityEnds{ get; set; }
[JsonProperty("free_available_date")]
public DateTime? FreeAvailableDate{ get; set; }
public string Playback{ get; set; } public string Playback{ get; set; }
[JsonProperty("channel_id")] public ChannelId? ChannelId{ get; set; } // ChannelID should be an enum or struct.
[JsonProperty("channel_id")]
public ChannelId? ChannelId{ get; set; }
public string? Episode{ get; set; } public string? Episode{ get; set; }
[JsonProperty("is_mature")] public bool IsMature{ get; set; }
[JsonProperty("listing_id")] public string ListingId{ get; set; } [JsonProperty("is_mature")]
[JsonProperty("episode_air_date")] public DateTime? EpisodeAirDate{ get; set; } public bool IsMature{ get; set; }
[JsonProperty("listing_id")]
public string ListingId{ get; set; }
[JsonProperty("episode_air_date")]
public DateTime? EpisodeAirDate{ get; set; }
public string Slug{ get; set; } public string Slug{ get; set; }
[JsonProperty("available_date")] public DateTime? AvailableDate{ get; set; }
[JsonProperty("subtitle_locales")] public List<string> SubtitleLocales{ get; set; } [JsonProperty("available_date")]
[JsonProperty("slug_title")] public string SlugTitle{ get; set; } public DateTime? AvailableDate{ get; set; }
[JsonProperty("available_offline")] public bool AvailableOffline{ get; set; }
[JsonProperty("subtitle_locales")]
public List<string> SubtitleLocales{ get; set; }
[JsonProperty("slug_title")]
public string SlugTitle{ get; set; }
[JsonProperty("available_offline")]
public bool AvailableOffline{ get; set; }
public string Description{ get; set; } public string Description{ get; set; }
[JsonProperty("is_subbed")] public bool IsSubbed{ get; set; }
[JsonProperty("premium_date")] public DateTime? PremiumDate{ get; set; } [JsonProperty("is_subbed")]
[JsonProperty("upload_date")] public DateTime? UploadDate{ get; set; } public bool IsSubbed{ get; set; }
[JsonProperty("season_slug_title")] public string SeasonSlugTitle{ get; set; }
[JsonProperty("premium_date")]
public DateTime? PremiumDate{ get; set; }
[JsonProperty("upload_date")]
public DateTime? UploadDate{ get; set; }
[JsonProperty("season_slug_title")]
public string SeasonSlugTitle{ get; set; }
[JsonProperty("closed_captions_available")] [JsonProperty("closed_captions_available")]
public bool ClosedCaptionsAvailable{ get; set; } public bool ClosedCaptionsAvailable{ get; set; }
[JsonProperty("episode_number")] public int? EpisodeNumber{ get; set; } [JsonProperty("episode_number")]
[JsonProperty("season_tags")] public List<object> SeasonTags{ get; set; } // More specific type could be used if known. public int? EpisodeNumber{ get; set; }
[JsonProperty("maturity_ratings")] public List<string> MaturityRatings{ get; set; } // MaturityRating should be defined based on possible values.
[JsonProperty("streams_link")] public string? StreamsLink{ get; set; } [JsonProperty("season_tags")]
[JsonProperty("mature_blocked")] public bool? MatureBlocked{ get; set; } public List<object> SeasonTags{ get; set; }
[JsonProperty("is_clip")] public bool IsClip{ get; set; }
[JsonProperty("hd_flag")] public bool HdFlag{ get; set; } [JsonProperty("maturity_ratings")]
[JsonProperty("hide_season_title")] public bool? HideSeasonTitle{ get; set; } public List<string> MaturityRatings{ get; set; }
[JsonProperty("hide_season_number")] public bool? HideSeasonNumber{ get; set; }
[JsonProperty("streams_link")]
public string? StreamsLink{ get; set; }
[JsonProperty("mature_blocked")]
public bool? MatureBlocked{ get; set; }
[JsonProperty("is_clip")]
public bool IsClip{ get; set; }
[JsonProperty("hd_flag")]
public bool HdFlag{ get; set; }
[JsonProperty("hide_season_title")]
public bool? HideSeasonTitle{ get; set; }
[JsonProperty("hide_season_number")]
public bool? HideSeasonNumber{ get; set; }
public bool? IsSelected{ get; set; } public bool? IsSelected{ get; set; }
[JsonProperty("seq_id")] public string SeqId{ get; set; }
[JsonProperty("__links__")] public Links? Links{ get; set; } [JsonProperty("seq_id")]
public string SeqId{ get; set; }
[JsonProperty("__links__")]
public Links? Links{ get; set; }
} }
// public struct CrunchyEpisode{
//
// public string channel_id{ get; set; }
// public bool is_mature{ get; set; }
// public string upload_date{ get; set; }
// public string free_available_date{ get; set; }
// public List<string> content_descriptors{ get; set; }
// public Dictionary<object, object> images{ get; set; } // Consider specifying actual key and value types if known
// public int season_sequence_number{ get; set; }
// public string audio_locale{ get; set; }
// public string title{ get; set; }
// public Dictionary<object, object>
// extended_maturity_rating{ get; set; } // Consider specifying actual key and value types if known
// public bool available_offline{ get; set; }
// public string identifier{ get; set; }
// public string listing_id{ get; set; }
// public List<string> season_tags{ get; set; }
// public string next_episode_id{ get; set; }
// public string next_episode_title{ get; set; }
// public bool is_subbed{ get; set; }
// public string slug{ get; set; }
// public List<Version> versions{ get; set; }
// public int season_number{ get; set; }
// public string availability_ends{ get; set; }
// public string eligible_region{ get; set; }
// public bool is_clip{ get; set; }
// public string description{ get; set; }
// public string seo_description{ get; set; }
// public bool is_premium_only{ get; set; }
// public string streams_link{ get; set; }
// public int episode_number{ get; set; }
// public bool closed_captions_available{ get; set; }
//
// public bool is_dubbed{ get; set; }
// public string seo_title{ get; set; }
// public long duration_ms{ get; set; }
// public string id{ get; set; }
// public string series_id{ get; set; }
// public string series_slug_title{ get; set; }
// public string episode_air_date{ get; set; }
// public bool hd_flag{ get; set; }
// public bool mature_blocked{ get; set; }
//
// public string availability_notes{ get; set; }
//
// public List<string> maturity_ratings{ get; set; }
// public string episode{ get; set; }
// public int sequence_number{ get; set; }
// public List<string> subtitle_locales{ get; set; }
//
// }
public struct Images{ public struct Images{
[JsonProperty("poster_tall")] public List<List<Image>>? PosterTall{ get; set; } [JsonProperty("poster_tall")]
[JsonProperty("poster_wide")] public List<List<Image>>? PosterWide{ get; set; } public List<List<Image>>? PosterTall{ get; set; }
[JsonProperty("promo_image")] public List<List<Image>>? PromoImage{ get; set; }
[JsonProperty("poster_wide")]
public List<List<Image>>? PosterWide{ get; set; }
[JsonProperty("promo_image")]
public List<List<Image>>? PromoImage{ get; set; }
public List<List<Image>> Thumbnail{ get; set; } public List<List<Image>> Thumbnail{ get; set; }
} }
@ -150,12 +196,22 @@ public struct Image{
} }
public struct EpisodeVersion{ public struct EpisodeVersion{
[JsonProperty("audio_locale")] public string AudioLocale{ get; set; } [JsonProperty("audio_locale")]
public string AudioLocale{ get; set; }
public string Guid{ get; set; } public string Guid{ get; set; }
[JsonProperty("is_premium_only")] public bool IsPremiumOnly{ get; set; }
[JsonProperty("media_guid")] public string? MediaGuid{ get; set; } [JsonProperty("is_premium_only")]
public bool IsPremiumOnly{ get; set; }
[JsonProperty("media_guid")]
public string? MediaGuid{ get; set; }
public bool Original{ get; set; } public bool Original{ get; set; }
[JsonProperty("season_guid")] public string SeasonGuid{ get; set; }
[JsonProperty("season_guid")]
public string SeasonGuid{ get; set; }
public string Variant{ get; set; } public string Variant{ get; set; }
} }
@ -194,7 +250,6 @@ public class CrunchyEpMeta{
} }
public class DownloadProgress{ public class DownloadProgress{
public bool IsDownloading = false; public bool IsDownloading = false;
public bool Done = false; public bool Done = false;
public bool Error = false; public bool Error = false;

View File

@ -0,0 +1,21 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
namespace CRD.Utils.UI;
public class UiValueConverterCalendarBackground : IValueConverter{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture){
if (value is bool boolValue){
return boolValue ? new SolidColorBrush(Color.Parse("#10f5d800")) : new SolidColorBrush(Color.Parse("#10FFFFFF"));
}
return new SolidColorBrush(Color.Parse("#10FFFFFF"));
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture){
throw new NotImplementedException();
}
}

View File

@ -14,7 +14,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
public ObservableCollection<HistorySeries> Items{ get; } public ObservableCollection<HistorySeries> Items{ get; }
[ObservableProperty] [ObservableProperty]
private bool? _showLoading = false; private static bool _fetchingData;
[ObservableProperty] [ObservableProperty]
public HistorySeries _selectedSeries; public HistorySeries _selectedSeries;
@ -35,38 +35,52 @@ public partial class HistoryPageViewModel : ViewModelBase{
} }
partial void OnSelectedSeriesChanged(HistorySeries value){ partial void OnSelectedSeriesChanged(HistorySeries value){
Crunchyroll.Instance.SelectedSeries = value; Crunchyroll.Instance.SelectedSeries = value;
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, false)); NavToSeries();
_selectedSeries = null; _selectedSeries = null;
} }
[RelayCommand] [RelayCommand]
public void RemoveSeries(string? seriesId){ public void RemoveSeries(string? seriesId){
HistorySeries? objectToRemove = Crunchyroll.Instance.HistoryList.ToList().Find(se => se.SeriesId == seriesId) ?? null; HistorySeries? objectToRemove = Crunchyroll.Instance.HistoryList.ToList().Find(se => se.SeriesId == seriesId) ?? null;
if (objectToRemove != null) { if (objectToRemove != null){
Crunchyroll.Instance.HistoryList.Remove(objectToRemove); Crunchyroll.Instance.HistoryList.Remove(objectToRemove);
Items.Remove(objectToRemove); Items.Remove(objectToRemove);
} }
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList); CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
} }
[RelayCommand] [RelayCommand]
public void NavToSeries(){ public void NavToSeries(){
if (FetchingData){
return;
}
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, false)); MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, false));
} }
[RelayCommand] [RelayCommand]
public async void RefreshAll(){ public async void RefreshAll(){
FetchingData = true;
RaisePropertyChanged(nameof(FetchingData));
for (int i = 0; i < Items.Count; i++){ for (int i = 0; i < Items.Count; i++){
ShowLoading = true; Items[i].SetFetchingData();
}
for (int i = 0; i < Items.Count; i++){
FetchingData = true;
RaisePropertyChanged(nameof(FetchingData));
await Items[i].FetchData(""); await Items[i].FetchData("");
Items[i].UpdateNewEpisodes(); Items[i].UpdateNewEpisodes();
} }
ShowLoading = false; FetchingData = false;
RaisePropertyChanged(nameof(FetchingData));
} }
[RelayCommand] [RelayCommand]
@ -74,7 +88,5 @@ public partial class HistoryPageViewModel : ViewModelBase{
for (int i = 0; i < Items.Count; i++){ for (int i = 0; i < Items.Count; i++){
await Items[i].AddNewMissingToDownloads(); await Items[i].AddNewMissingToDownloads();
} }
ShowLoading = false;
} }
} }

View File

@ -10,6 +10,7 @@ using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
@ -100,6 +101,9 @@ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _sonarrUseSsl = false; private bool _sonarrUseSsl = false;
[ObservableProperty]
private bool _sonarrUseSonarrNumbering = 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),
@ -230,6 +234,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
if (props != null){ if (props != null){
SonarrUseSsl = props.UseSsl; SonarrUseSsl = props.UseSsl;
SonarrUseSonarrNumbering = props.UseSonarrNumbering;
SonarrHost = props.Host + ""; SonarrHost = props.Host + "";
SonarrPort = props.Port + ""; SonarrPort = props.Port + "";
SonarrApiKey = props.ApiKey + ""; SonarrApiKey = props.ApiKey + "";
@ -316,13 +321,21 @@ public partial class SettingsPageViewModel : ViewModelBase{
var props = new SonarrProperties(); var props = new SonarrProperties();
props.UseSsl = SonarrUseSsl; props.UseSsl = SonarrUseSsl;
props.UseSonarrNumbering = SonarrUseSonarrNumbering;
props.Host = SonarrHost; props.Host = SonarrHost;
props.Port = Convert.ToInt32(SonarrPort);
if (int.TryParse(SonarrPort, out var portNumber)){
props.Port = portNumber;
} else{
props.Port = 8989;
}
props.ApiKey = SonarrApiKey; props.ApiKey = SonarrApiKey;
Crunchyroll.Instance.CrunOptions.SonarrProperties = props; Crunchyroll.Instance.CrunOptions.SonarrProperties = props;
Crunchyroll.Instance.RefreshSonarr();
//TODO - Mux Options //TODO - Mux Options
@ -469,4 +482,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
partial void OnSonarrUseSslChanged(bool value){ partial void OnSonarrUseSslChanged(bool value){
UpdateSettings(); UpdateSettings();
} }
partial void OnSonarrUseSonarrNumberingChanged(bool value){
UpdateSettings();
}
} }

View File

@ -4,11 +4,16 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vm="clr-namespace:CRD.ViewModels" xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:ui="clr-namespace:CRD.Utils.UI"
x:DataType="vm:CalendarPageViewModel" x:DataType="vm:CalendarPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.CalendarPageView"> x:Class="CRD.Views.CalendarPageView">
<UserControl.Resources>
<ui:UiValueConverterCalendarBackground x:Key="UiValueConverterCalendarBackground" />
</UserControl.Resources>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- For the button --> <RowDefinition Height="Auto" /> <!-- For the button -->
@ -51,12 +56,12 @@
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"> <Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
<!-- Spinner Style ProgressBar --> <!-- Spinner Style ProgressBar -->
<ProgressBar IsIndeterminate="True" <!-- <ProgressBar IsIndeterminate="True" -->
Value="50" <!-- MaxWidth="100" -->
Maximum="100" <!-- IsVisible="{Binding ShowLoading}"> -->
MaxWidth="100" <!-- </ProgressBar> -->
IsVisible="{Binding ShowLoading}">
</ProgressBar> <controls:ProgressRing IsVisible="{Binding ShowLoading}" Width="100" Height="100"></controls:ProgressRing>
</Grid> </Grid>
<ItemsControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" IsVisible="{Binding !ShowLoading}" <ItemsControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" IsVisible="{Binding !ShowLoading}"
@ -84,60 +89,59 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ListBox for episodes --> <ScrollViewer Grid.Row="1" >
<ListBox Grid.Row="1" ItemsSource="{Binding CalendarEpisodes}"> <!-- Adjust MaxHeight as needed --> <ItemsControl ItemsSource="{Binding CalendarEpisodes}">
<ListBox.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border Padding="10" Margin="5"> <Border Padding="10" Margin="5 5 15 5" CornerRadius="5" Background="{Binding IsPremiere, Converter={StaticResource UiValueConverterCalendarBackground}}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock HorizontalAlignment="Center" <TextBlock HorizontalAlignment="Center"
Text="{Binding DateTime, StringFormat='hh:mm tt'}" Text="{Binding DateTime, StringFormat='hh:mm tt'}"
Margin="0,0,0,0" /> Margin="0,0,0,0" />
<Grid HorizontalAlignment="Center"> <Grid HorizontalAlignment="Center">
<Image HorizontalAlignment="Center" Source="{Binding ImageBitmap}" /> <Image HorizontalAlignment="Center" Source="{Binding ImageBitmap}" />
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Left"> <StackPanel VerticalAlignment="Top" HorizontalAlignment="Left">
<TextBlock VerticalAlignment="Center" TextAlignment="Center" <TextBlock VerticalAlignment="Center" TextAlignment="Center"
Margin="0 0 5 0" Width="30" Height="30" Margin="0 0 5 0" Width="30" Height="30"
Background="Black" Opacity="0.8" Background="Black" Opacity="0.8"
Text="{Binding EpisodeNumber}" Text="{Binding EpisodeNumber}"
Padding="0,5,0,0" /> Padding="0,5,0,0" />
</StackPanel> </StackPanel>
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right" <StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right"
IsVisible="{Binding IsPremiumOnly}" Margin="0,0,5,5"> IsVisible="{Binding IsPremiumOnly}" Margin="0,0,5,5">
<Canvas Width="28" Height="28"> <Canvas Width="28" Height="28">
<Ellipse Fill="#40FFFFFF" Width="28" Height="28" /> <Ellipse Fill="#40FFFFFF" Width="28" Height="28" />
<Viewbox Width="24" Height="24" Stretch="Uniform" <Viewbox Width="24" Height="24" Stretch="Uniform"
Canvas.Left="2" Canvas.Top="2"> Canvas.Left="2" Canvas.Top="2">
<Canvas Width="50" Height="50"> <!-- Ensure inner canvas is large enough to hold the path data --> <Canvas Width="50" Height="50"> <!-- Ensure inner canvas is large enough to hold the path data -->
<Path Fill="#f78c25" <Path Fill="#f78c25"
Stroke="#f78c25" Stroke="#f78c25"
StrokeThickness="1" StrokeThickness="1"
Data="M35.7,36.2H12.3c-0.7,0-1.4-0.5-1.6-1.2L6.1,18.6c-0.2-0.6,0-1.3,0.5-1.7c0.5-0.4,1.2-0.5,1.8-0.2l8.1,4.1 l6.2-8.3c0.3-0.4,0.8-0.7,1.3-0.7h0c0.5,0,1,0.2,1.3,0.7l6.2,8.3l8.2-4.1c0.6-0.3,1.3-0.2,1.8,0.2c0.5,0.4,0.7,1.1,0.5,1.7 L37.3,35C37.1,35.7,36.4,36.2,35.7,36.2z" /> Data="M35.7,36.2H12.3c-0.7,0-1.4-0.5-1.6-1.2L6.1,18.6c-0.2-0.6,0-1.3,0.5-1.7c0.5-0.4,1.2-0.5,1.8-0.2l8.1,4.1 l6.2-8.3c0.3-0.4,0.8-0.7,1.3-0.7h0c0.5,0,1,0.2,1.3,0.7l6.2,8.3l8.2-4.1c0.6-0.3,1.3-0.2,1.8,0.2c0.5,0.4,0.7,1.1,0.5,1.7 L37.3,35C37.1,35.7,36.4,36.2,35.7,36.2z" />
</Canvas> </Canvas>
</Viewbox> </Viewbox>
</Canvas> </Canvas>
</StackPanel> </StackPanel>
</Grid> </Grid>
<TextBlock HorizontalAlignment="Center" Text="{Binding SeasonName}"
TextWrapping="NoWrap"
Margin="0,0,0,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SeasonName}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<Button HorizontalAlignment="Center" Content="Download"
IsEnabled="{Binding HasPassed}"
Command="{Binding AddEpisodeToQue}"
CommandParameter="{Binding EpisodeUrl}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<TextBlock HorizontalAlignment="Center" Text="{Binding SeasonName}"
TextWrapping="NoWrap"
Margin="0,0,0,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SeasonName}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<Button HorizontalAlignment="Center" Content="Download"
IsEnabled="{Binding HasPassed}"
Command="{Binding AddEpisodeToQue}"
CommandParameter="{Binding EpisodeUrl}" />
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>

View File

@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CRD.ViewModels" xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:ui="clr-namespace:CRD.Utils.UI" xmlns:ui="clr-namespace:CRD.Utils.UI"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
@ -22,22 +21,12 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Margin="20 0 0 0 "> <StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Margin="20 0 0 0 ">
<Button Command="{Binding RefreshAll}" Margin="10">Refresh All</Button> <Button Command="{Binding RefreshAll}" Margin="10" IsEnabled="{Binding !FetchingData}">Refresh All</Button>
<Button Command="{Binding AddMissingToQueue}" Margin="10">Add To Queue</Button> <Button Command="{Binding AddMissingToQueue}" Margin="10" IsEnabled="{Binding !FetchingData}">Add To Queue</Button>
<ToggleButton IsChecked="{Binding EditMode}" Margin="10">Edit</ToggleButton> <ToggleButton IsChecked="{Binding EditMode}" Margin="10" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton>
</StackPanel> </StackPanel>
<Grid Grid.Row="1" Grid.Column="0"> <ListBox Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedSeries}" Margin="5">
<!-- Spinner Style ProgressBar -->
<ProgressBar IsIndeterminate="True"
Value="50"
Maximum="100"
MaxWidth="100"
IsVisible="{Binding ShowLoading}">
</ProgressBar>
</Grid>
<ListBox Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Items}" IsVisible="{Binding !ShowLoading}" SelectedItem="{Binding SelectedSeries}" Margin="5">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
@ -59,8 +48,8 @@
Padding="0,5,0,0"/> Padding="0,5,0,0"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<TextBlock HorizontalAlignment="Center" Text="{Binding SeriesTitle}" TextWrapping="NoWrap" <TextBlock HorizontalAlignment="Center" Text="{Binding SeriesTitle}" TextWrapping="NoWrap" MaxWidth="240"
Margin="4,0,0,0"> Margin="4,0,4,0">
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
@ -70,6 +59,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}" Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}"
CommandParameter="{Binding SeriesId}" CommandParameter="{Binding SeriesId}"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).FetchingData}"
> >
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="Remove Series" FontSize="15" /> <TextBlock Text="Remove Series" FontSize="15" />
@ -80,6 +70,14 @@
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="250" Width="250"
MaxHeight="400" Height="400" Background="#90000000" IsVisible="{Binding FetchingData}">
<!-- <ProgressBar IsIndeterminate="{Binding FetchingData}" -->
<!-- MaxWidth="100"> -->
<!-- </ProgressBar> -->
<controls:ProgressRing Width="100" Height="100"></controls:ProgressRing>
</StackPanel>
</Grid> </Grid>

View File

@ -1,11 +1,11 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views; namespace CRD.Views;
public partial class HistoryPageView : UserControl{ public partial class HistoryPageView : UserControl{
public HistoryPageView(){ public HistoryPageView(){
InitializeComponent(); InitializeComponent();
} }
} }

View File

@ -6,7 +6,8 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:SettingsPageViewModel" x:DataType="vm:SettingsPageViewModel"
x:Class="CRD.Views.SettingsPageView"> x:Class="CRD.Views.SettingsPageView"
Unloaded="OnUnloaded">
<Design.DataContext> <Design.DataContext>
<vm:SettingsPageViewModel /> <vm:SettingsPageViewModel />
@ -240,6 +241,7 @@
<controls:SettingsExpander Header="Sonarr Settings" <controls:SettingsExpander Header="Sonarr Settings"
IconSource="Globe" IconSource="Globe"
Description="Adjust sonarr settings" Description="Adjust sonarr settings"
IsEnabled="{Binding History}"
IsExpanded="False"> IsExpanded="False">
<controls:SettingsExpanderItem Content="Host"> <controls:SettingsExpanderItem Content="Host">
@ -269,6 +271,12 @@
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use Sonarr Numbering" Description="Potentially wrong if it couldn't be matched">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SonarrUseSonarrNumbering}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsExpander Header="App Theme" <controls:SettingsExpander Header="App Theme"

View File

@ -1,6 +1,9 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using CRD.Downloader;
using CRD.Utils.Sonarr;
using CRD.ViewModels; using CRD.ViewModels;
namespace CRD.Views; namespace CRD.Views;
@ -9,4 +12,10 @@ public partial class SettingsPageView : UserControl{
public SettingsPageView(){ public SettingsPageView(){
InitializeComponent(); InitializeComponent();
} }
private void OnUnloaded(object? sender, RoutedEventArgs e){
if (DataContext is SettingsPageViewModel viewModel){
Crunchyroll.Instance.RefreshSonarr();
}
}
} }