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"
x:Class="CRD.App"
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>
<crd:ViewLocator/>
<crd:ViewLocator />
</Application.DataTemplates>
<Application.Styles>
<sty:FluentAvaloniaTheme/>
<sty:FluentAvaloniaTheme PreferSystemTheme="True" PreferUserAccentColor="True"/>
<StyleInclude Source="avares://CRD/Styling/ControlsGalleryStyles.axaml" />
<StyleInclude Source="avares://CRD/Assets/Icons.axaml"></StyleInclude>
</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;
Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
@ -56,7 +56,7 @@ public class CrEpisode(){
foreach (var episode in dlEpisodes.Data){
if (crunInstance.CrunOptions.History){
crunInstance.CrHistory.UpdateWithEpisode(episode);
await crunInstance.CrHistory.UpdateWithEpisode(episode);
}
// Prepare the episode array

View File

@ -164,7 +164,6 @@ public class Crunchyroll{
RefreshSonarr();
}
calendarLanguage = new(){
{ "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
{ "es", "https://www.crunchyroll.com/es/simulcastcalendar" },
@ -181,8 +180,8 @@ public class Crunchyroll{
}
public async void RefreshSonarr(){
if (CrunOptions.SonarrProperties != null && !string.IsNullOrEmpty(CrunOptions.SonarrProperties.ApiKey)){
SonarrClient.Instance.SetApiUrl();
await SonarrClient.Instance.CheckSonarrSettings();
if (CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
SonarrSeries = await SonarrClient.Instance.GetSeries();
CrHistory.MatchHistorySeriesWithSonarr(true);
}
@ -221,8 +220,6 @@ public class Crunchyroll{
var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim();
// Console.WriteLine($"Day: {dayName}, Date: {date}");
CalendarDay calDay = new CalendarDay();
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 thumbnailUrl = episode.SelectSingleNode(".//img[contains(@class, 'thumbnail')]")?.GetAttributeValue("src", "No image");
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 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();
calEpisode.DateTime = episodeTime;
@ -259,6 +252,7 @@ public class Crunchyroll{
calEpisode.EpisodeUrl = episodeLink;
calEpisode.ThumbnailUrl = thumbnailUrl;
calEpisode.IsPremiumOnly = isPremiumOnly;
calEpisode.IsPremiere = isPremiere;
calEpisode.SeasonName = seasonName;
calEpisode.EpisodeNumber = episodeNumber;
@ -267,7 +261,6 @@ public class Crunchyroll{
}
week.CalendarDays.Add(calDay);
// Console.WriteLine();
}
} else{
Console.WriteLine("No days found in the HTML document.");
@ -291,10 +284,19 @@ public class Crunchyroll{
return;
}
var sList = CrEpisode.EpisodeData((CrunchyEpisodeList)episodeL);
var sList = await CrEpisode.EpisodeData((CrunchyEpisodeList)episodeL);
var selected = CrEpisode.EpisodeMeta(sList.Data, dubLang);
var metas = selected.Values.ToList();
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);
}
@ -310,6 +312,14 @@ public class Crunchyroll{
foreach (var crunchyEpMeta in selected.Values.ToList()){
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);
} else{
failed = true;
@ -362,7 +372,7 @@ public class Crunchyroll{
};
Queue.Refresh();
await MuxStreams(res.Data,
new CrunchyMuxOptions{
FfmpegOptions = options.FfmpegOptions,
@ -434,7 +444,7 @@ public class Crunchyroll{
subt.Fonts = downloadedMedia.Fonts;
subsList.Add(subt);
}
if (File.Exists($"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}") && !string.IsNullOrEmpty(filename)){
string newFilePath = filename;
int counter = 1;
@ -446,7 +456,7 @@ public class Crunchyroll{
filename = newFilePath;
}
var merger = new Merger(new MergerOptions{
OnlyVid = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List<MergerInput>(),
@ -559,10 +569,11 @@ public class Crunchyroll{
};
}
bool dlFailed = false;
bool dlVideoOnce = false;
if (data.Data != null)
if (data.Data != null){
foreach (CrunchyEpMetaData epMeta in data.Data){
Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}");
@ -1065,6 +1076,7 @@ public class Crunchyroll{
if (File.Exists($"{tsFile}.video.m4s")){
File.Delete($"{tsFile}.video.m4s");
}
File.Move($"{tempTsFile}.video.m4s", $"{tsFile}.video.m4s");
} catch (IOException ex){
Console.WriteLine($"An error occurred: {ex.Message}");
@ -1111,6 +1123,7 @@ public class Crunchyroll{
if (File.Exists($"{tsFile}.audio.m4s")){
File.Delete($"{tsFile}.audio.m4s");
}
File.Move($"{tempTsFile}.audio.m4s", $"{tsFile}.audio.m4s");
} catch (IOException ex){
Console.WriteLine($"An error occurred: {ex.Message}");
@ -1226,7 +1239,7 @@ public class Crunchyroll{
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("width", quality == 0 ? plQuality.Last().RESOLUTION.Width : plQuality[quality - 1].RESOLUTION.Width, false));
@ -1476,7 +1489,7 @@ public class Crunchyroll{
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url,
HardsubLocale = Helpers.ConvertStringToLocale(stream.Hlang)
HardsubLocale = stream.Hlang
};
}
@ -1552,7 +1565,7 @@ public class Crunchyroll{
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
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>>>() };
temppbData = new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
@ -1569,7 +1582,7 @@ public class Crunchyroll{
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url = stream.Url,
HardsubLocale = Helpers.ConvertStringToLocale(stream.Hlang)
HardsubLocale = stream.Hlang
};
}
@ -1578,7 +1591,7 @@ public class Crunchyroll{
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.Subtitles = new Subtitles();

View File

@ -22,7 +22,7 @@ public class History(){
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");
@ -79,11 +79,30 @@ public class History(){
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);
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 void UpdateWithEpisode(CrunchyEpisode episodeParam){
public async Task UpdateWithEpisode(CrunchyEpisode episodeParam){
var episode = episodeParam;
if (episode.Versions != null){
var version = episode.Versions.Find(a => a.Original);
if (version.AudioLocale != episode.AudioLocale){
@ -137,23 +156,23 @@ public class History(){
historySeries.UpdateNewEpisodes();
} else{
var newHistorySeries = new HistorySeries{
historySeries = new HistorySeries{
SeriesTitle = episode.SeriesTitle,
SeriesId = episode.SeriesId,
Seasons =[],
};
crunInstance.HistoryList.Add(newHistorySeries);
crunInstance.HistoryList.Add(historySeries);
var newSeason = NewHistorySeason(episode);
var series = await crunInstance.CrSeries.SeriesById(seriesId);
if (series?.Data != null){
newHistorySeries.SeriesDescription = series.Data.First().Description;
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
newHistorySeries.SeriesTitle = series.Data.First().Title;
historySeries.SeriesDescription = series.Data.First().Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
historySeries.SeriesTitle = series.Data.First().Title;
}
newHistorySeries.Seasons.Add(newSeason);
newHistorySeries.UpdateNewEpisodes();
historySeries.Seasons.Add(newSeason);
historySeries.UpdateNewEpisodes();
}
var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList();
@ -163,10 +182,11 @@ public class History(){
}
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false,historySeries);
UpdateHistoryFile();
}
public async void UpdateWithSeasonData(CrunchyEpisodeList seasonData){
public async Task UpdateWithSeasonData(CrunchyEpisodeList seasonData){
if (seasonData.Data != null){
var firstEpisode = seasonData.Data.First();
var seriesId = firstEpisode.SeriesId;
@ -203,7 +223,6 @@ public class History(){
historyEpisode.EpisodeId = crunchyEpisode.Id;
historyEpisode.Episode = crunchyEpisode.Episode;
historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + "";
}
}
@ -220,12 +239,12 @@ public class History(){
historySeries.UpdateNewEpisodes();
} else{
var newHistorySeries = new HistorySeries{
historySeries = new HistorySeries{
SeriesTitle = firstEpisode.SeriesTitle,
SeriesId = firstEpisode.SeriesId,
Seasons =[],
};
crunInstance.HistoryList.Add(newHistorySeries);
crunInstance.HistoryList.Add(historySeries);
var newSeason = NewHistorySeason(seasonData, firstEpisode);
@ -233,26 +252,26 @@ public class History(){
var series = await crunInstance.CrSeries.SeriesById(seriesId);
if (series?.Data != null){
newHistorySeries.SeriesDescription = series.Data.First().Description;
newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
newHistorySeries.SeriesTitle = series.Data.First().Title;
historySeries.SeriesDescription = series.Data.First().Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
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();
crunInstance.HistoryList.Clear();
foreach (var item in sortedList){
crunInstance.HistoryList.Add(item);
MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false,historySeries);
UpdateHistoryFile();
}
MatchHistorySeriesWithSonarr(false);
UpdateHistoryFile();
}
private string GetSeriesThumbnail(CrSeriesBase series){
@ -322,6 +341,11 @@ public class History(){
}
public void MatchHistorySeriesWithSonarr(bool updateAll){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;
}
foreach (var historySeries in crunInstance.HistoryList){
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
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)){
var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
@ -345,7 +373,7 @@ public class History(){
}
List<HistoryEpisode> failedEpisodes =[];
foreach (var historyEpisode in allHistoryEpisodes){
if (updateAll || string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
var episode = FindClosestMatchEpisodes(episodes, historyEpisode.EpisodeTitle);
@ -372,8 +400,9 @@ public class History(){
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);
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 + "";
@ -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);
}
private int LevenshteinDistance(string source, string target){
public int LevenshteinDistance(string source, string target){
if (string.IsNullOrEmpty(source)){
return string.IsNullOrEmpty(target) ? 0 : target.Length;
}
@ -454,35 +480,32 @@ public class History(){
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];
// Use a single array for distances.
int[] distances = new int[m + 1];
// Initialize the previous distance array.
// Initialize the distance array.
for (int j = 0; j <= m; j++){
previousDistances[j] = j;
distances[j] = j;
}
for (int i = 1; i <= n; i++){
// Initialize the current distance array.
currentDistances[0] = i;
int previousDiagonal = distances[0];
distances[0] = i;
for (int j = 1; j <= m; j++){
int previousDistance = distances[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);
}
distances[j] = Math.Min(
Math.Min(distances[j - 1] + 1, distances[j] + 1),
previousDiagonal + cost);
// Swap the arrays for the next iteration.
var temp = previousDistances;
previousDistances = currentDistances;
currentDistances = temp;
previousDiagonal = previousDistance;
}
}
// The final distance is in the previous distance array.
return previousDistances[m];
// The final distance is in the last cell.
return distances[m];
}
}
@ -530,6 +553,9 @@ public class HistorySeries : INotifyPropertyChanged{
public event PropertyChangedEventHandler? PropertyChanged;
[JsonIgnore]
public bool FetchingData{ get; set; }
public async Task LoadImage(){
try{
using (var client = new HttpClient()){
@ -574,6 +600,11 @@ public class HistorySeries : INotifyPropertyChanged{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
}
public void SetFetchingData(){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
}
public async Task AddNewMissingToDownloads(){
bool foundWatched = false;
@ -600,7 +631,12 @@ public class HistorySeries : INotifyPropertyChanged{
}
public async Task FetchData(string? seasonId){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId);
FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false,this);
}
}
@ -654,10 +690,10 @@ 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; }

View File

@ -146,12 +146,9 @@ public class CfgManager{
}
private static object fileLock = new object();
public static void WriteJsonToFile(string pathToFile, object obj){
try{
// Serialize the object to a JSON string.
var jsonString = JsonConvert.SerializeObject(obj, Formatting.Indented);
// Check if the directory exists; if not, create it.
string directoryPath = Path.GetDirectoryName(pathToFile);
if (!Directory.Exists(directoryPath)){
@ -159,14 +156,19 @@ public class CfgManager{
}
lock (fileLock){
// Write the JSON string to file. Creates the file if it does not exist.
File.WriteAllText(pathToFile, jsonString);
// Write the JSON string to file using a streaming approach.
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){
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
public static bool CheckIfFileExists(string filePath){
string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;

View File

@ -421,7 +421,7 @@ public class HlsDownloader{
try{
response = await HttpClientReq.Instance.GetHttpClient().SendAsync(request, HttpCompletionOption.ResponseContentRead);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
return await ReadContentAsByteArrayAsync(response.Content);
} catch (HttpRequestException ex){
// Log retry attempts
string partType = isKey ? "Key" : "Part";
@ -437,6 +437,14 @@ public class HlsDownloader{
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){
var clone = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri){
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(){
@ -88,22 +92,44 @@ public class Helpers{
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);
private static readonly char[] Delimiters ={ ' ', ',', '.', ';', ':', '-', '_', '\'' };
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){
var lowerWord = word.ToLower();
if (!wordFrequency.ContainsKey(lowerWord)){
wordFrequency[lowerWord] = 0;
if (wordFrequency.TryGetValue(word, out double count)){
wordFrequency[word] = count + 1;
} else{
wordFrequency[word] = 1;
}
wordFrequency[lowerWord]++;
}
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){
var intersection = vector1.Keys.Intersect(vector2.Keys);

View File

@ -16,7 +16,7 @@ using Newtonsoft.Json;
namespace CRD.Utils.Sonarr;
public class SonarrClient{
private string apiUrl;
private string? apiUrl;
private HttpClient httpClient;
@ -50,11 +50,39 @@ public class SonarrClient{
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";
if (properties != null ){
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(){
var json = await GetJson($"/v3/series{(true ? $"?includeSeasonImages={true}" : "")}");
@ -151,4 +179,7 @@ public class SonarrProperties(){
public bool UseSsl{ 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 CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Views;
using ReactiveUI;
namespace CRD.Utils.Structs;
@ -36,6 +34,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
public string? EpisodeNumber{ get; set; }
public bool IsPremiumOnly{ get; set; }
public bool IsPremiere{ get; set; }
public string? SeasonName{ get; set; }
@ -51,7 +50,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
Crunchyroll.Instance.AddEpisodeToQue(id, locale, Crunchyroll.Instance.CrunOptions.DubLang);
}
}
public async Task LoadImage(){
try{
using (var client = new HttpClient()){

View File

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

View File

@ -11,24 +11,55 @@ public struct CrunchyEpisodeList{
}
public struct CrunchyEpisode{
[JsonProperty("next_episode_id")] public string NextEpisodeId{ get; set; }
[JsonProperty("series_id")] public string SeriesId{ get; set; }
[JsonProperty("season_number")] public int SeasonNumber{ get; set; }
[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; } // Assume Version is defined elsewhere.
[JsonProperty("next_episode_id")]
public string NextEpisodeId{ get; set; }
[JsonProperty("series_id")]
public string SeriesId{ get; set; }
[JsonProperty("season_number")]
public int SeasonNumber{ get; set; }
[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; }
[JsonProperty("sequence_number")] public float SequenceNumber{ get; set; }
[JsonProperty("eligible_region")] public string EligibleRegion{ get; set; }
[JsonProperty("availability_starts")] public DateTime? AvailabilityStarts{ 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("seo_title")] public string SeoTitle{ get; set; }
[JsonProperty("is_premium_only")] public bool IsPremiumOnly{ get; set; }
[JsonProperty("sequence_number")]
public float SequenceNumber{ get; set; }
[JsonProperty("eligible_region")]
public string EligibleRegion{ 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")]
public Dictionary<string, object> ExtendedMaturityRating{ get; set; }
@ -41,104 +72,119 @@ public struct CrunchyEpisode{
[JsonProperty("premium_available_date")]
public DateTime? PremiumAvailableDate{ get; set; }
[JsonProperty("season_title")] public string SeasonTitle{ get; set; }
[JsonProperty("seo_description")] public string SeoDescription{ get; set; }
[JsonProperty("season_title")]
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; }
[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("free_available_date")] public DateTime? FreeAvailableDate{ get; set; }
[JsonProperty("media_type")]
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; }
[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; }
[JsonProperty("is_mature")] public bool IsMature{ get; set; }
[JsonProperty("listing_id")] public string ListingId{ get; set; }
[JsonProperty("episode_air_date")] public DateTime? EpisodeAirDate{ get; set; }
[JsonProperty("is_mature")]
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; }
[JsonProperty("available_date")] public DateTime? AvailableDate{ 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; }
[JsonProperty("available_date")]
public DateTime? AvailableDate{ 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; }
[JsonProperty("is_subbed")] public bool IsSubbed{ 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("is_subbed")]
public bool IsSubbed{ 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")]
public bool ClosedCaptionsAvailable{ get; set; }
[JsonProperty("episode_number")] public int? EpisodeNumber{ get; set; }
[JsonProperty("season_tags")] public List<object> SeasonTags{ get; set; } // More specific type could be used if known.
[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("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; }
[JsonProperty("episode_number")]
public int? EpisodeNumber{ get; set; }
[JsonProperty("season_tags")]
public List<object> SeasonTags{ get; set; }
[JsonProperty("maturity_ratings")]
public List<string> MaturityRatings{ 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; }
[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{
[JsonProperty("poster_tall")] public List<List<Image>>? PosterTall{ get; set; }
[JsonProperty("poster_wide")] public List<List<Image>>? PosterWide{ get; set; }
[JsonProperty("promo_image")] public List<List<Image>>? PromoImage{ get; set; }
[JsonProperty("poster_tall")]
public List<List<Image>>? PosterTall{ 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; }
}
@ -150,12 +196,22 @@ public struct Image{
}
public struct EpisodeVersion{
[JsonProperty("audio_locale")] public string AudioLocale{ get; set; }
[JsonProperty("audio_locale")]
public string AudioLocale{ 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; }
[JsonProperty("season_guid")] public string SeasonGuid{ get; set; }
[JsonProperty("season_guid")]
public string SeasonGuid{ get; set; }
public string Variant{ get; set; }
}
@ -187,14 +243,13 @@ public class CrunchyEpMeta{
public string? Image{ get; set; }
public bool Paused{ get; set; }
public DownloadProgress? DownloadProgress{ get; set; }
public List<string>? SelectedDubs{ get; set; }
public List<string>? AvailableSubs{ get; set; }
}
public class DownloadProgress{
public bool IsDownloading = false;
public bool Done = 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; }
[ObservableProperty]
private bool? _showLoading = false;
private static bool _fetchingData;
[ObservableProperty]
public HistorySeries _selectedSeries;
@ -35,38 +35,52 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
partial void OnSelectedSeriesChanged(HistorySeries value){
Crunchyroll.Instance.SelectedSeries = value;
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, false));
NavToSeries();
_selectedSeries = null;
}
[RelayCommand]
public void RemoveSeries(string? seriesId){
HistorySeries? objectToRemove = Crunchyroll.Instance.HistoryList.ToList().Find(se => se.SeriesId == seriesId) ?? null;
if (objectToRemove != null) {
if (objectToRemove != null){
Crunchyroll.Instance.HistoryList.Remove(objectToRemove);
Items.Remove(objectToRemove);
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
[RelayCommand]
public void NavToSeries(){
if (FetchingData){
return;
}
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, false));
}
[RelayCommand]
public async void RefreshAll(){
FetchingData = true;
RaisePropertyChanged(nameof(FetchingData));
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("");
Items[i].UpdateNewEpisodes();
}
ShowLoading = false;
FetchingData = false;
RaisePropertyChanged(nameof(FetchingData));
}
[RelayCommand]
@ -74,7 +88,5 @@ public partial class HistoryPageViewModel : ViewModelBase{
for (int i = 0; i < Items.Count; i++){
await Items[i].AddNewMissingToDownloads();
}
ShowLoading = false;
}
}

View File

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

View File

@ -4,11 +4,16 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:ui="clr-namespace:CRD.Utils.UI"
x:DataType="vm:CalendarPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.CalendarPageView">
<UserControl.Resources>
<ui:UiValueConverterCalendarBackground x:Key="UiValueConverterCalendarBackground" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- For the button -->
@ -51,12 +56,12 @@
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
<!-- Spinner Style ProgressBar -->
<ProgressBar IsIndeterminate="True"
Value="50"
Maximum="100"
MaxWidth="100"
IsVisible="{Binding ShowLoading}">
</ProgressBar>
<!-- <ProgressBar IsIndeterminate="True" -->
<!-- MaxWidth="100" -->
<!-- IsVisible="{Binding ShowLoading}"> -->
<!-- </ProgressBar> -->
<controls:ProgressRing IsVisible="{Binding ShowLoading}" Width="100" Height="100"></controls:ProgressRing>
</Grid>
<ItemsControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" IsVisible="{Binding !ShowLoading}"
@ -84,60 +89,59 @@
</StackPanel>
</Border>
<!-- ListBox for episodes -->
<ListBox Grid.Row="1" ItemsSource="{Binding CalendarEpisodes}"> <!-- Adjust MaxHeight as needed -->
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="10" Margin="5">
<StackPanel Orientation="Vertical">
<TextBlock HorizontalAlignment="Center"
Text="{Binding DateTime, StringFormat='hh:mm tt'}"
Margin="0,0,0,0" />
<Grid HorizontalAlignment="Center">
<Image HorizontalAlignment="Center" Source="{Binding ImageBitmap}" />
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Left">
<TextBlock VerticalAlignment="Center" TextAlignment="Center"
Margin="0 0 5 0" Width="30" Height="30"
Background="Black" Opacity="0.8"
Text="{Binding EpisodeNumber}"
Padding="0,5,0,0" />
</StackPanel>
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right"
IsVisible="{Binding IsPremiumOnly}" Margin="0,0,5,5">
<Canvas Width="28" Height="28">
<Ellipse Fill="#40FFFFFF" Width="28" Height="28" />
<Viewbox Width="24" Height="24" Stretch="Uniform"
Canvas.Left="2" Canvas.Top="2">
<Canvas Width="50" Height="50"> <!-- Ensure inner canvas is large enough to hold the path data -->
<Path Fill="#f78c25"
Stroke="#f78c25"
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" />
</Canvas>
</Viewbox>
</Canvas>
</StackPanel>
</Grid>
<ScrollViewer Grid.Row="1" >
<ItemsControl ItemsSource="{Binding CalendarEpisodes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Padding="10" Margin="5 5 15 5" CornerRadius="5" Background="{Binding IsPremiere, Converter={StaticResource UiValueConverterCalendarBackground}}">
<StackPanel Orientation="Vertical">
<TextBlock HorizontalAlignment="Center"
Text="{Binding DateTime, StringFormat='hh:mm tt'}"
Margin="0,0,0,0" />
<Grid HorizontalAlignment="Center">
<Image HorizontalAlignment="Center" Source="{Binding ImageBitmap}" />
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Left">
<TextBlock VerticalAlignment="Center" TextAlignment="Center"
Margin="0 0 5 0" Width="30" Height="30"
Background="Black" Opacity="0.8"
Text="{Binding EpisodeNumber}"
Padding="0,5,0,0" />
</StackPanel>
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right"
IsVisible="{Binding IsPremiumOnly}" Margin="0,0,5,5">
<Canvas Width="28" Height="28">
<Ellipse Fill="#40FFFFFF" Width="28" Height="28" />
<Viewbox Width="24" Height="24" Stretch="Uniform"
Canvas.Left="2" Canvas.Top="2">
<Canvas Width="50" Height="50"> <!-- Ensure inner canvas is large enough to hold the path data -->
<Path Fill="#f78c25"
Stroke="#f78c25"
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" />
</Canvas>
</Viewbox>
</Canvas>
</StackPanel>
</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>
</ListBox.ItemTemplate>
</ListBox>
<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>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>

View File

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

View File

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

View File

@ -6,7 +6,8 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:SettingsPageViewModel"
x:Class="CRD.Views.SettingsPageView">
x:Class="CRD.Views.SettingsPageView"
Unloaded="OnUnloaded">
<Design.DataContext>
<vm:SettingsPageViewModel />
@ -240,6 +241,7 @@
<controls:SettingsExpander Header="Sonarr Settings"
IconSource="Globe"
Description="Adjust sonarr settings"
IsEnabled="{Binding History}"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Host">
@ -269,6 +271,12 @@
</controls:SettingsExpanderItem.Footer>
</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 Header="App Theme"

View File

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