Add - Added **Skip Unmonitored Sonarr Episodes** option to settings

Add - Added **Include Crunchyroll Artists (Music)** in history to settings for expanded tracking
Add - Added **filters to history tab** to hide series or artists for a cleaner view
Add - Added a **toggle to include featured music** in series search results
Chg - Made small changes to **sync timing** to more accurately detect delays
Chg - Migrated settings to json file
Fix - Fixed a **sync timing issue** with longer comparison videos to ensure proper synchronization
Fix - Fixed issues with artist urls
This commit is contained in:
Elwador 2025-01-29 19:57:20 +01:00
parent 6886217dd7
commit 93244a749f
57 changed files with 2311 additions and 1010 deletions

View file

@ -11,7 +11,6 @@ using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using DynamicData;
using HtmlAgilityPack;
using Newtonsoft.Json;
@ -90,56 +89,60 @@ public class CalendarManager{
foreach (var day in dayNodes){
// Extract the date and day name
var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date");
DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind);
if (date != null){
DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind);
if (week.FirstDayOfWeek == DateTime.MinValue){
week.FirstDayOfWeek = dayDateTime;
week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd");
}
var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim();
CalendarDay calDay = new CalendarDay();
calDay.CalendarEpisodes = new List<CalendarEpisode>();
calDay.DayName = dayName;
calDay.DateTime = dayDateTime;
// Iterate through each episode listed under this day
var episodes = day.SelectNodes(".//article[contains(@class, 'release')]");
if (episodes != null){
foreach (var episode in episodes){
var episodeTimeStr = episode.SelectSingleNode(".//time[contains(@class, 'available-time')]")?.GetAttributeValue("datetime", null);
DateTime episodeTime = DateTime.Parse(episodeTimeStr, null, DateTimeStyles.RoundtripKind);
var hasPassed = DateTime.Now > episodeTime;
var episodeName = episode.SelectSingleNode(".//h1[contains(@class, 'episode-name')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim();
var seasonLink = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-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 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", "?");
CalendarEpisode calEpisode = new CalendarEpisode();
calEpisode.DateTime = episodeTime;
calEpisode.HasPassed = hasPassed;
calEpisode.EpisodeName = episodeName;
calEpisode.SeriesUrl = seasonLink;
calEpisode.EpisodeUrl = episodeLink;
calEpisode.ThumbnailUrl = thumbnailUrl;
calEpisode.IsPremiumOnly = isPremiumOnly;
calEpisode.IsPremiere = isPremiere;
calEpisode.SeasonName = seasonName;
calEpisode.EpisodeNumber = episodeNumber;
calDay.CalendarEpisodes.Add(calEpisode);
if (week.FirstDayOfWeek == DateTime.MinValue){
week.FirstDayOfWeek = dayDateTime;
week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd");
}
}
week.CalendarDays.Add(calDay);
var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim();
CalendarDay calDay = new CalendarDay();
calDay.CalendarEpisodes = new List<CalendarEpisode>();
calDay.DayName = dayName;
calDay.DateTime = dayDateTime;
// Iterate through each episode listed under this day
var episodes = day.SelectNodes(".//article[contains(@class, 'release')]");
if (episodes != null){
foreach (var episode in episodes){
var episodeTimeStr = episode.SelectSingleNode(".//time[contains(@class, 'available-time')]")?.GetAttributeValue("datetime", null);
if (episodeTimeStr != null){
DateTime episodeTime = DateTime.Parse(episodeTimeStr, null, DateTimeStyles.RoundtripKind);
var hasPassed = DateTime.Now > episodeTime;
var episodeName = episode.SelectSingleNode(".//h1[contains(@class, 'episode-name')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim();
var seasonLink = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-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 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", "?");
CalendarEpisode calEpisode = new CalendarEpisode();
calEpisode.DateTime = episodeTime;
calEpisode.HasPassed = hasPassed;
calEpisode.EpisodeName = episodeName;
calEpisode.SeriesUrl = seasonLink;
calEpisode.EpisodeUrl = episodeLink;
calEpisode.ThumbnailUrl = thumbnailUrl;
calEpisode.IsPremiumOnly = isPremiumOnly;
calEpisode.IsPremiere = isPremiere;
calEpisode.SeasonName = seasonName;
calEpisode.EpisodeNumber = episodeNumber;
calDay.CalendarEpisodes.Add(calEpisode);
}
}
}
week.CalendarDays.Add(calDay);
}
}
} else{
Console.Error.WriteLine("No days found in the HTML document.");
@ -260,7 +263,7 @@ public class CalendarManager{
calEpisode.EpisodeName = crBrowseEpisode.Title;
calEpisode.SeriesUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId;
calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/watch/{crBrowseEpisode.Id}/";
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault().Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault()?.Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
@ -268,10 +271,10 @@ public class CalendarManager{
calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId;
var existingEpisode = calendarDay.CalendarEpisodes
?.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName);
.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName);
if (existingEpisode != null){
if (!int.TryParse(existingEpisode.EpisodeNumber, out var num)){
if (!int.TryParse(existingEpisode.EpisodeNumber, out _)){
existingEpisode.EpisodeNumber = "...";
} else{
var existingNumbers = existingEpisode.EpisodeNumber
@ -300,7 +303,7 @@ public class CalendarManager{
existingEpisode.CalendarEpisodes.Add(calEpisode);
} else{
calendarDay.CalendarEpisodes?.Add(calEpisode);
calendarDay.CalendarEpisodes.Add(calEpisode);
}
}
}
@ -429,8 +432,6 @@ public class CalendarManager{
calEp.EpisodeNumber = anilistEle.Episode.ToString();
calEp.AnilistEpisode = true;
var crunchyrollID = "";
if (anilistEle.Media?.ExternalLinks != null){
var url = anilistEle.Media.ExternalLinks.First(external =>
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase)).Url;
@ -438,10 +439,11 @@ public class CalendarManager{
string pattern = @"series\/([^\/]+)";
Match match = Regex.Match(url, pattern);
string crunchyrollId;
if (match.Success){
crunchyrollID = match.Groups[1].Value;
crunchyrollId = match.Groups[1].Value;
AdjustReleaseTimeToHistory(calEp, crunchyrollID);
AdjustReleaseTimeToHistory(calEp, crunchyrollId);
} else{
Uri uri = new Uri(url);
@ -462,9 +464,9 @@ public class CalendarManager{
Match match2 = Regex.Match(finalUrl ?? string.Empty, pattern);
if (match2.Success){
crunchyrollID = match2.Groups[1].Value;
crunchyrollId = match2.Groups[1].Value;
AdjustReleaseTimeToHistory(calEp, crunchyrollID);
AdjustReleaseTimeToHistory(calEp, crunchyrollId);
}
}
}

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using CRD.Views;
@ -15,6 +16,11 @@ namespace CRD.Downloader.Crunchyroll;
public class CrAuth{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
private readonly string authorization = ApiUrls.authBasicMob;
private readonly string userAgent = ApiUrls.MobileUserAgent;
private const string DeviceType = "OnePlus CPH2449";
private const string DeviceName = "CPH2449";
public async Task AuthAnonymous(){
string uuid = Guid.NewGuid().ToString();
@ -22,17 +28,18 @@ public class CrAuth{
{ "grant_type", "client_id" },
{ "scope", "offline_access" },
{ "device_id", uuid },
{ "device_type", "Chrome on Windows" }
{ "device_name", DeviceName },
{ "device_type", DeviceType },
};
var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
{ "Authorization", authorization },
{ "User-Agent", userAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent
};
@ -63,7 +70,7 @@ public class CrAuth{
crunInstance.Token.device_id = deviceId;
crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in);
CfgManager.WriteTokenToYamlFile(crunInstance.Token, CfgManager.PathCrToken);
CfgManager.WriteJsonToFile(CfgManager.PathCrToken, crunInstance.Token);
}
}
@ -76,17 +83,18 @@ public class CrAuth{
{ "grant_type", "password" },
{ "scope", "offline_access" },
{ "device_id", uuid },
{ "device_type", "Chrome on Windows" }
{ "device_name", DeviceName },
{ "device_type", DeviceType },
};
var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
{ "Authorization", authorization },
{ "User-Agent", userAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent
};
@ -120,7 +128,7 @@ public class CrAuth{
return;
}
var request = HttpClientReq.CreateRequestMessage(ApiUrls.BetaProfile, HttpMethod.Get, true, true, null);
var request = HttpClientReq.CreateRequestMessage(ApiUrls.Profile, HttpMethod.Get, true, true, null);
var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -183,18 +191,19 @@ public class CrAuth{
{ "refresh_token", crunInstance.Token.refresh_token },
{ "scope", "offline_access" },
{ "device_id", uuid },
{ "device_type", "Chrome on Windows" },
{ "grant_type", "refresh_token" }
{ "grant_type", "refresh_token" },
{ "device_name", DeviceName },
{ "device_type", DeviceType },
};
var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
{ "Authorization", authorization },
{ "User-Agent", userAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent
};
@ -237,21 +246,22 @@ public class CrAuth{
string uuid = Guid.NewGuid().ToString();
var formData = new Dictionary<string, string>{
{ "refresh_token", crunInstance.Token.refresh_token },
{ "refresh_token", crunInstance.Token?.refresh_token ?? "" },
{ "grant_type", "refresh_token" },
{ "scope", "offline_access" },
{ "device_id", uuid },
{ "device_type", "Chrome on Windows" }
{ "device_name", DeviceName },
{ "device_type", DeviceType },
};
var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
{ "Authorization", authorization },
{ "User-Agent", userAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent
};
@ -259,7 +269,7 @@ public class CrAuth{
request.Headers.Add(header.Key, header.Value);
}
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
HttpClientReq.Instance.SetETPCookie(crunInstance.Token?.refresh_token ?? string.Empty);
var response = await HttpClientReq.Instance.SendHttpRequest(request);

View file

@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs;
namespace CRD.Downloader.Crunchyroll;
@ -35,9 +36,9 @@ public class CrEpisode(){
return null;
}
CrunchyEpisodeList epsidoe = Helpers.Deserialize<CrunchyEpisodeList>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
CrunchyEpisodeList epsidoe = Helpers.Deserialize<CrunchyEpisodeList>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyEpisodeList();
if (epsidoe.Total < 1){
if (epsidoe is{ Total: < 1 }){
return null;
}
@ -59,7 +60,7 @@ public class CrEpisode(){
CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData();
if (crunInstance.CrunOptions.History && updateHistory){
await crunInstance.History.UpdateWithSeasonData(new List<CrunchyEpisode>(){dlEpisode},false);
await crunInstance.History.UpdateWithEpisodeList([dlEpisode]);
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == dlEpisode.SeriesId);
if (historySeries != null){
CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(false);
@ -81,7 +82,13 @@ public class CrEpisode(){
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale));
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem{
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
});
}
}
} else{
@ -91,7 +98,13 @@ public class CrEpisode(){
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale));
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? new LanguageItem{
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
});
}
}
@ -100,7 +113,7 @@ public class CrEpisode(){
int epIndex = 1;
var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special).
var isSpecial = !Regex.IsMatch(episode.EpisodeAndLanguages.Items[0].Episode ?? string.Empty, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special).
string newKey;
if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){
newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id);
@ -110,14 +123,14 @@ public class CrEpisode(){
episode.Key = newKey;
var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)"))?.SeasonTitle
?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
var title = episode.EpisodeAndLanguages.Items[0].Title;
var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString();
var languages = episode.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆
$"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆
Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
@ -125,7 +138,7 @@ public class CrEpisode(){
if (!serieshasversions){
Console.WriteLine("Couldn\'t find versions on episode, added languages with language array.");
}
return episode;
}
@ -160,17 +173,17 @@ public class CrEpisode(){
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ??
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle))?.SeriesTitle ??
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ??
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle))?.SeasonTitle ??
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title;
epMeta.SeasonId = item.SeasonId;
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
epMeta.ShowId = item.SeriesId;
epMeta.SeriesId = item.SeriesId;
epMeta.AbsolutEpisodeNumberE = epNum;
epMeta.Image = images[images.Count / 2].FirstOrDefault().Source;
epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source;
epMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = false,
Done = false,
@ -197,7 +210,7 @@ public class CrEpisode(){
}
}
if (retMeta.Data != null){
if (retMeta.Data is{ Count: > 0 }){
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
retMeta.Data.Add(epMetaData);
} else{
@ -215,7 +228,7 @@ public class CrEpisode(){
return retMeta;
}
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount,DateTime? firstWeekDay = null , bool forcedLang = false){
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){
await crunInstance.CrAuth.RefreshToken(true);
CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase();
complete.Data =[];
@ -257,7 +270,6 @@ public class CrEpisode(){
requestAmount += 50;
}
}
}
} else{
break;

View file

@ -34,7 +34,7 @@ public class CrMovies{
return null;
}
CrunchyMovieList movie = Helpers.Deserialize<CrunchyMovieList>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
CrunchyMovieList movie = Helpers.Deserialize<CrunchyMovieList>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyMovieList();
if (movie.Total < 1){
return null;
@ -67,9 +67,9 @@ public class CrMovies{
epMeta.EpisodeTitle = episodeP.Title;
epMeta.SeasonId = "";
epMeta.Season = "";
epMeta.ShowId = "";
epMeta.SeriesId = "";
epMeta.AbsolutEpisodeNumberE = "";
epMeta.Image = images[images.Count / 2].FirstOrDefault().Source;
epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source;
epMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = false,
Done = false,

View file

@ -14,44 +14,119 @@ namespace CRD.Downloader.Crunchyroll;
public class CrMusic{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task<CrunchyMusicVideo?> ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false){
return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos");
public async Task<CrunchyMusicVideoList?> ParseFeaturedMusicVideoByIdAsync(string seriesId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/featured/{seriesId}", crLocale, forcedLang);
if (musicVideos.Data is{ Count: > 0 } && updateHistory){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
}
return musicVideos;
}
public async Task<CrunchyMusicVideo?> ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideo = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos");
if (musicVideo != null && updateHistory){
await crunInstance.History.UpdateWithMusicEpisodeList([musicVideo]);
}
return musicVideo;
}
public async Task<CrunchyMusicVideo?> ParseConcertByIdAsync(string id, string crLocale, bool forcedLang = false){
return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/concerts");
public async Task<CrunchyMusicVideo?> ParseConcertByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){
var concert = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/concerts");
if (concert != null){
concert.EpisodeType = EpisodeType.Concert;
if (updateHistory){
await crunInstance.History.UpdateWithMusicEpisodeList([concert]);
}
}
return concert;
}
public async Task<CrunchyMusicVideoList?> ParseArtistMusicVideosByIdAsync(string id, string crLocale, bool forcedLang = false){
var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/music_videos", crLocale, forcedLang);
var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/concerts", crLocale, forcedLang);
public async Task<CrunchyMusicVideoList?> ParseArtistMusicVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang);
if (updateHistory){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
}
return musicVideos;
}
public async Task<CrunchyMusicVideoList?> ParseArtistConcertVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var concerts = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/concerts", crLocale, forcedLang);
if (concerts.Data.Count > 0){
foreach (var crunchyConcertVideo in concerts.Data){
crunchyConcertVideo.EpisodeType = EpisodeType.Concert;
}
}
if (updateHistory){
await crunInstance.History.UpdateWithMusicEpisodeList(concerts.Data);
}
return concerts;
}
public async Task<CrunchyMusicVideoList?> ParseArtistVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang);
var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/concerts", crLocale, forcedLang);
await Task.WhenAll(musicVideosTask, concertsTask);
var musicVideos = await musicVideosTask;
var concerts = await concertsTask;
musicVideos.Total += concerts.Total;
musicVideos.Data ??= new List<CrunchyMusicVideo>();
if (concerts.Data != null){
musicVideos.Total += concerts.Total;
if (concerts.Data.Count > 0){
foreach (var crunchyConcertVideo in concerts.Data){
crunchyConcertVideo.EpisodeType = EpisodeType.Concert;
}
musicVideos.Data.AddRange(concerts.Data);
}
if (updateHistory){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
}
return musicVideos;
}
public async Task<CrArtist> ParseArtistByIdAsync(string id, string crLocale, bool forcedLang = false){
var query = CreateQueryParameters(crLocale, forcedLang);
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/music/artists/{id}", HttpMethod.Get, true, true, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.Error.WriteLine($"Request to {ApiUrls.Content}/music/artists/{id} failed");
return new CrArtist();
}
var artistList = Helpers.Deserialize<CrunchyArtistList>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyArtistList();
return artistList.Data.FirstOrDefault() ?? new CrArtist();
}
private async Task<CrunchyMusicVideo?> ParseMediaByIdAsync(string id, string crLocale, bool forcedLang, string endpoint){
var mediaList = await FetchMediaListAsync($"{ApiUrls.Content}/{endpoint}/{id}", crLocale, forcedLang);
switch (mediaList.Total){
case < 1:
return null;
case 1 when mediaList.Data != null:
case 1 when mediaList.Data.Count > 0:
return mediaList.Data.First();
default:
Console.Error.WriteLine($"Multiple items returned for endpoint {endpoint} with ID {id}");
return mediaList.Data?.First();
return mediaList.Data.First();
}
}
@ -66,7 +141,7 @@ public class CrMusic{
return new CrunchyMusicVideoList();
}
return Helpers.Deserialize<CrunchyMusicVideoList>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
return Helpers.Deserialize<CrunchyMusicVideoList>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrunchyMusicVideoList();
}
private NameValueCollection CreateQueryParameters(string crLocale, bool forcedLang){
@ -88,15 +163,16 @@ public class CrMusic{
public CrunchyEpMeta EpisodeMeta(CrunchyMusicVideo episodeP){
var images = (episodeP.Images?.Thumbnail ?? new List<Image>{ new Image{ Source = "/notFound.png" } });
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = episodeP.Id, Versions = null } };
epMeta.SeriesTitle = "Music";
epMeta.SeasonTitle = episodeP.DisplayArtistName;
epMeta.SeriesTitle = episodeP.GetSeriesTitle();
epMeta.SeasonTitle = episodeP.GetSeasonTitle();
epMeta.EpisodeNumber = episodeP.SequenceNumber + "";
epMeta.EpisodeTitle = episodeP.Title;
epMeta.SeasonId = "";
epMeta.EpisodeTitle = episodeP.GetEpisodeTitle();
epMeta.SeasonId = episodeP.GetSeasonId();
epMeta.Season = "";
epMeta.ShowId = "";
epMeta.SeriesId = episodeP.GetSeriesId();
epMeta.AbsolutEpisodeNumberE = "";
epMeta.Image = images[images.Count / 2].Source;
epMeta.DownloadProgress = new DownloadProgress(){

View file

@ -7,13 +7,14 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Views;
using ReactiveUI;
namespace CRD.Downloader.Crunchyroll;
public class CrSeries(){
public class CrSeries{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public Dictionary<string, CrunchyEpMeta> ItemSelectMultiDub(Dictionary<string, EpisodeAndLanguage> eps, List<string> dubLang, bool? but, bool? all, List<string>? e){
@ -61,15 +62,15 @@ public class CrSeries(){
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle))?.SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle))?.SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title;
epMeta.SeasonId = item.SeasonId;
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
epMeta.ShowId = item.SeriesId;
epMeta.SeriesId = item.SeriesId;
epMeta.AbsolutEpisodeNumberE = epNum;
epMeta.Image = images[images.Count / 2].FirstOrDefault().Source;
epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source ?? "";
epMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = false,
Done = false,
@ -98,7 +99,7 @@ public class CrSeries(){
if (all is true || e != null && e.Contains(epNum)){
if (ret.TryGetValue(key, out var epMe)){
epMetaData.Lang = episode.Langs[index];
epMe.Data?.Add(epMetaData);
epMe.Data.Add(epMetaData);
} else{
epMetaData.Lang = episode.Langs[index];
epMeta.Data[0] = epMetaData;
@ -122,7 +123,7 @@ public class CrSeries(){
bool serieshasversions = true;
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale,forcedLocale);
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale, forcedLocale);
if (parsedSeries == null || parsedSeries.Data == null){
Console.Error.WriteLine("Parse Data Invalid");
@ -131,66 +132,59 @@ public class CrSeries(){
// var result = ParseSeriesResult(parsedSeries);
Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
if (crunInstance.CrunOptions.History){
crunInstance.History.CRUpdateSeries(id,"");
_ = crunInstance.History.CrUpdateSeries(id, "");
}
var cachedSeasonID = "";
var cachedSeasonId = "";
var seasonData = new CrunchyEpisodeList();
foreach (var s in parsedSeries.Data){
if (data?.S != null && s.Id != data.Value.S) continue;
int fallbackIndex = 0;
if (cachedSeasonID != s.Id){
seasonData = await GetSeasonDataById(s.Id, forcedLocale ? crLocale : "");
cachedSeasonID = s.Id;
}
if (seasonData.Data != null){
if (data?.S != null && s.Id != data.S) continue;
int fallbackIndex = 0;
if (cachedSeasonId != s.Id){
seasonData = await GetSeasonDataById(s.Id, forcedLocale ? crLocale : "");
cachedSeasonId = s.Id;
}
foreach (var episode in seasonData.Data){
// Prepare the episode array
EpisodeAndLanguage item;
if (seasonData.Data != null){
foreach (var episode in seasonData.Data){
// Prepare the episode array
EpisodeAndLanguage item;
string episodeNum = (episode.Episode != String.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}")) ?? string.Empty;
string episodeNum = (episode.Episode != String.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}")) ?? string.Empty;
var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier) ? s.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}";
var episodeKey = $"{seasonIdentifier}E{episodeNum}";
var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier) ? s.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}";
var episodeKey = $"{seasonIdentifier}E{episodeNum}";
if (!episodes.ContainsKey(episodeKey)){
item = new EpisodeAndLanguage{
Items = new List<CrunchyEpisode>(),
Langs = new List<LanguageItem>()
};
episodes[episodeKey] = item;
} else{
item = episodes[episodeKey];
}
if (!episodes.ContainsKey(episodeKey)){
item = new EpisodeAndLanguage{
Items = new List<CrunchyEpisode>(),
Langs = new List<LanguageItem>()
};
episodes[episodeKey] = item;
} else{
item = episodes[episodeKey];
}
if (episode.Versions != null){
foreach (var version in episode.Versions){
// Ensure there is only one of the same language
if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
item.Items.Add(episode);
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale));
}
}
} else{
// Episode didn't have versions, mark it as such to be logged.
serieshasversions = false;
// Ensure there is only one of the same language
if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){
// Push to arrays if there are no duplicates of the same language
if (episode.Versions != null){
foreach (var version in episode.Versions){
if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
item.Items.Add(episode);
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale));
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem());
}
}
} else{
serieshasversions = false;
if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){
item.Items.Add(episode);
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale) ?? new LanguageItem());
}
}
}
}
}
if (crunInstance.CrunOptions.History){
@ -215,21 +209,21 @@ public class CrSeries(){
string newKey;
if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){
newKey = $"SP{specialIndex}_" + item.Items[0].Episode ?? "SP" + (specialIndex + " " + item.Items[0].Id);
newKey = $"SP{specialIndex}_" + item.Items[0].Episode;// ?? "SP" + (specialIndex + " " + item.Items[0].Id);
} else{
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}";
}
episodes.Remove(key);
int counter = 1;
string originalKey = newKey;
while (episodes.ContainsKey(newKey)){
newKey = originalKey + "_" + counter;
counter++;
}
episodes.Add(newKey, item);
if (isSpecial){
@ -249,14 +243,14 @@ public class CrSeries(){
var key = kvp.Key;
var item = kvp.Value;
var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)"))?.SeasonTitle
?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
var title = item.Items[0].Title;
var seasonNumber = Helpers.ExtractNumberAfterS(item.Items[0].Identifier) ?? item.Items[0].SeasonNumber.ToString();
var languages = item.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆
$"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆
Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
}
@ -275,7 +269,7 @@ public class CrSeries(){
var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0);
var langList = value.Langs.Select(a => a.CrLocale).ToList();
Languages.SortListByLangList(langList);
return new Episode{
E = key.StartsWith("E") ? key.Substring(1) : key,
Lang = langList,
@ -285,8 +279,9 @@ public class CrSeries(){
SeasonTitle = Regex.Replace(value.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(),
EpisodeNum = key.StartsWith("SP") ? key : value.Items[0].EpisodeNumber?.ToString() ?? value.Items[0].Episode ?? "?",
Id = value.Items[0].SeasonId,
Img = images[images.Count / 2].FirstOrDefault().Source,
Img = images[images.Count / 2].FirstOrDefault()?.Source ?? "",
Description = value.Items[0].Description,
EpisodeType = EpisodeType.Episode,
Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds.
};
}).ToList();
@ -294,7 +289,7 @@ public class CrSeries(){
return crunchySeriesList;
}
public async Task<CrunchyEpisodeList> GetSeasonDataById(string seasonID, string? crLocale, bool forcedLang = false, bool log = false){
public async Task<CrunchyEpisodeList> GetSeasonDataById(string seasonId, string? crLocale, bool forcedLang = false, bool log = false){
CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List<CrunchyEpisode>(), Total = 0, Meta = new Meta() };
NameValueCollection query;
@ -309,7 +304,7 @@ public class CrSeries(){
}
}
var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}", HttpMethod.Get, true, true, query);
var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}", HttpMethod.Get, true, true, query);
var response = await HttpClientReq.Instance.SendHttpRequest(showRequest);
@ -330,14 +325,15 @@ public class CrSeries(){
}
}
var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}/episodes", HttpMethod.Get, true, true, query);
var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}/episodes", HttpMethod.Get, true, true, query);
var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest);
if (!episodeRequestResponse.IsOk){
Console.Error.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}");
} else{
episodeList = Helpers.Deserialize<CrunchyEpisodeList>(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
episodeList = Helpers.Deserialize<CrunchyEpisodeList>(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ??
new CrunchyEpisodeList(){ Data =[], Total = 0, Meta = new Meta() };
}
if (episodeList.Total < 1){
@ -351,6 +347,8 @@ public class CrSeries(){
var ret = new Dictionary<int, Dictionary<string, SeriesSearchItem>>();
int i = 0;
if (seasonsList.Data == null) return ret;
foreach (var item in seasonsList.Data){
i++;
foreach (var lang in Languages.languages){
@ -482,12 +480,12 @@ public class CrSeries(){
}
}
}
return series;
}
public async Task<CrBrowseSeriesBase?> GetAllSeries(string? crLocale){
CrBrowseSeriesBase? complete = new CrBrowseSeriesBase();
CrBrowseSeriesBase complete = new CrBrowseSeriesBase();
complete.Data =[];
var i = 0;

View file

@ -21,6 +21,7 @@ using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using CRD.Utils.Structs.History;
using CRD.ViewModels;
using CRD.ViewModels.Utils;
using CRD.Views;
using CRD.Views.Utils;
@ -130,9 +131,75 @@ public class CrunchyrollManager{
options.BackgroundImageOpacity = 0.5;
options.BackgroundImageBlurRadius = 10;
options.HistoryPageProperties = new HistoryPageProperties{
SelectedView = HistoryViewType.Posters,
SelectedSorting = SortingType.SeriesTitle,
SelectedFilter = FilterType.All,
ScaleValue = 0.73,
Ascending = false,
ShowSeries = true,
ShowArtists = true
};
options.History = true;
CfgManager.UpdateSettingsFromFile(options);
if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
var optionsYaml = new CrDownloadOptionsYaml();
optionsYaml.AutoDownload = false;
optionsYaml.RemoveFinishedDownload = false;
optionsYaml.Chapters = true;
optionsYaml.Hslang = "none";
optionsYaml.Force = "Y";
optionsYaml.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]";
optionsYaml.Partsize = 10;
optionsYaml.DlSubs = new List<string>{ "en-US" };
optionsYaml.SkipMuxing = false;
optionsYaml.MkvmergeOptions = new List<string>{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" };
optionsYaml.FfmpegOptions = new();
optionsYaml.DefaultAudio = "ja-JP";
optionsYaml.DefaultSub = "en-US";
optionsYaml.QualityAudio = "best";
optionsYaml.QualityVideo = "best";
optionsYaml.CcTag = "CC";
optionsYaml.CcSubsFont = "Trebuchet MS";
optionsYaml.FsRetryTime = 5;
optionsYaml.Numbers = 2;
optionsYaml.Timeout = 15000;
optionsYaml.DubLang = new List<string>(){ "ja-JP" };
optionsYaml.SimultaneousDownloads = 2;
// options.AccentColor = Colors.SlateBlue.ToString();
optionsYaml.Theme = "System";
optionsYaml.SelectedCalendarLanguage = "en-us";
optionsYaml.CalendarDubFilter = "none";
optionsYaml.CustomCalendar = true;
optionsYaml.DlVideoOnce = true;
optionsYaml.StreamEndpoint = "web/firefox";
optionsYaml.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
optionsYaml.HistoryLang = DefaultLocale;
optionsYaml.BackgroundImageOpacity = 0.5;
optionsYaml.BackgroundImageBlurRadius = 10;
optionsYaml.HistoryPageProperties = new HistoryPageProperties{
SelectedView = HistoryViewType.Posters,
SelectedSorting = SortingType.SeriesTitle,
SelectedFilter = FilterType.All,
ScaleValue = 0.73,
Ascending = false,
ShowSeries = true,
ShowArtists = true
};
optionsYaml.History = true;
CfgManager.UpdateSettingsFromFileYAML(optionsYaml);
options = Helpers.MigrateSettings(optionsYaml);
} else{
CfgManager.UpdateSettingsFromFile(options, CfgManager.PathCrDownloadOptions);
}
return options;
}
@ -147,7 +214,6 @@ public class CrunchyrollManager{
CrMusic = new CrMusic();
History = new History();
Profile = new CrProfile{
Username = "???",
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
@ -155,6 +221,11 @@ public class CrunchyrollManager{
PreferredContentSubtitleLanguage = DefaultLocale,
HasPremium = false,
};
if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
CfgManager.WriteCrSettings();
Helpers.DeleteFile(CfgManager.PathCrDownloadOptionsOld);
}
}
public async Task Init(){
@ -183,8 +254,11 @@ public class CrunchyrollManager{
}
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
Token = CfgManager.DeserializeFromFile<CrToken>(CfgManager.PathCrToken);
Token = CfgManager.ReadJsonFromFile<CrToken>(CfgManager.PathCrToken);
await CrAuth.LoginWithToken();
if (Path.Exists(CfgManager.PathCrTokenOld)){
Helpers.DeleteFile(CfgManager.PathCrTokenOld);
}
} else{
await CrAuth.AuthAnonymous();
}
@ -200,12 +274,11 @@ public class CrunchyrollManager{
);
if (historyList != null){
HistoryList = historyList;
Parallel.ForEach(historyList, historySeries => {
historySeries.Init();
foreach (var historySeriesSeason in historySeries.Seasons){
historySeriesSeason.Init();
}
@ -318,11 +391,13 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data);
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
}
if (options.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
}
}
} else{
@ -364,11 +439,12 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data);
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
if (preset != null && result.merger != null) await Helpers.RunFFmpegWithPresetAsync(result.merger.options.Output, preset, data);
}
if (options.DownloadToTempFolder){
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
}
}
@ -417,8 +493,8 @@ public class CrunchyrollManager{
QueueManager.Instance.ActiveDownloads--;
QueueManager.Instance.Queue.Refresh();
if (options.History && data.Data != null && data.Data.Count > 0){
History.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId);
if (options.History && data.Data is{ Count: > 0 } && (options.HistoryIncludeCrArtists && data.Music || !data.Music)){
History.SetAsDownloaded(data.SeriesId, data.SeasonId, data.Data.First().MediaId);
}
@ -473,9 +549,9 @@ public class CrunchyrollManager{
? options.DownloadDirPath
: CfgManager.PathVIDEOS_DIR;
var destinationPath = Path.Combine(destinationFolder ?? string.Empty, fileName ?? string.Empty);
var destinationPath = Path.Combine(destinationFolder ?? string.Empty, fileName);
string destinationDirectory = Path.GetDirectoryName(destinationPath);
string? destinationDirectory = Path.GetDirectoryName(destinationPath);
if (string.IsNullOrEmpty(destinationDirectory)){
Console.WriteLine("Invalid destination directory path.");
return;
@ -519,7 +595,7 @@ public class CrunchyrollManager{
foreach (var downloadedMedia in subs){
var subt = new SubtitleFonts();
subt.Language = downloadedMedia.Language;
subt.Fonts = downloadedMedia.Fonts;
subt.Fonts = downloadedMedia.Fonts ??[];
subsList.Add(subt);
}
@ -609,7 +685,9 @@ public class CrunchyrollManager{
}
}
syncVideosList.ForEach(syncVideo => Helpers.DeleteFile(syncVideo.Path));
syncVideosList.ForEach(syncVideo => {
if (syncVideo.Path != null) Helpers.DeleteFile(syncVideo.Path);
});
}
if (!options.Mp4 && !muxToMp3){
@ -725,7 +803,15 @@ public class CrunchyrollManager{
bool dlVideoOnce = false;
string fileDir = CfgManager.PathVIDEOS_DIR;
if (data.Data != null){
if (data.Data is{ Count: > 0 }){
options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
var sortedMetaData = data.Data
.OrderBy(metaData => options.DubLang.IndexOf(metaData.Lang?.CrLocale ?? string.Empty) != -1 ? options.DubLang.IndexOf(metaData.Lang?.CrLocale ?? string.Empty) : int.MaxValue)
.ToList();
data.Data = sortedMetaData;
foreach (CrunchyEpMetaData epMeta in data.Data){
Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}");
@ -753,10 +839,10 @@ public class CrunchyrollManager{
string mediaGuid = currentMediaId;
if (epMeta.Versions != null){
if (epMeta.Lang != null){
currentVersion = epMeta.Versions.Find(a => a.AudioLocale == epMeta.Lang?.CrLocale);
currentVersion = epMeta.Versions.Find(a => a.AudioLocale == epMeta.Lang?.CrLocale) ?? currentVersion;
} else if (data.SelectedDubs is{ Count: 1 }){
LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]);
currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang.CrLocale);
LanguageItem? lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]);
currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang?.CrLocale) ?? currentVersion;
} else if (epMeta.Versions.Count == 1){
currentVersion = epMeta.Versions[0];
}
@ -772,7 +858,7 @@ public class CrunchyrollManager{
mediaGuid = currentVersion.Guid;
if (!isPrimary){
primaryVersion = epMeta.Versions.Find(a => a.Original);
primaryVersion = epMeta.Versions.Find(a => a.Original) ?? currentVersion;
} else{
primaryVersion = currentVersion;
}
@ -816,12 +902,12 @@ public class CrunchyrollManager{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Too many active streams that couldn't be stopped"
ErrorText = "Too many active streams that couldn't be stopped\nClose open cruchyroll tabs in your browser"
};
}
}
MainWindow.Instance.ShowError("Couldn't get Playback Data");
MainWindow.Instance.ShowError("Couldn't get Playback Data\nTry again later or else check logs and crunchyroll");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
@ -1193,9 +1279,10 @@ public class CrunchyrollManager{
}
//string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.CrLocale);
string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale);
string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override)
string tempFile = Path.Combine(FileNameManager
.ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override)
.ToArray());
string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile);
@ -1207,7 +1294,7 @@ public class CrunchyrollManager{
} else if (options.Novids){
Console.WriteLine("Skipping video download...");
} else{
var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tsFile, tempTsFile, data, fileDir);
var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tempTsFile, data, fileDir);
tsFile = videoDownloadResult.tsFile;
@ -1226,7 +1313,7 @@ public class CrunchyrollManager{
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tsFile, tempTsFile, data, fileDir);
var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tempTsFile, data, fileDir);
tsFile = audioDownloadResult.tsFile;
@ -1403,7 +1490,7 @@ public class CrunchyrollManager{
videoDownloadMedia = new DownloadedMedia{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s",
Lang = lang.Value,
Lang = lang,
IsPrimary = isPrimary
};
files.Add(videoDownloadMedia);
@ -1471,7 +1558,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s",
Lang = lang.Value,
Lang = lang,
IsPrimary = isPrimary
});
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
@ -1487,7 +1574,7 @@ public class CrunchyrollManager{
videoDownloadMedia = new DownloadedMedia{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s",
Lang = lang.Value,
Lang = lang,
IsPrimary = isPrimary
};
files.Add(videoDownloadMedia);
@ -1498,7 +1585,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s",
Lang = lang.Value,
Lang = lang,
IsPrimary = isPrimary
});
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
@ -1547,7 +1634,13 @@ public class CrunchyrollManager{
}
// Finding language by code
var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang);
var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang) ?? new LanguageItem{
CrLocale = "und",
Locale = "un",
Code = "und",
Name = string.Empty,
Language = string.Empty
};
if (lang.Code == "und"){
Console.Error.WriteLine($"Unable to find language for code {curStream?.AudioLang}");
}
@ -1571,7 +1664,7 @@ public class CrunchyrollManager{
}
if (!options.SkipSubs && data.DownloadSubs.IndexOf("none") == -1){
await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir, data, (options.DlVideoOnce && dlVideoOnce && options.SyncTiming), videoDownloadMedia);
await DownloadSubtitles(options, pbData, audDub, fileName, files, fileDir, data, videoDownloadMedia);
} else{
Console.WriteLine("Subtitles downloading skipped!");
}
@ -1646,7 +1739,7 @@ public class CrunchyrollManager{
};
}
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files, string fileDir, CrunchyEpMeta data, bool needsDelay,
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files, string fileDir, CrunchyEpMeta data,
DownloadedMedia videoDownloadMedia){
if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[];
@ -1804,12 +1897,13 @@ public class CrunchyrollManager{
}
}
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data,
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tempTsFile, CrunchyEpMeta data,
string fileDir){
// Prepare for video download
int totalParts = chosenVideoSegments.segments.Count;
int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
string mathMsg = $"({mathParts}*{options.Partsize})";
string tsFile;
Console.WriteLine($"Total parts in video stream: {totalParts} {mathMsg}");
if (Path.IsPathRooted(outFile)){
@ -1843,9 +1937,10 @@ public class CrunchyrollManager{
return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile);
}
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data,
private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tempTsFile, CrunchyEpMeta data,
string fileDir){
// Prepare for audio download
string tsFile;
int totalParts = chosenAudioSegments.segments.Count;
int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
string mathMsg = $"({mathParts}*{options.Partsize})";
@ -1911,7 +2006,7 @@ public class CrunchyrollManager{
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint);
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint);
}
if (playbackRequestResponse.IsOk){
@ -1922,7 +2017,7 @@ public class CrunchyrollManager{
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint);
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint);
}
if (playbackRequestResponse.IsOk){
@ -1932,7 +2027,7 @@ public class CrunchyrollManager{
}
}
return (IsOk: playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent);
return (playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent);
}
private async Task<(bool IsOk, string ResponseContent)> SendPlaybackRequestAsync(string endpoint){
@ -1940,7 +2035,7 @@ public class CrunchyrollManager{
return await HttpClientReq.Instance.SendHttpRequest(request);
}
private async Task<(bool IsOk, string ResponseContent)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent) response, string mediaGuidId, string endpoint){
private async Task<(bool IsOk, string ResponseContent)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent) response, string endpoint){
if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response;
var error = StreamError.FromJson(response.ResponseContent);
@ -1990,7 +2085,7 @@ public class CrunchyrollManager{
temppbData.Meta = new PlaybackMeta{
AudioLocale = playStream.AudioLocale,
Versions = playStream.Versions,
Bifs = new List<string>{ playStream.Bifs },
Bifs = new List<string>{ playStream.Bifs ?? "" },
MediaId = mediaId,
Captions = playStream.Captions,
Subtitles = new Subtitles()
@ -2012,7 +2107,7 @@ public class CrunchyrollManager{
#endregion
private async Task<bool> ParseChapters(string currentMediaId, List<string> compiledChapters){
private async Task ParseChapters(string currentMediaId, List<string> compiledChapters){
var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null);
var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
@ -2024,11 +2119,11 @@ public class CrunchyrollManager{
try{
JObject jObject = JObject.Parse(showRequestResponse.ResponseContent);
if (jObject.TryGetValue("lastUpdate", out JToken lastUpdateToken)){
if (jObject.TryGetValue("lastUpdate", out JToken? lastUpdateToken)){
chapterData.lastUpdate = lastUpdateToken.ToObject<DateTime>();
}
if (jObject.TryGetValue("mediaId", out JToken mediaIdToken)){
if (jObject.TryGetValue("mediaId", out JToken? mediaIdToken)){
chapterData.mediaId = mediaIdToken.ToObject<string>();
}
@ -2037,7 +2132,7 @@ public class CrunchyrollManager{
foreach (var property in jObject.Properties()){
if (property.Value.Type == JTokenType.Object && property.Name != "lastUpdate" && property.Name != "mediaId"){
try{
CrunchyChapter chapter = property.Value.ToObject<CrunchyChapter>();
CrunchyChapter chapter = property.Value.ToObject<CrunchyChapter>() ?? new CrunchyChapter();
chapterData.Chapters.Add(chapter);
} catch (Exception ex){
Console.Error.WriteLine($"Error parsing chapter: {ex.Message}");
@ -2046,7 +2141,7 @@ public class CrunchyrollManager{
}
} catch (Exception ex){
Console.Error.WriteLine($"Error parsing JSON response: {ex.Message}");
return false;
return;
}
if (chapterData.Chapters.Count > 0){
@ -2098,8 +2193,6 @@ public class CrunchyrollManager{
compiledChapters.Add($"CHAPTER{chapterNumber}NAME={formattedChapterType} End");
}
}
return true;
}
} else{
Console.WriteLine("Chapter request failed, attempting old API ");
@ -2109,7 +2202,7 @@ public class CrunchyrollManager{
showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
if (showRequestResponse.IsOk){
CrunchyOldChapter chapterData = Helpers.Deserialize<CrunchyOldChapter>(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings);
CrunchyOldChapter chapterData = Helpers.Deserialize<CrunchyOldChapter>(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new CrunchyOldChapter();
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@ -2137,13 +2230,10 @@ public class CrunchyrollManager{
chapterNumber = (compiledChapters.Count / 2) + 1;
compiledChapters.Add($"CHAPTER{chapterNumber}={endFormatted}");
compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Episode");
return true;
return;
}
Console.Error.WriteLine("Chapter request failed");
return false;
}
return true;
}
}

View file

@ -12,8 +12,10 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using CRD.Utils.Structs.History;
using CRD.ViewModels;
using CRD.ViewModels.Utils;
@ -42,7 +44,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private bool _includeCcSubs;
[ObservableProperty]
private ComboBoxItem _selectedScaledBorderAndShadow;
@ -77,10 +79,10 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private bool _skipSubMux;
[ObservableProperty]
private double? _leadingNumbers;
[ObservableProperty]
private double? _partSize;
@ -107,7 +109,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedHSLang;
[ObservableProperty]
private ComboBoxItem _selectedDescriptionLang;
@ -131,81 +133,78 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem? _selectedAudioQuality;
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedSubLang = new();
private ObservableCollection<ListBoxItem> _selectedSubLang =[];
[ObservableProperty]
private Color _listBoxColor;
public ObservableCollection<ComboBoxItem> VideoQualityList{ get; } = new(){
new ComboBoxItem(){ Content = "best" },
new ComboBoxItem(){ Content = "1080" },
new ComboBoxItem(){ Content = "720" },
new ComboBoxItem(){ Content = "480" },
new ComboBoxItem(){ Content = "360" },
new ComboBoxItem(){ Content = "240" },
new ComboBoxItem(){ Content = "worst" },
};
public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } = new(){
new ComboBoxItem(){ Content = "best" },
new ComboBoxItem(){ Content = "128kB/s" },
new ComboBoxItem(){ Content = "96kB/s" },
new ComboBoxItem(){ Content = "64kB/s" },
new ComboBoxItem(){ Content = "worst" },
};
public ObservableCollection<ComboBoxItem> VideoQualityList{ get; } =[
new(){ Content = "best" },
new(){ Content = "1080" },
new(){ Content = "720" },
new(){ Content = "480" },
new(){ Content = "360" },
new(){ Content = "240" },
new(){ Content = "worst" }
];
public ObservableCollection<ComboBoxItem> HardSubLangList{ get; } = new(){
new ComboBoxItem(){ Content = "none" },
};
public ObservableCollection<ComboBoxItem> DescriptionLangList{ get; } = new(){
new ComboBoxItem(){ Content = "default" },
new ComboBoxItem(){ Content = "de-DE" },
new ComboBoxItem(){ Content = "en-US" },
new ComboBoxItem(){ Content = "es-419" },
new ComboBoxItem(){ Content = "es-ES" },
new ComboBoxItem(){ Content = "fr-FR" },
new ComboBoxItem(){ Content = "it-IT" },
new ComboBoxItem(){ Content = "pt-BR" },
new ComboBoxItem(){ Content = "pt-PT" },
new ComboBoxItem(){ Content = "ru-RU" },
new ComboBoxItem(){ Content = "hi-IN" },
new ComboBoxItem(){ Content = "ar-SA" },
};
public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } =[
new(){ Content = "best" },
new(){ Content = "128kB/s" },
new(){ Content = "96kB/s" },
new(){ Content = "64kB/s" },
new(){ Content = "worst" }
];
public ObservableCollection<ListBoxItem> DubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> HardSubLangList{ get; } =[
new(){ Content = "none" }
];
public ObservableCollection<ComboBoxItem> DescriptionLangList{ get; } =[
new(){ Content = "default" },
new(){ Content = "de-DE" },
new(){ Content = "en-US" },
new(){ Content = "es-419" },
new(){ Content = "es-ES" },
new(){ Content = "fr-FR" },
new(){ Content = "it-IT" },
new(){ Content = "pt-BR" },
new(){ Content = "pt-PT" },
new(){ Content = "ru-RU" },
new(){ Content = "hi-IN" },
new(){ Content = "ar-SA" }
];
public ObservableCollection<ListBoxItem> DubLangList{ get; } =[];
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } =[];
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } =[];
public ObservableCollection<ListBoxItem> SubLangList{ get; } = new(){
new ListBoxItem(){ Content = "all" },
new ListBoxItem(){ Content = "none" },
};
public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
new(){ Content = "all" },
new(){ Content = "none" }
];
public ObservableCollection<ComboBoxItem> StreamEndpoints{ get; } = new(){
new ComboBoxItem(){ Content = "web/firefox" },
new ComboBoxItem(){ Content = "console/switch" },
new ComboBoxItem(){ Content = "console/ps4" },
new ComboBoxItem(){ Content = "console/ps5" },
new ComboBoxItem(){ Content = "console/xbox_one" },
new ComboBoxItem(){ Content = "web/edge" },
// new ComboBoxItem(){ Content = "web/safari" },
new ComboBoxItem(){ Content = "web/chrome" },
new ComboBoxItem(){ Content = "web/fallback" },
// new ComboBoxItem(){ Content = "ios/iphone" },
// new ComboBoxItem(){ Content = "ios/ipad" },
new ComboBoxItem(){ Content = "android/phone" },
new ComboBoxItem(){ Content = "tv/samsung" },
};
public ObservableCollection<ComboBoxItem> StreamEndpoints{ get; } =[
new(){ Content = "web/firefox" },
new(){ Content = "console/switch" },
new(){ Content = "console/ps4" },
new(){ Content = "console/ps5" },
new(){ Content = "console/xbox_one" },
new(){ Content = "web/edge" },
// new (){ Content = "web/safari" },
new(){ Content = "web/chrome" },
new(){ Content = "web/fallback" },
// new (){ Content = "ios/iphone" },
// new (){ Content = "ios/ipad" },
new(){ Content = "android/phone" },
new(){ Content = "tv/samsung" }
];
[ObservableProperty]
private bool _isEncodeEnabled;
@ -214,8 +213,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
private StringItem _selectedEncodingPreset;
public ObservableCollection<StringItem> EncodingPresetsList{ get; } = new();
[ObservableProperty]
private bool _cCSubsMuxingFlag;
@ -224,11 +223,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private bool _signsSubsAsForced;
[ObservableProperty]
private bool _searchFetchFeaturedMusic;
private bool settingsLoaded;
public CrunchyrollSettingsViewModel(){
foreach (var languageItem in Languages.languages){
HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
@ -244,7 +245,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrDownloadOptions options = CrunchyrollManager.Instance.CrunOptions;
StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == options.EncodingPresetName) ?? null;
StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => !string.IsNullOrEmpty(a.stringValue) && a.stringValue == options.EncodingPresetName) ?? null;
SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null;
@ -275,7 +276,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem);
}
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
@ -301,22 +302,23 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers;
FileName = options.FileName;
SearchFetchFeaturedMusic = options.SearchFetchFeaturedMusic;
ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null;
SelectedAudioQuality = qualityAudio ?? AudioQualityList[0];
ComboBoxItem? qualityVideo = VideoQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityVideo) ?? null;
SelectedVideoQuality = qualityVideo ?? VideoQualityList[0];
MkvMergeOptions.Clear();
if (options.MkvmergeOptions != null){
if (options.MkvmergeOptions is{ Count: > 0 }){
foreach (var mkvmergeParam in options.MkvmergeOptions){
MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam });
}
}
FfmpegOptions.Clear();
if (options.FfmpegOptions != null){
if (options.FfmpegOptions is{ Count: > 0 }){
foreach (var ffmpegParam in options.FfmpegOptions){
FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam });
}
@ -341,7 +343,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
if (!settingsLoaded){
return;
}
CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced = SignsSubsAsForced;
CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag = CCSubsMuxingFlag;
CrunchyrollManager.Instance.CrunOptions.CcSubsFont = CCSubsFont;
@ -364,8 +366,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.FileName = FileName;
CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs;
CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs;
CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 0), 0, 10000);
CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 1), 1, 10000);
CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic = SearchFetchFeaturedMusic;
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
List<string> softSubs = new List<string>();
@ -378,7 +381,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
string descLang = SelectedDescriptionLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
string hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang;
@ -398,7 +401,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + "";
CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + "";
List<string> mkvmergeParams = new List<string>();
foreach (var mkvmergeParam in MkvMergeOptions){
mkvmergeParams.Add(mkvmergeParam.stringValue);
@ -413,7 +416,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams;
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
@ -469,7 +472,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
FfmpegOptions.Remove(param);
RaisePropertyChanged(nameof(FfmpegOptions));
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings();
@ -515,7 +518,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.HistoryList =[];
}
}
}
[RelayCommand]
@ -542,9 +544,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
}
settingsLoaded = true;
StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == CrunchyrollManager.Instance.CrunOptions.EncodingPresetName) ?? null;
StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => string.IsNullOrEmpty(a.stringValue) && a.stringValue == CrunchyrollManager.Instance.CrunOptions.EncodingPresetName) ?? null;
SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
}
}
}

View file

@ -191,6 +191,22 @@
</controls:SettingsExpander>
<controls:SettingsExpander Header="Search Settings"
IconSource="Find"
Description="Adjust search settings"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Include featured Music"
Description="Fetch featured music when searching for a series">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SearchFetchFeaturedMusic}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Download Settings"
IconSource="Download"
Description="Adjust download settings"

View file

@ -2,13 +2,14 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData;
@ -16,10 +17,10 @@ using ReactiveUI;
namespace CRD.Downloader;
public class History(){
public class History{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task<bool> CRUpdateSeries(string seriesId, string? seasonId){
public async Task<bool> CrUpdateSeries(string seriesId, string? seasonId){
await crunInstance.CrAuth.RefreshToken(true);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true);
@ -46,7 +47,8 @@ public class History(){
var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
if (seasonData.Data is{ Count: > 0 }) await UpdateWithSeasonData(seasonData.Data);
if (seasonData.Data is{ Count: > 0 }) await UpdateWithSeasonData(seasonData.Data.ToList<IHistorySource>());
}
@ -64,6 +66,141 @@ public class History(){
}
public async Task UpdateWithMusicEpisodeList(List<CrunchyMusicVideo> episodeList){
if (episodeList is{ Count: > 0 }){
if (crunInstance.CrunOptions is{ History: true, HistoryIncludeCrArtists: true }){
var concertGroups = episodeList.Where(e => e.EpisodeType == EpisodeType.Concert).GroupBy(e => e.Artist.Id);
var musicVideoGroups = episodeList.Where(e => e.EpisodeType == EpisodeType.MusicVideo).GroupBy(e => e.Artist.Id);
foreach (var concertGroup in concertGroups){
await UpdateWithSeasonData(concertGroup.ToList<IHistorySource>());
}
foreach (var musicVideoGroup in musicVideoGroups){
await UpdateWithSeasonData(musicVideoGroup.ToList<IHistorySource>());
}
}
}
}
public async Task UpdateWithEpisodeList(List<CrunchyEpisode> episodeList){
if (episodeList is{ Count: > 0 }){
var episodeVersions = episodeList.First().Versions;
if (episodeVersions != null){
var version = episodeVersions.Find(a => a.Original);
if (version?.AudioLocale != episodeList.First().AudioLocale){
await CrUpdateSeries(episodeList.First().SeriesId, version?.SeasonGuid);
return;
}
} else{
await CrUpdateSeries(episodeList.First().SeriesId, "");
return;
}
await UpdateWithSeasonData(episodeList.ToList<IHistorySource>());
}
}
/// <summary>
/// This method updates the History with a list of episodes. The episodes have to be from the same season.
/// </summary>
private async Task UpdateWithSeasonData(List<IHistorySource> episodeList){
if (episodeList is{ Count: > 0 }){
var firstEpisode = episodeList.First();
var seriesId = firstEpisode.GetSeriesId();
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){
historySeries.HistorySeriesAddDate ??= DateTime.Now;
historySeries.SeriesType = firstEpisode.GetSeriesType();
historySeries.SeriesStreamingService = StreamingService.Crunchyroll;
await RefreshSeriesData(seriesId, historySeries);
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.GetSeasonId());
if (historySeason != null){
historySeason.SeasonTitle = firstEpisode.GetSeasonTitle();
historySeason.SeasonNum = firstEpisode.GetSeasonNum();
historySeason.SpecialSeason = firstEpisode.IsSpecialSeason();
foreach (var historySource in episodeList){
if (historySource.GetSeasonId() != historySeason.SeasonId){
continue;
}
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == historySource.GetEpisodeId());
if (historyEpisode == null){
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = historySource.GetEpisodeTitle(),
EpisodeDescription = historySource.GetEpisodeDescription(),
EpisodeId = historySource.GetEpisodeId(),
Episode = historySource.GetEpisodeNumber(),
EpisodeSeasonNum = historySource.GetSeasonNum(),
SpecialEpisode = historySource.IsSpecialEpisode(),
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType()
};
historySeason.EpisodesList.Add(newHistoryEpisode);
} else{
//Update existing episode
historyEpisode.EpisodeTitle = historySource.GetEpisodeTitle();
historyEpisode.SpecialEpisode = historySource.IsSpecialEpisode();
historyEpisode.EpisodeDescription = historySource.GetEpisodeDescription();
historyEpisode.EpisodeId = historySource.GetEpisodeId();
historyEpisode.Episode = historySource.GetEpisodeNumber();
historyEpisode.EpisodeSeasonNum = historySource.GetSeasonNum();
historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate();
historyEpisode.EpisodeType = historySource.GetEpisodeType();
historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang();
historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs();
}
}
historySeason.EpisodesList.Sort(new NumericStringPropertyComparer());
} else{
var newSeason = NewHistorySeason(episodeList, firstEpisode);
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
historySeries.Seasons.Add(newSeason);
newSeason.Init();
}
historySeries.UpdateNewEpisodes();
} else if (!string.IsNullOrEmpty(seriesId)){
historySeries = new HistorySeries{
SeriesTitle = firstEpisode.GetSeriesTitle(),
SeriesId = firstEpisode.GetSeriesId(),
Seasons =[],
HistorySeriesAddDate = DateTime.Now,
SeriesType = firstEpisode.GetSeriesType(),
SeriesStreamingService = StreamingService.Crunchyroll
};
crunInstance.HistoryList.Add(historySeries);
var newSeason = NewHistorySeason(episodeList, firstEpisode);
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
await RefreshSeriesData(seriesId, historySeries);
historySeries.Seasons.Add(newSeason);
historySeries.UpdateNewEpisodes();
historySeries.Init();
newSeason.Init();
}
SortItems();
if (historySeries != null){
SortSeasons(historySeries);
}
}
}
public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
@ -230,175 +367,57 @@ public class History(){
}
public async Task UpdateWithSeasonData(List<CrunchyEpisode>? episodeList, bool skippVersionCheck = true){
if (episodeList != null){
if (!skippVersionCheck){
var episodeVersions = episodeList.First().Versions;
if (episodeVersions != null){
var version = episodeVersions.Find(a => a.Original);
if (version.AudioLocale != episodeList.First().AudioLocale){
await CRUpdateSeries(episodeList.First().SeriesId, version.SeasonGuid);
return;
}
} else{
await CRUpdateSeries(episodeList.First().SeriesId, "");
return;
}
}
var firstEpisode = episodeList.First();
var seriesId = firstEpisode.SeriesId;
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){
historySeries.HistorySeriesAddDate ??= DateTime.Now;
await RefreshSeriesData(seriesId, historySeries);
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.SeasonId);
if (historySeason != null){
historySeason.SeasonTitle = firstEpisode.SeasonTitle;
historySeason.SeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "";
historySeason.SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier);
foreach (var crunchyEpisode in episodeList){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id);
if (historyEpisode == null){
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description,
EpisodeId = crunchyEpisode.Id,
Episode = crunchyEpisode.Episode,
EpisodeSeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList),
HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales),
EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate
};
historySeason.EpisodesList.Add(newHistoryEpisode);
} else{
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
//Update existing episode
historyEpisode.EpisodeTitle = GetEpisodeTitle(crunchyEpisode);
historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _);
historyEpisode.EpisodeDescription = crunchyEpisode.Description;
historyEpisode.EpisodeId = crunchyEpisode.Id;
historyEpisode.Episode = crunchyEpisode.Episode;
historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + "";
historyEpisode.EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate;
historyEpisode.HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList);
historyEpisode.HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales);
}
}
historySeason.EpisodesList.Sort(new NumericStringPropertyComparer());
} else{
var newSeason = NewHistorySeason(episodeList, firstEpisode);
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
historySeries.Seasons.Add(newSeason);
newSeason.Init();
}
historySeries.UpdateNewEpisodes();
} else{
historySeries = new HistorySeries{
SeriesTitle = firstEpisode.SeriesTitle,
SeriesId = firstEpisode.SeriesId,
Seasons =[],
HistorySeriesAddDate = DateTime.Now,
};
crunInstance.HistoryList.Add(historySeries);
var newSeason = NewHistorySeason(episodeList, firstEpisode);
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
await RefreshSeriesData(seriesId, historySeries);
historySeries.Seasons.Add(newSeason);
historySeries.UpdateNewEpisodes();
historySeries.Init();
newSeason.Init();
}
SortItems();
SortSeasons(historySeries);
}
}
private CrSeriesBase? cachedSeries;
private string GetEpisodeTitle(CrunchyEpisode crunchyEpisode){
if (crunchyEpisode.Identifier.Contains("|M|")){
if (string.IsNullOrEmpty(crunchyEpisode.Title)){
if (crunchyEpisode.SeasonTitle.StartsWith(crunchyEpisode.SeriesTitle)){
var splitTitle = crunchyEpisode.SeasonTitle.Split(new[]{ crunchyEpisode.SeriesTitle }, StringSplitOptions.None);
var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0];
var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", "");
return cleanedTitle;
}
return crunchyEpisode.SeasonTitle;
}
if (crunchyEpisode.Title.StartsWith(crunchyEpisode.SeriesTitle)){
var splitTitle = crunchyEpisode.Title.Split(new[]{ crunchyEpisode.SeriesTitle }, StringSplitOptions.None);
var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0];
var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", "");
return cleanedTitle;
}
return crunchyEpisode.Title;
}
return crunchyEpisode.Title;
}
private SeriesDataCache? cachedSeries;
private async Task RefreshSeriesData(string seriesId, HistorySeries historySeries){
if (cachedSeries == null || (cachedSeries.Data != null && cachedSeries.Data.First().Id != seriesId)){
cachedSeries = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
} else{
if (cachedSeries?.Data != null){
var series = cachedSeries.Data.First();
historySeries.SeriesDescription = series.Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries);
historySeries.SeriesTitle = series.Title;
historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales);
historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales);
if (cachedSeries == null || (!string.IsNullOrEmpty(cachedSeries.SeriesId) && cachedSeries.SeriesId != seriesId)){
if (historySeries.SeriesType == SeriesType.Series){
var seriesData = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
if (seriesData is{ Data: not null }){
var firstEpisode = seriesData.Data.First();
cachedSeries = new SeriesDataCache{
SeriesDescription = firstEpisode.Description,
SeriesId = seriesId,
SeriesTitle = firstEpisode.Title,
ThumbnailImageUrl = GetSeriesThumbnail(seriesData),
HistorySeriesAvailableDubLang = Languages.SortListByLangList(firstEpisode.AudioLocales),
HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(firstEpisode.SubtitleLocales)
};
historySeries.SeriesDescription = cachedSeries.SeriesDescription;
historySeries.ThumbnailImageUrl = cachedSeries.ThumbnailImageUrl;
historySeries.SeriesTitle = cachedSeries.SeriesTitle;
historySeries.HistorySeriesAvailableDubLang = cachedSeries.HistorySeriesAvailableDubLang;
historySeries.HistorySeriesAvailableSoftSubs = cachedSeries.HistorySeriesAvailableSoftSubs;
}
} else if (historySeries.SeriesType == SeriesType.Artist){
var artisteData = await crunInstance.CrMusic.ParseArtistByIdAsync(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang,
true);
if (!string.IsNullOrEmpty(artisteData.Id)){
cachedSeries = new SeriesDataCache{
SeriesDescription = artisteData.Description ?? "",
SeriesId = artisteData.Id,
SeriesTitle = artisteData.Name ?? "",
ThumbnailImageUrl = artisteData.Images.PosterTall.FirstOrDefault(e => e.Height == 360)?.Source ?? "",
HistorySeriesAvailableDubLang =[],
HistorySeriesAvailableSoftSubs =[]
};
historySeries.SeriesDescription = cachedSeries.SeriesDescription;
historySeries.ThumbnailImageUrl = cachedSeries.ThumbnailImageUrl;
historySeries.SeriesTitle = cachedSeries.SeriesTitle;
historySeries.HistorySeriesAvailableDubLang = cachedSeries.HistorySeriesAvailableDubLang;
historySeries.HistorySeriesAvailableSoftSubs = cachedSeries.HistorySeriesAvailableSoftSubs;
}
}
} else{
if (cachedSeries != null){
historySeries.SeriesDescription = cachedSeries.SeriesDescription;
historySeries.ThumbnailImageUrl = cachedSeries.ThumbnailImageUrl;
historySeries.SeriesTitle = cachedSeries.SeriesTitle;
historySeries.HistorySeriesAvailableDubLang = cachedSeries.HistorySeriesAvailableDubLang;
historySeries.HistorySeriesAvailableSoftSubs = cachedSeries.HistorySeriesAvailableSoftSubs;
}
return;
}
if (cachedSeries?.Data != null){
var series = cachedSeries.Data.First();
historySeries.SeriesDescription = series.Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries);
historySeries.SeriesTitle = series.Title;
historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales);
historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales);
}
}
@ -441,8 +460,8 @@ public class History(){
var sortedSeriesDates = sortingDir
? CrunchyrollManager.Instance.HistoryList
.OrderByDescending(s => {
var date = ParseDate(s.SonarrNextAirDate, today);
return date.HasValue ? date.Value : DateTime.MinValue;
var date = ParseDate(s.SonarrNextAirDate ?? string.Empty, today);
return date ?? DateTime.MinValue;
})
.ThenByDescending(s => s.SonarrNextAirDate == "Today" ? 1 : 0)
.ThenBy(s => string.IsNullOrEmpty(s.SonarrNextAirDate) ? 1 : 0)
@ -452,8 +471,8 @@ public class History(){
.OrderByDescending(s => s.SonarrNextAirDate == "Today")
.ThenBy(s => s.SonarrNextAirDate == "Today" ? s.SeriesTitle : null)
.ThenBy(s => {
var date = ParseDate(s.SonarrNextAirDate, today);
return date.HasValue ? date.Value : DateTime.MaxValue;
var date = ParseDate(s.SonarrNextAirDate ?? string.Empty, today);
return date ?? DateTime.MaxValue;
})
.ThenBy(s => s.SeriesTitle)
.ToList();
@ -499,55 +518,40 @@ public class History(){
private string GetSeriesThumbnail(CrSeriesBase series){
// var series = await crunInstance.CrSeries.SeriesById(seriesId);
if ((series.Data ?? Array.Empty<SeriesBaseItem>()).First().Images.PosterTall?.Count > 0){
return series.Data.First().Images.PosterTall.First().First(e => e.Height == 360).Source;
if (series.Data != null && series.Data.First().Images.PosterTall?.Count > 0){
var imagesPosterTall = series.Data.First().Images.PosterTall;
if (imagesPosterTall != null) return imagesPosterTall.First().First(e => e.Height == 360).Source;
}
return "";
}
private bool CheckStringForSpecial(string identifier){
if (string.IsNullOrEmpty(identifier)){
return false;
}
// Regex pattern to match any sequence that does NOT contain "|S" followed by one or more digits immediately after
string pattern = @"^(?!.*\|S\d+).*";
// Use Regex.IsMatch to check if the identifier matches the pattern
return Regex.IsMatch(identifier, pattern);
}
private HistorySeason NewHistorySeason(List<CrunchyEpisode> seasonData, CrunchyEpisode firstEpisode){
private HistorySeason NewHistorySeason(List<IHistorySource> episodeList, IHistorySource firstEpisode){
var newSeason = new HistorySeason{
SeasonTitle = firstEpisode.SeasonTitle,
SeasonId = firstEpisode.SeasonId,
SeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "",
SeasonTitle = firstEpisode.GetSeasonTitle(),
SeasonId = firstEpisode.GetSeasonId(),
SeasonNum = firstEpisode.GetSeasonNum(),
EpisodesList =[],
SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier)
SpecialSeason = firstEpisode.IsSpecialSeason()
};
foreach (var crunchyEpisode in seasonData){
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
foreach (var historySource in episodeList){
if (historySource.GetSeasonId() != newSeason.SeasonId){
continue;
}
Languages.SortListByLangList(langList);
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description,
EpisodeId = crunchyEpisode.Id,
Episode = crunchyEpisode.Episode,
EpisodeSeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = langList,
HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales,
EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate
EpisodeTitle = historySource.GetEpisodeTitle(),
EpisodeDescription = historySource.GetEpisodeDescription(),
EpisodeId = historySource.GetEpisodeId(),
Episode = historySource.GetEpisodeNumber(),
EpisodeSeasonNum = historySource.GetSeasonNum(),
SpecialEpisode = historySource.IsSpecialEpisode(),
HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType()
};
newSeason.EpisodesList.Add(newHistoryEpisode);
@ -563,7 +567,7 @@ public class History(){
foreach (var historySeries in crunInstance.HistoryList){
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle);
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty);
if (sonarrSeries != null){
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
@ -581,7 +585,7 @@ public class History(){
}
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
List<SonarrEpisode>? episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
List<SonarrEpisode> episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
@ -603,8 +607,10 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode.Id + "";
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode.HasFile;
historyEpisode.SonarrIsMonitored = episode.Monitored;
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
lock (_lock){
episodes.Remove(episode);
}
@ -622,8 +628,8 @@ public class History(){
return false;
}
var episodeNumberStr = ele.EpisodeNumber.ToString() ?? string.Empty;
var seasonNumberStr = ele.SeasonNumber.ToString() ?? string.Empty;
var episodeNumberStr = ele.EpisodeNumber.ToString();
var seasonNumberStr = ele.SeasonNumber.ToString();
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
});
@ -631,6 +637,7 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode.Id + "";
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode.HasFile;
historyEpisode.SonarrIsMonitored = episode.Monitored;
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
lock (_lock){
@ -649,6 +656,7 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode1.Id + "";
historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode1.HasFile;
historyEpisode.SonarrIsMonitored = episode1.Monitored;
historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + "";
lock (_lock){
@ -666,6 +674,7 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode2.Id + "";
historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode2.HasFile;
historyEpisode.SonarrIsMonitored = episode2.Monitored;
historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + "";
lock (_lock){
@ -677,8 +686,6 @@ public class History(){
}
}
});
}
}
@ -706,6 +713,10 @@ public class History(){
}
private SonarrSeries? FindClosestMatch(string title){
if (string.IsNullOrEmpty(title)){
return null;
}
SonarrSeries? closestMatch = null;
double highestSimilarity = 0.0;
@ -748,7 +759,7 @@ public class History(){
Parallel.ForEach(episodeList, episode => {
if (episode != null){
double similarity = CalculateSimilarity(episode.Title, title);
double similarity = CalculateSimilarity(episode.Title ?? string.Empty, title);
lock (lockObject) // Ensure thread-safe access to shared variables
{
if (similarity > highestSimilarity){
@ -810,13 +821,13 @@ public class History(){
}
public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
public int Compare(HistoryEpisode x, HistoryEpisode y){
if (double.TryParse(x.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) &&
double.TryParse(y.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){
public int Compare(HistoryEpisode? x, HistoryEpisode? y){
if (double.TryParse(x?.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) &&
double.TryParse(y?.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){
return xDouble.CompareTo(yDouble);
}
// Fall back to string comparison if not parseable as doubles
return string.Compare(x.Episode, y.Episode, StringComparison.Ordinal);
return string.Compare(x?.Episode, y?.Episode, StringComparison.Ordinal);
}
}

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -13,7 +12,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Utils.Updater;
using FluentAvalonia.Styling;
@ -51,10 +49,10 @@ public partial class ProgramManager : ObservableObject{
private bool _updateAvailable = true;
[ObservableProperty]
private bool _finishedLoading = false;
private bool _finishedLoading;
[ObservableProperty]
private bool _navigationLock = false;
private bool _navigationLock;
#endregion
@ -66,7 +64,7 @@ public partial class ProgramManager : ObservableObject{
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
private bool exitOnTaskFinish = false;
private bool exitOnTaskFinish;
public IStorageProvider StorageProvider;
@ -111,7 +109,7 @@ public partial class ProgramManager : ObservableObject{
await Task.WhenAll(tasks);
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress != null && e.DownloadProgress.Done != true)){
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress.Done != true)){
Console.WriteLine("Waiting for downloads to complete...");
await Task.Delay(2000); // Wait for 2 second before checking again
}

View file

@ -54,7 +54,7 @@ public class QueueManager{
if (e.Action == NotifyCollectionChangedAction.Remove){
if (e.OldItems != null)
foreach (var eOldItem in e.OldItems){
var downloadItem = DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(eOldItem));
var downloadItem = DownloadItemModels.FirstOrDefault(downloadItem => downloadItem.epMeta.Equals(eOldItem));
if (downloadItem != null){
DownloadItemModels.Remove(downloadItem);
} else{
@ -87,13 +87,17 @@ public class QueueManager{
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){
if (string.IsNullOrEmpty(epId)){
return;
}
await CrunchyrollManager.Instance.CrAuth.RefreshToken(true);
var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale);
if (episodeL != null){
if (episodeL.Value.IsPremiumOnly && !CrunchyrollManager.Instance.Profile.HasPremium){
if (episodeL.IsPremiumOnly && !CrunchyrollManager.Instance.Profile.HasPremium){
MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3));
return;
}
@ -206,6 +210,15 @@ public class QueueManager{
if (musicVideo != null){
var musicVideoMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(musicVideo);
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", "");
if (CrunchyrollManager.Instance.CrunOptions.History){
historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(musicVideoMeta.SeriesId, musicVideoMeta.SeasonId, musicVideoMeta.Data.First().MediaId);
}
musicVideoMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
Queue.Add(musicVideoMeta);
MessageBus.Current.SendMessage(new ToastMessage($"Added music video to the queue", ToastType.Information, 1));
}
@ -218,6 +231,14 @@ public class QueueManager{
if (concert != null){
var concertMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(concert);
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", "");
if (CrunchyrollManager.Instance.CrunOptions.History){
historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(concertMeta.SeriesId, concertMeta.SeasonId, concertMeta.Data.First().MediaId);
}
concertMeta.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
Queue.Add(concertMeta);
MessageBus.Current.SendMessage(new ToastMessage($"Added concert to the queue", ToastType.Information, 1));
}
@ -232,7 +253,7 @@ public class QueueManager{
foreach (var crunchyEpMeta in selected.Values.ToList()){
if (crunchyEpMeta.Data?.First() != null){
if (CrunchyrollManager.Instance.CrunOptions.History){
var historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDownloadDir(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
var historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDownloadDir(crunchyEpMeta.SeriesId, crunchyEpMeta.SeasonId, crunchyEpMeta.Data.First().MediaId);
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
if (historyEpisode.historyEpisode != null){
if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){
@ -258,7 +279,7 @@ public class QueueManager{
}
}
var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId);
var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.SeriesId, crunchyEpMeta.SeasonId);
crunchyEpMeta.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;

View file

@ -15,7 +15,7 @@ using ProtoBuf;
namespace CRD.Utils.DRM;
public struct ContentDecryptionModule{
public class ContentDecryptionModule{
public byte[] privateKey{ get; set; }
public byte[] identifierBlob{ get; set; }
}

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CRD.Utils.Files;
namespace CRD.Utils.DRM;

View file

@ -5,6 +5,24 @@ using Newtonsoft.Json;
namespace CRD.Utils;
public enum StreamingService{
Crunchyroll,
Unknown
}
public enum EpisodeType{
MusicVideo,
Concert,
Episode,
Unknown
}
public enum SeriesType{
Artist,
Series,
Unknown
}
[DataContract]
[JsonConverter(typeof(LocaleConverter))]
public enum Locale{

View file

@ -4,21 +4,25 @@ using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Runtime.InteropServices;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using Newtonsoft.Json;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace CRD.Utils;
namespace CRD.Utils.Files;
public class CfgManager{
private static string WorkingDirectory = AppContext.BaseDirectory;
public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.yml");
public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.yml");
public static readonly string PathCrTokenOld = Path.Combine(WorkingDirectory, "config", "cr_token.yml");
public static readonly string PathCrDownloadOptionsOld = Path.Combine(WorkingDirectory, "config", "settings.yml");
public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.json");
public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.json");
public static readonly string PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json");
public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json");
@ -83,18 +87,36 @@ public class CfgManager{
}
}
public static void WriteJsonResponseToYamlFile(string jsonResponse, string filePath){
// Convert JSON to an object
var deserializer = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance) // Adjust this as needed
.Build();
var jsonObject = deserializer.Deserialize<object>(jsonResponse);
public static void WriteCrSettings(){
WriteJsonToFile(PathCrDownloadOptions, CrunchyrollManager.Instance.CrunOptions);
}
// Convert the object to YAML
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention
.Build();
var yaml = serializer.Serialize(jsonObject);
// public static void WriteTokenToYamlFile(CrToken token, string filePath){
// // Convert the object to YAML
// var serializer = new SerializerBuilder()
// .WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention
// .Build();
// var yaml = serializer.Serialize(token);
//
// string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;
//
// if (!Directory.Exists(dirPath)){
// Directory.CreateDirectory(dirPath);
// }
//
// if (!File.Exists(filePath)){
// using (var fileStream = File.Create(filePath)){
// }
// }
//
// // Write the YAML to a file
// File.WriteAllText(filePath, yaml);
// }
public static void UpdateSettingsFromFile<T>(T options, string filePath) where T : class{
if (options == null){
throw new ArgumentNullException(nameof(options));
}
string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;
@ -103,74 +125,81 @@ public class CfgManager{
}
if (!File.Exists(filePath)){
// Create the file if it doesn't exist
using (var fileStream = File.Create(filePath)){
}
}
// Write the YAML to a file
File.WriteAllText(filePath, yaml);
}
public static void WriteTokenToYamlFile(CrToken token, string filePath){
// Convert the object to YAML
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention
.Build();
var yaml = serializer.Serialize(token);
string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;
if (!Directory.Exists(dirPath)){
Directory.CreateDirectory(dirPath);
}
if (!File.Exists(filePath)){
using (var fileStream = File.Create(filePath)){
}
}
// Write the YAML to a file
File.WriteAllText(filePath, yaml);
}
public static void WriteSettingsToFile(){
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance) // Use the underscore style
.Build();
string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty;
if (!Directory.Exists(dirPath)){
Directory.CreateDirectory(dirPath);
}
if (!File.Exists(PathCrDownloadOptions)){
using (var fileStream = File.Create(PathCrDownloadOptions)){
}
}
var yaml = serializer.Serialize(CrunchyrollManager.Instance.CrunOptions);
// Write to file
File.WriteAllText(PathCrDownloadOptions, yaml);
}
public static void UpdateSettingsFromFile(CrDownloadOptions options){
string dirPath = Path.GetDirectoryName(PathCrDownloadOptions) ?? string.Empty;
if (!Directory.Exists(dirPath)){
Directory.CreateDirectory(dirPath);
}
if (!File.Exists(PathCrDownloadOptions)){
using (var fileStream = File.Create(PathCrDownloadOptions)){
}
return;
}
var input = File.ReadAllText(PathCrDownloadOptions);
var input = File.ReadAllText(filePath);
if (string.IsNullOrWhiteSpace(input)){
return;
}
// Deserialize JSON into a dictionary to get top-level properties
var propertiesPresentInJson = GetTopLevelPropertiesInJson(input);
// Deserialize JSON into the provided options object type
var loadedOptions = JsonConvert.DeserializeObject<T>(input);
if (loadedOptions == null){
return;
}
foreach (PropertyInfo property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)){
// Use the JSON property name if present, otherwise use the property name
string jsonPropertyName = property.Name;
var jsonPropertyAttribute = property.GetCustomAttribute<JsonPropertyAttribute>();
if (jsonPropertyAttribute != null){
jsonPropertyName = jsonPropertyAttribute.PropertyName ?? property.Name;
}
if (propertiesPresentInJson.Contains(jsonPropertyName)){
// Update the target property
var value = property.GetValue(loadedOptions);
var targetProperty = options.GetType().GetProperty(property.Name);
if (targetProperty != null && targetProperty.CanWrite){
targetProperty.SetValue(options, value);
}
}
}
}
private static HashSet<string> GetTopLevelPropertiesInJson(string jsonContent){
var properties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var reader = new JsonTextReader(new StringReader(jsonContent))){
while (reader.Read()){
if (reader.TokenType == JsonToken.PropertyName){
properties.Add(reader.Value?.ToString() ?? string.Empty);
}
}
}
return properties;
}
#region YAML OLD
public static void UpdateSettingsFromFileYAML(CrDownloadOptionsYaml options){
string dirPath = Path.GetDirectoryName(PathCrDownloadOptionsOld) ?? string.Empty;
if (!Directory.Exists(dirPath)){
Directory.CreateDirectory(dirPath);
}
if (!File.Exists(PathCrDownloadOptionsOld)){
using (var fileStream = File.Create(PathCrDownloadOptionsOld)){
}
return;
}
var input = File.ReadAllText(PathCrDownloadOptionsOld);
if (input.Length <= 0){
return;
@ -180,17 +209,18 @@ public class CfgManager{
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input);
var loadedOptions = deserializer.Deserialize<CrDownloadOptions>(new StringReader(input));
var loadedOptions = deserializer.Deserialize<CrDownloadOptionsYaml>(new StringReader(input));
var instanceOptions = options;
foreach (PropertyInfo property in typeof(CrDownloadOptions).GetProperties()){
foreach (PropertyInfo property in typeof(CrDownloadOptionsYaml).GetProperties()){
var yamlMemberAttribute = property.GetCustomAttribute<YamlMemberAttribute>();
string yamlPropertyName = yamlMemberAttribute?.Alias ?? property.Name;
// var jsonMemberAttribute = property.GetCustomAttribute<JsonPropertyAttribute>();
string yamlPropertyName = yamlMemberAttribute?.Alias ?? property.Name;
if (propertiesPresentInYaml.Contains(yamlPropertyName)){
PropertyInfo instanceProperty = instanceOptions.GetType().GetProperty(property.Name);
PropertyInfo? instanceProperty = instanceOptions.GetType().GetProperty(property.Name);
if (instanceProperty != null && instanceProperty.CanWrite){
instanceProperty.SetValue(instanceOptions, property.GetValue(loadedOptions));
}
@ -216,6 +246,9 @@ public class CfgManager{
return properties;
}
#endregion
public static void UpdateHistoryFile(){
if (!CrunchyrollManager.Instance.CrunOptions.History){
return;
@ -308,12 +341,32 @@ public class CfgManager{
return Directory.Exists(dirPath) && File.Exists(filePath);
}
public static T DeserializeFromFile<T>(string filePath){
var deserializer = new DeserializerBuilder()
.Build();
// public static T DeserializeFromFile<T>(string filePath){
// var deserializer = new DeserializerBuilder()
// .Build();
//
// using (var reader = new StreamReader(filePath)){
// return deserializer.Deserialize<T>(reader);
// }
// }
using (var reader = new StreamReader(filePath)){
return deserializer.Deserialize<T>(reader);
public static T? ReadJsonFromFile<T>(string pathToFile) where T : class{
try{
if (!File.Exists(pathToFile)){
throw new FileNotFoundException($"The file at path {pathToFile} does not exist.");
}
lock (fileLock){
using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read))
using (var streamReader = new StreamReader(fileStream))
using (var jsonReader = new JsonTextReader(streamReader)){
var serializer = new JsonSerializer();
return serializer.Deserialize<T>(jsonReader);
}
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred while reading the JSON file: {ex.Message}");
return null;
}
}
}

View file

@ -95,7 +95,7 @@ public class HlsDownloader{
// Check if the file exists and it is not a resume download
if (File.Exists(fn) && !_data.IsResume){
string rwts = _data.Override ?? "Y";
string rwts = !string.IsNullOrEmpty(_data.Override) ? _data.Override : "Y";
rwts = rwts.ToUpper(); // ?? "N"
if (rwts.StartsWith("Y")){
@ -304,7 +304,7 @@ public class HlsDownloader{
double downloadSpeed = downloadedBytes / (dateElapsed / 1000);
int partsLeft = partsTotal - partsDownloaded;
double remainingTime = (partsLeft * (totalDownloadedBytes / partsDownloaded)) / downloadSpeed;
double remainingTime = (partsLeft * ((double)totalDownloadedBytes / partsDownloaded)) / downloadSpeed;
return new Info{
Percent = percent,

View file

@ -15,8 +15,10 @@ using Avalonia.Media;
using Avalonia.Media.Imaging;
using CRD.Downloader;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
using CRD.Utils.JsonConv;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using Microsoft.Win32;
using Newtonsoft.Json;
@ -747,4 +749,95 @@ public class Helpers{
return false;
}
}
public static CrDownloadOptions MigrateSettings(CrDownloadOptionsYaml yaml){
if (yaml == null){
throw new ArgumentNullException(nameof(yaml));
}
return new CrDownloadOptions{
// General Settings
AutoDownload = yaml.AutoDownload,
RemoveFinishedDownload = yaml.RemoveFinishedDownload,
Timeout = yaml.Timeout,
FsRetryTime = yaml.FsRetryTime,
Force = yaml.Force,
SimultaneousDownloads = yaml.SimultaneousDownloads,
Theme = yaml.Theme,
AccentColor = yaml.AccentColor,
BackgroundImagePath = yaml.BackgroundImagePath,
BackgroundImageOpacity = yaml.BackgroundImageOpacity,
BackgroundImageBlurRadius = yaml.BackgroundImageBlurRadius,
Override = yaml.Override,
CcTag = yaml.CcTag,
Nocleanup = yaml.Nocleanup,
History = yaml.History,
HistoryIncludeCrArtists = yaml.HistoryIncludeCrArtists,
HistoryLang = yaml.HistoryLang,
HistoryAddSpecials = yaml.HistoryAddSpecials,
HistorySkipUnmonitored = yaml.HistorySkipUnmonitored,
HistoryCountSonarr = yaml.HistoryCountSonarr,
SonarrProperties = yaml.SonarrProperties,
LogMode = yaml.LogMode,
DownloadDirPath = yaml.DownloadDirPath,
DownloadTempDirPath = yaml.DownloadTempDirPath,
DownloadToTempFolder = yaml.DownloadToTempFolder,
HistoryPageProperties = yaml.HistoryPageProperties,
SeasonsPageProperties = yaml.SeasonsPageProperties,
DownloadSpeedLimit = yaml.DownloadSpeedLimit,
ProxyEnabled = yaml.ProxyEnabled,
ProxySocks = yaml.ProxySocks,
ProxyHost = yaml.ProxyHost,
ProxyPort = yaml.ProxyPort,
ProxyUsername = yaml.ProxyUsername,
ProxyPassword = yaml.ProxyPassword,
// Crunchyroll Settings
Hslang = yaml.Hslang,
Kstream = yaml.Kstream,
Novids = yaml.Novids,
Noaudio = yaml.Noaudio,
StreamServer = yaml.StreamServer,
QualityVideo = yaml.QualityVideo,
QualityAudio = yaml.QualityAudio,
FileName = yaml.FileName,
Numbers = yaml.Numbers,
Partsize = yaml.Partsize,
DlSubs = yaml.DlSubs,
SkipSubs = yaml.SkipSubs,
SkipSubsMux = yaml.SkipSubsMux,
SubsAddScaledBorder = yaml.SubsAddScaledBorder,
IncludeSignsSubs = yaml.IncludeSignsSubs,
SignsSubsAsForced = yaml.SignsSubsAsForced,
IncludeCcSubs = yaml.IncludeCcSubs,
CcSubsFont = yaml.CcSubsFont,
CcSubsMuxingFlag = yaml.CcSubsMuxingFlag,
Mp4 = yaml.Mp4,
VideoTitle = yaml.VideoTitle,
IncludeVideoDescription = yaml.IncludeVideoDescription,
DescriptionLang = yaml.DescriptionLang,
FfmpegOptions = yaml.FfmpegOptions,
MkvmergeOptions = yaml.MkvmergeOptions,
DefaultSub = yaml.DefaultSub,
DefaultSubSigns = yaml.DefaultSubSigns,
DefaultSubForcedDisplay = yaml.DefaultSubForcedDisplay,
DefaultAudio = yaml.DefaultAudio,
DlVideoOnce = yaml.DlVideoOnce,
KeepDubsSeperate = yaml.KeepDubsSeperate,
SkipMuxing = yaml.SkipMuxing,
SyncTiming = yaml.SyncTiming,
IsEncodeEnabled = yaml.IsEncodeEnabled,
EncodingPresetName = yaml.EncodingPresetName,
Chapters = yaml.Chapters,
DubLang = yaml.DubLang,
SelectedCalendarLanguage = yaml.SelectedCalendarLanguage,
CalendarDubFilter = yaml.CalendarDubFilter,
CustomCalendar = yaml.CustomCalendar,
CalendarHideDubs = yaml.CalendarHideDubs,
CalendarFilterByAirDate = yaml.CalendarFilterByAirDate,
CalendarShowUpcomingEpisodes = yaml.CalendarShowUpcomingEpisodes,
StreamEndpoint = yaml.StreamEndpoint,
SearchFetchFeaturedMusic = yaml.SearchFetchFeaturedMusic
};
}
}

View file

@ -249,26 +249,26 @@ public static class ApiUrls{
public static readonly string ApiBeta = "https://beta-api.crunchyroll.com";
public static readonly string ApiN = "https://www.crunchyroll.com";
public static readonly string Anilist = "https://graphql.anilist.co";
public static readonly string Auth = ApiN + "/auth/v1/token";
public static readonly string BetaAuth = ApiBeta + "/auth/v1/token";
public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile";
public static readonly string BetaCmsToken = ApiBeta + "/index/v2";
public static readonly string Search = ApiBeta + "/content/v2/discover/search";
public static readonly string Browse = ApiBeta + "/content/v2/discover/browse";
public static readonly string Cms = ApiBeta + "/content/v2/cms";
public static readonly string Content = ApiBeta + "/content/v2";
public static readonly string Profile = ApiN + "/accounts/v1/me/profile";
public static readonly string CmsToken = ApiN + "/index/v2";
public static readonly string Search = ApiN + "/content/v2/discover/search";
public static readonly string Browse = ApiN + "/content/v2/discover/browse";
public static readonly string Cms = ApiN + "/content/v2/cms";
public static readonly string Content = ApiN + "/content/v2";
public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse";
public static readonly string BetaCms = ApiBeta + "/cms/v2";
public static readonly string DRM = ApiBeta + "/drm/v1/auth";
public static readonly string Subscription = ApiBeta + "/subs/v3/subscriptions/";
public static readonly string CmsN = ApiN + "/content/v2/cms";
public static readonly string Subscription = ApiN + "/subs/v3/subscriptions/";
public static readonly string authBasic = "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6";
public static readonly string authBasicMob = "Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o=";
public static readonly string authBasicMob = "Basic ZG1yeWZlc2NkYm90dWJldW56NXo6NU45aThPV2cyVmtNcm1oekNfNUNXekRLOG55SXo0QU0=";
public static readonly string authBasicSwitch = "Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=";
public static readonly string ChromeUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36";
public static readonly string MobileUserAgent = "Crunchyroll/3.74.2 Android/14 okhttp/4.12.0";
}

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Utils.Files;
using CRD.Utils.Structs;
namespace CRD.Utils.Muxing;

View file

@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Files;
using CRD.Utils.Structs;
namespace CRD.Utils.Muxing;
@ -332,18 +333,25 @@ public class Merger{
Time = GetTimeFromFileName(fp, extractFramesCompareEnd.frameRate)
}).ToList();
// Calculate offsets
var startOffset = SyncingHelper.CalculateOffset(baseFramesStart, compareFramesStart);
var endOffset = SyncingHelper.CalculateOffset(baseFramesEnd, compareFramesEnd,true);
var lengthDiff = Math.Abs(baseVideoDurationTimeSpan.Value.TotalMicroseconds - compareVideoDurationTimeSpan.Value.TotalMicroseconds) / 1000000;
var lengthDiff = (baseVideoDurationTimeSpan.Value.TotalMicroseconds - compareVideoDurationTimeSpan.Value.TotalMicroseconds) / 1000000;
endOffset += lengthDiff;
Console.WriteLine($"Start offset: {startOffset} seconds");
Console.WriteLine($"End offset: {endOffset} seconds");
CleanupDirectory(cleanupDir);
baseFramesStart.Clear();
baseFramesEnd.Clear();
compareFramesStart.Clear();
compareFramesEnd.Clear();
var difference = Math.Abs(startOffset - endOffset);
@ -370,7 +378,7 @@ public class Merger{
private static double GetTimeFromFileName(string fileName, double frameRate){
var match = Regex.Match(Path.GetFileName(fileName), @"frame(\d+)");
if (match.Success){
return int.Parse(match.Groups[1].Value) / frameRate; // Assuming 30 fps
return int.Parse(match.Groups[1].Value) / frameRate;
}
return 0;

View file

@ -1,10 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
@ -16,7 +17,7 @@ namespace CRD.Utils.Muxing;
public class SyncingHelper{
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
var ffmpegPath = CfgManager.PathFFMPEG;
var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -fps_mode vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%03d.png\"";
var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -fps_mode vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%05d.png\"";
var output = "";
@ -68,7 +69,7 @@ public class SyncingHelper{
return 0;
}
private static double CalculateSSIM(float[] pixels1, float[] pixels2, int width, int height){
private static double CalculateSSIM(float[] pixels1, float[] pixels2){
double mean1 = pixels1.Average();
double mean2 = pixels2.Average();
@ -110,7 +111,7 @@ public class SyncingHelper{
return pixels;
}
public static double ComputeSSIM(string imagePath1, string imagePath2, int targetWidth, int targetHeight){
public static (double ssim, double pixelDiff) ComputeSSIM(string imagePath1, string imagePath2, int targetWidth, int targetHeight){
using (var image1 = Image.Load<Rgba32>(imagePath1))
using (var image2 = Image.Load<Rgba32>(imagePath2)){
// Preprocess images (resize and convert to grayscale)
@ -131,13 +132,24 @@ public class SyncingHelper{
// Check if any frame is completely black, if so, skip SSIM calculation
if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2)){
// Return a negative value or zero to indicate no SSIM comparison for black frames.
return -1.0;
return (-1.0,99);
}
// Compute SSIM
return CalculateSSIM(pixels1, pixels2, targetWidth, targetHeight);
return (CalculateSSIM(pixels1, pixels2),CalculatePixelDifference(pixels1,pixels2));
}
}
private static double CalculatePixelDifference(float[] pixels1, float[] pixels2){
double totalDifference = 0;
int count = pixels1.Length;
for (int i = 0; i < count; i++){
totalDifference += Math.Abs(pixels1[i] - pixels2[i]);
}
return totalDifference / count; // Average difference
}
private static bool IsBlackFrame(float[] pixels, float threshold = 1.0f){
// Check if all pixel values are below the threshold, indicating a black frame.
@ -145,10 +157,13 @@ public class SyncingHelper{
}
public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){
double ssim = ComputeSSIM(imagePath1, imagePath2, 256, 256);
var (ssim, pixelDiff) = ComputeSSIM(imagePath1, imagePath2, 256, 256);
// Console.WriteLine($"SSIM: {ssim}");
return ssim > ssimThreshold;
// Console.WriteLine(pixelDiff);
return ssim > ssimThreshold && pixelDiff < 10;
}
public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames,bool reverseCompare = false, double ssimThreshold = 0.9){
@ -160,7 +175,9 @@ public class SyncingHelper{
foreach (var baseFrame in baseFrames){
var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold));
if (matchingFrame != null){
Console.WriteLine($"Matched Frame: Base Frame Time: {baseFrame.Time}, Compare Frame Time: {matchingFrame.Time}");
Console.WriteLine($"Matched Frame:");
Console.WriteLine($"\t Base Frame Path: {baseFrame.FilePath} Time: {baseFrame.Time},");
Console.WriteLine($"\t Compare Frame Path: {matchingFrame.FilePath} Time: {matchingFrame.Time}");
return baseFrame.Time - matchingFrame.Time;
} else{
// Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");

View file

@ -87,7 +87,7 @@ public static class MPDParser{
throw new NotImplementedException();
}
var foundLanguage = Languages.FindLang(Languages.languages.FirstOrDefault(a => a.Code == item.language).CrLocale ?? "unknown");
var foundLanguage = Languages.FindLang(Languages.languages.FirstOrDefault(a => a.Code == item.language)?.CrLocale ?? "unknown");
LanguageItem? audioLang = item.language != null ? foundLanguage : (language != null ? language : foundLanguage);
var pItem = new AudioPlaylist{

View file

@ -4,13 +4,13 @@ using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public struct CrunchyChapters{
public class CrunchyChapters{
public List<CrunchyChapter> Chapters { get; set; }
public DateTime lastUpdate { get; set; }
public string? mediaId { get; set; }
}
public struct CrunchyChapter{
public class CrunchyChapter{
public string approverId { get; set; }
public string distributionNumber { get; set; }
public double? end { get; set; }
@ -22,7 +22,7 @@ public struct CrunchyChapter{
public string type { get; set; }
}
public struct CrunchyOldChapter{
public class CrunchyOldChapter{
public string media_id { get; set; }
public double startTime { get; set; }
public double endTime { get; set; }

View file

@ -1,13 +1,261 @@
using System.Collections.Generic;
using CRD.Utils.Sonarr;
using CRD.ViewModels;
using Newtonsoft.Json;
using YamlDotNet.Serialization;
namespace CRD.Utils.Structs;
namespace CRD.Utils.Structs.Crunchyroll;
public class CrDownloadOptions{
#region General Settings
[JsonProperty("auto_download")]
public bool AutoDownload{ get; set; }
[JsonProperty("remove_finished_downloads")]
public bool RemoveFinishedDownload{ get; set; }
[JsonIgnore]
public int Timeout{ get; set; }
[JsonIgnore]
public int FsRetryTime{ get; set; }
[JsonIgnore]
public string Force{ get; set; } = "";
[JsonProperty("simultaneous_downloads")]
public int SimultaneousDownloads{ get; set; }
[JsonProperty("theme")]
public string Theme{ get; set; } = "";
[JsonProperty("accent_color")]
public string? AccentColor{ get; set; }
[JsonProperty("background_image_path")]
public string? BackgroundImagePath{ get; set; }
[JsonProperty("background_image_opacity")]
public double BackgroundImageOpacity{ get; set; }
[JsonProperty("background_image_blur_radius")]
public double BackgroundImageBlurRadius{ get; set; }
[JsonIgnore]
public List<string> Override{ get; set; } =[];
[JsonIgnore]
public string CcTag{ get; set; } = "";
[JsonIgnore]
public bool Nocleanup{ get; set; }
[JsonProperty("history")]
public bool History{ get; set; }
[JsonProperty("history_include_cr_artists")]
public bool HistoryIncludeCrArtists{ get; set; }
[JsonProperty("history_lang")]
public string? HistoryLang{ get; set; }
[JsonProperty("history_add_specials")]
public bool HistoryAddSpecials{ get; set; }
[JsonProperty("history_skip_unmonitored")]
public bool HistorySkipUnmonitored{ get; set; }
[JsonProperty("history_count_sonarr")]
public bool HistoryCountSonarr{ get; set; }
[JsonProperty("sonarr_properties")]
public SonarrProperties? SonarrProperties{ get; set; }
[JsonProperty("log_mode")]
public bool LogMode{ get; set; }
[JsonProperty("download_dir_path")]
public string? DownloadDirPath{ get; set; }
[JsonProperty("download_temp_dir_path")]
public string? DownloadTempDirPath{ get; set; }
[JsonProperty("download_to_temp_folder")]
public bool DownloadToTempFolder{ get; set; }
[JsonProperty("history_page_properties")]
public HistoryPageProperties? HistoryPageProperties{ get; set; }
[JsonProperty("seasons_page_properties")]
public SeasonsPageProperties? SeasonsPageProperties{ get; set; }
[JsonProperty("download_speed_limit")]
public int DownloadSpeedLimit{ get; set; }
[JsonProperty("proxy_enabled")]
public bool ProxyEnabled{ get; set; }
[JsonProperty("proxy_socks")]
public bool ProxySocks{ get; set; }
[JsonProperty("proxy_host")]
public string? ProxyHost{ get; set; }
[JsonProperty("proxy_port")]
public int ProxyPort{ get; set; }
[JsonProperty("proxy_username")]
public string? ProxyUsername{ get; set; }
[JsonProperty("proxy_password")]
public string? ProxyPassword{ get; set; }
#endregion
#region Crunchyroll Settings
[JsonProperty("hard_sub_lang")]
public string Hslang{ get; set; } = "";
[JsonIgnore]
public int Kstream{ get; set; }
[JsonProperty("no_video")]
public bool Novids{ get; set; }
[JsonProperty("no_audio")]
public bool Noaudio{ get; set; }
[JsonIgnore]
public int StreamServer{ get; set; }
[JsonProperty("quality_video")]
public string QualityVideo{ get; set; } = "";
[JsonProperty("quality_audio")]
public string QualityAudio{ get; set; } = "";
[JsonProperty("file_name")]
public string FileName{ get; set; } = "";
[JsonProperty("leading_numbers")]
public int Numbers{ get; set; }
[JsonProperty("download_part_size")]
public int Partsize{ get; set; }
[JsonProperty("soft_subs")]
public List<string> DlSubs{ get; set; } =[];
[JsonIgnore]
public bool SkipSubs{ get; set; }
[JsonProperty("mux_skip_subs")]
public bool SkipSubsMux{ get; set; }
[JsonProperty("subs_add_scaled_border")]
public ScaledBorderAndShadowSelection SubsAddScaledBorder{ get; set; }
[JsonProperty("include_signs_subs")]
public bool IncludeSignsSubs{ get; set; }
[JsonProperty("mux_signs_subs_flag")]
public bool SignsSubsAsForced{ get; set; }
[JsonProperty("include_cc_subs")]
public bool IncludeCcSubs{ get; set; }
[JsonProperty("cc_subs_font")]
public string? CcSubsFont{ get; set; }
[JsonProperty("mux_cc_subs_flag")]
public bool CcSubsMuxingFlag{ get; set; }
[JsonProperty("mux_mp4")]
public bool Mp4{ get; set; }
[JsonProperty("mux_video_title")]
public string? VideoTitle{ get; set; }
[JsonProperty("mux_video_description")]
public bool IncludeVideoDescription{ get; set; }
[JsonProperty("mux_description_lang")]
public string? DescriptionLang{ get; set; }
[JsonProperty("mux_ffmpeg")]
public List<string> FfmpegOptions{ get; set; } =[];
[JsonProperty("mux_mkvmerge")]
public List<string> MkvmergeOptions{ get; set; } =[];
[JsonProperty("mux_default_sub")]
public string DefaultSub{ get; set; } = "";
[JsonProperty("mux_default_sub_signs")]
public bool DefaultSubSigns{ get; set; }
[JsonProperty("mux_default_sub_forced_display")]
public bool DefaultSubForcedDisplay{ get; set; }
[JsonProperty("mux_default_dub")]
public string DefaultAudio{ get; set; } = "";
[JsonProperty("dl_video_once")]
public bool DlVideoOnce{ get; set; }
[JsonProperty("keep_dubs_seperate")]
public bool KeepDubsSeperate{ get; set; }
[JsonProperty("mux_skip_muxing")]
public bool SkipMuxing{ get; set; }
[JsonProperty("mux_sync_dubs")]
public bool SyncTiming{ get; set; }
[JsonProperty("encode_enabled")]
public bool IsEncodeEnabled{ get; set; }
[JsonProperty("encode_preset")]
public string? EncodingPresetName{ get; set; }
[JsonProperty("chapters")]
public bool Chapters{ get; set; }
[JsonProperty("dub_lang")]
public List<string> DubLang{ get; set; } =[];
[JsonProperty("calendar_language")]
public string? SelectedCalendarLanguage{ get; set; }
[JsonProperty("calendar_dub_filter")]
public string? CalendarDubFilter{ get; set; }
[JsonProperty("calendar_custom")]
public bool CustomCalendar{ get; set; }
[JsonProperty("calendar_hide_dubs")]
public bool CalendarHideDubs{ get; set; }
[JsonProperty("calendar_filter_by_air_date")]
public bool CalendarFilterByAirDate{ get; set; }
[JsonProperty("calendar_show_upcoming_episodes")]
public bool CalendarShowUpcomingEpisodes{ get; set; }
[JsonProperty("stream_endpoint")]
public string? StreamEndpoint{ get; set; }
[JsonProperty("search_fetch_featured_music")]
public bool SearchFetchFeaturedMusic{ get; set; }
#endregion
}
public class CrDownloadOptionsYaml{
#region General Settings
[YamlMember(Alias = "auto_download", ApplyNamingConventions = false)]
public bool AutoDownload{ get; set; }
@ -21,13 +269,13 @@ public class CrDownloadOptions{
public int FsRetryTime{ get; set; }
[YamlIgnore]
public string Force{ get; set; }
public string Force{ get; set; } = "";
[YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)]
public int SimultaneousDownloads{ get; set; }
[YamlMember(Alias = "theme", ApplyNamingConventions = false)]
public string Theme{ get; set; }
public string Theme{ get; set; } = "";
[YamlMember(Alias = "accent_color", ApplyNamingConventions = false)]
public string? AccentColor{ get; set; }
@ -43,23 +291,29 @@ public class CrDownloadOptions{
[YamlIgnore]
public List<string> Override{ get; set; }
public List<string> Override{ get; } =[];
[YamlIgnore]
public string CcTag{ get; set; }
public string CcTag{ get; set; } = "";
[YamlIgnore]
public bool Nocleanup{ get; set; }
public bool Nocleanup{ get; } = false;
[YamlMember(Alias = "history", ApplyNamingConventions = false)]
public bool History{ get; set; }
[YamlMember(Alias = "history_include_cr_artists", ApplyNamingConventions = false)]
public bool HistoryIncludeCrArtists{ get; set; }
[YamlMember(Alias = "history_lang", ApplyNamingConventions = false)]
public string? HistoryLang{ get; set; }
[YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)]
public bool HistoryAddSpecials{ get; set; }
[YamlMember(Alias = "history_skip_unmonitored", ApplyNamingConventions = false)]
public bool HistorySkipUnmonitored{ get; set; }
[YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)]
public bool HistoryCountSonarr{ get; set; }
@ -80,7 +334,7 @@ public class CrDownloadOptions{
[YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
public HistoryPageProperties? HistoryPageProperties{ get; set; }
[YamlMember(Alias = "seasons_page_properties", ApplyNamingConventions = false)]
public SeasonsPageProperties? SeasonsPageProperties{ get; set; }
@ -89,7 +343,7 @@ public class CrDownloadOptions{
[YamlMember(Alias = "proxy_enabled", ApplyNamingConventions = false)]
public bool ProxyEnabled{ get; set; }
[YamlMember(Alias = "proxy_socks", ApplyNamingConventions = false)]
public bool ProxySocks{ get; set; }
@ -98,20 +352,20 @@ public class CrDownloadOptions{
[YamlMember(Alias = "proxy_port", ApplyNamingConventions = false)]
public int ProxyPort{ get; set; }
[YamlMember(Alias = "proxy_username", ApplyNamingConventions = false)]
public string? ProxyUsername{ get; set; }
[YamlMember(Alias = "proxy_password", ApplyNamingConventions = false)]
public string? ProxyPassword{ get; set; }
#endregion
#region Crunchyroll Settings
[YamlMember(Alias = "hard_sub_lang", ApplyNamingConventions = false)]
public string Hslang{ get; set; }
public string Hslang{ get; set; } = "";
[YamlIgnore]
public int Kstream{ get; set; }
@ -126,23 +380,23 @@ public class CrDownloadOptions{
public int StreamServer{ get; set; }
[YamlMember(Alias = "quality_video", ApplyNamingConventions = false)]
public string QualityVideo{ get; set; }
public string QualityVideo{ get; set; } = "";
[YamlMember(Alias = "quality_audio", ApplyNamingConventions = false)]
public string QualityAudio{ get; set; }
public string QualityAudio{ get; set; } = "";
[YamlMember(Alias = "file_name", ApplyNamingConventions = false)]
public string FileName{ get; set; }
public string FileName{ get; set; } = "";
[YamlMember(Alias = "leading_numbers", ApplyNamingConventions = false)]
public int Numbers{ get; set; }
[YamlIgnore]
[YamlMember(Alias = "download_part_size", ApplyNamingConventions = false)]
public int Partsize{ get; set; }
[YamlMember(Alias = "soft_subs", ApplyNamingConventions = false)]
public List<string> DlSubs{ get; set; }
public List<string> DlSubs{ get; set; } =[];
[YamlIgnore]
public bool SkipSubs{ get; set; }
@ -181,13 +435,13 @@ public class CrDownloadOptions{
public string? DescriptionLang{ get; set; }
[YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)]
public List<string> FfmpegOptions{ get; set; }
public List<string> FfmpegOptions{ get; set; } =[];
[YamlMember(Alias = "mux_mkvmerge", ApplyNamingConventions = false)]
public List<string> MkvmergeOptions{ get; set; }
public List<string> MkvmergeOptions{ get; set; } =[];
[YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)]
public string DefaultSub{ get; set; }
public string DefaultSub{ get; set; } = "";
[YamlMember(Alias = "mux_default_sub_signs", ApplyNamingConventions = false)]
public bool DefaultSubSigns{ get; set; }
@ -196,7 +450,7 @@ public class CrDownloadOptions{
public bool DefaultSubForcedDisplay{ get; set; }
[YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)]
public string DefaultAudio{ get; set; }
public string DefaultAudio{ get; set; } = "";
[YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)]
public bool DlVideoOnce{ get; set; }
@ -220,8 +474,7 @@ public class CrDownloadOptions{
public bool Chapters{ get; set; }
[YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)]
public List<string> DubLang{ get; set; }
public List<string> DubLang{ get; set; } =[];
[YamlMember(Alias = "calendar_language", ApplyNamingConventions = false)]
public string? SelectedCalendarLanguage{ get; set; }
@ -237,12 +490,15 @@ public class CrDownloadOptions{
[YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)]
public bool CalendarFilterByAirDate{ get; set; }
[YamlMember(Alias = "calendar_show_upcoming_episodes", ApplyNamingConventions = false)]
public bool CalendarShowUpcomingEpisodes{ get; set; }
[YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)]
public string? StreamEndpoint{ get; set; }
[YamlMember(Alias = "search_fetch_featured_music", ApplyNamingConventions = false)]
public bool SearchFetchFeaturedMusic{ get; set; }
#endregion
}

View file

@ -4,7 +4,7 @@ using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public struct CrunchyMovieList{
public class CrunchyMovieList{
public int Total{ get; set; }
public List<CrunchyMovie>? Data{ get; set; }
public Meta Meta{ get; set; }

View file

@ -185,7 +185,7 @@ public class CrBrowseEpisodeMetaData{
}
public struct CrBrowseEpisodeVersion{
public class CrBrowseEpisodeVersion{
[JsonProperty("audio_locale")]
public Locale? AudioLocale{ get; set; }

View file

@ -1,16 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using CRD.Utils.Structs.History;
using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public struct CrunchyEpisodeList{
public class CrunchyEpisodeList{
public int Total{ get; set; }
public List<CrunchyEpisode>? Data{ get; set; }
public Meta Meta{ get; set; }
}
public struct CrunchyEpisode{
public class CrunchyEpisode : IHistorySource{
[JsonProperty("next_episode_id")]
public string NextEpisodeId{ get; set; }
@ -81,7 +84,7 @@ public struct CrunchyEpisode{
[JsonProperty("audio_locale")]
public string AudioLocale{ get; set; }
public string Id{ get; set; }
public required string Id{ get; set; }
[JsonProperty("media_type")]
public string? MediaType{ get; set; }
@ -173,9 +176,123 @@ public struct CrunchyEpisode{
[JsonProperty("__links__")]
public Links? Links{ get; set; }
[JsonIgnore]
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Episode;
#region Interface
public string GetSeriesId(){
return SeriesId;
}
public string GetSeriesTitle(){
return SeriesTitle;
}
public string GetSeasonTitle(){
return SeasonTitle;
}
public string GetSeasonNum(){
return Helpers.ExtractNumberAfterS(Identifier) ?? SeasonNumber + "";
}
public string GetSeasonId(){
return SeasonId;
}
public string GetEpisodeId(){
return Id;
}
public string GetEpisodeNumber(){
return Episode ?? "";
}
public string GetEpisodeTitle(){
if (Identifier.Contains("|M|")){
if (string.IsNullOrEmpty(Title)){
if (SeasonTitle.StartsWith(SeriesTitle)){
var splitTitle = SeasonTitle.Split(new[]{ SeriesTitle }, StringSplitOptions.None);
var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0];
var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", "");
return cleanedTitle;
}
return SeasonTitle;
}
if (Title.StartsWith(SeriesTitle)){
var splitTitle = Title.Split(new[]{ SeriesTitle }, StringSplitOptions.None);
var titlePart = splitTitle.Length > 1 ? splitTitle[1] : splitTitle[0];
var cleanedTitle = Regex.Replace(titlePart, @"^[^a-zA-Z]+", "");
return cleanedTitle;
}
return Title;
}
return Title;
}
public string GetEpisodeDescription(){
return Description;
}
public bool IsSpecialSeason(){
if (string.IsNullOrEmpty(Identifier)){
return false;
}
// does NOT contain "|S" followed by one or more digits immediately after
string pattern = @"^(?!.*\|S\d+).*";
return Regex.IsMatch(Identifier, pattern);
}
public bool IsSpecialEpisode(){
return !int.TryParse(Episode, out _);
}
public List<string> GetAnimeIds(){
return[];
}
public List<string> GetEpisodeAvailableDubLang(){
var langList = new List<string>();
if (Versions != null){
langList.AddRange(Versions.Select(version => version.AudioLocale));
} else{
langList.Add(AudioLocale);
}
return Languages.SortListByLangList(langList);
}
public List<string> GetEpisodeAvailableSoftSubs(){
return Languages.SortListByLangList(SubtitleLocales);
}
public DateTime GetAvailableDate(){
return PremiumAvailableDate;
}
public SeriesType GetSeriesType(){
return SeriesType.Series;
}
public EpisodeType GetEpisodeType(){
return EpisodeType;
}
#endregion
}
public struct Images{
public class Images{
[JsonProperty("poster_tall")]
public List<List<Image>>? PosterTall{ get; set; }
@ -188,14 +305,14 @@ public struct Images{
public List<List<Image>>? Thumbnail{ get; set; }
}
public struct Image{
public class Image{
public int Height{ get; set; }
public string Source{ get; set; }
public ImageType Type{ get; set; }
public int Width{ get; set; }
}
public struct EpisodeVersion{
public class EpisodeVersion{
[JsonProperty("audio_locale")]
public string AudioLocale{ get; set; }
@ -215,11 +332,11 @@ public struct EpisodeVersion{
public string Variant{ get; set; }
}
public struct Link{
public class Link{
public string Href{ get; set; }
}
public struct Links(){
public class Links(){
public Dictionary<string, Link> LinkMappings{ get; set; } = new(){
{ "episode/channel", default },
{ "episode/next_episode", default },
@ -230,7 +347,7 @@ public struct Links(){
}
public class CrunchyEpMeta{
public List<CrunchyEpMetaData>? Data{ get; set; }
public List<CrunchyEpMetaData> Data{ get; set; } =[];
public string? SeriesTitle{ get; set; }
public string? SeasonTitle{ get; set; }
@ -239,11 +356,11 @@ public class CrunchyEpMeta{
public string? Description{ get; set; }
public string? SeasonId{ get; set; }
public string? Season{ get; set; }
public string? ShowId{ get; set; }
public string? SeriesId{ get; set; }
public string? AbsolutEpisodeNumberE{ get; set; }
public string? Image{ get; set; }
public bool Paused{ get; set; }
public DownloadProgress? DownloadProgress{ get; set; }
public DownloadProgress DownloadProgress{ get; set; } = new();
public List<string>? SelectedDubs{ get; set; }
@ -259,7 +376,7 @@ public class CrunchyEpMeta{
public string Resolution{ get; set; }
public List<string> downloadedFiles{ get; set; } =[];
public bool OnlySubs{ get; set; }
}
@ -274,7 +391,7 @@ public class DownloadProgress{
public double DownloadSpeed{ get; set; }
}
public struct CrunchyEpMetaData{
public class CrunchyEpMetaData{
public string MediaId{ get; set; }
public LanguageItem? Lang{ get; set; }
public string? Playback{ get; set; }
@ -283,7 +400,7 @@ public struct CrunchyEpMetaData{
public bool IsDubbed{ get; set; }
}
public struct CrunchyRollEpisodeData{
public class CrunchyRollEpisodeData{
public string Key{ get; set; }
public EpisodeAndLanguage EpisodeAndLanguages{ get; set; }
}

View file

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.Crunchyroll.Music;
public class CrunchyArtistList{
public int Total{ get; set; }
public List<CrArtist> Data{ get; set; } =[];
public Meta? Meta{ get; set; }
}
public class CrArtist{
[JsonProperty("description")]
public string? Description{ get; set; }
[JsonProperty("name")]
public string? Name{ get; set; }
[JsonProperty("slug")]
public string? Slug{ get; set; }
[JsonProperty("type")]
public string? Type{ get; set; }
[JsonProperty("id")]
public string? Id{ get; set; }
[JsonProperty("publishDate")]
public DateTime? PublishDate{ get; set; }
public MusicImages Images{ get; set; } = new();
}

View file

@ -1,132 +1,207 @@
using System;
using System.Collections.Generic;
using CRD.Utils.Structs.History;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.Crunchyroll.Music;
public struct CrunchyMusicVideoList{
public class CrunchyMusicVideoList{
public int Total{ get; set; }
public List<CrunchyMusicVideo>? Data{ get; set; }
public Meta Meta{ get; set; }
public List<CrunchyMusicVideo> Data{ get; set; } =[];
public Meta? Meta{ get; set; }
}
public class CrunchyMusicVideo{
public class CrunchyMusicVideo : IHistorySource{
[JsonProperty("copyright")]
public string? Copyright{ get; set; }
[JsonProperty("hash")]
public string? Hash{ get; set; }
[JsonProperty("availability")]
public MusicVideoAvailability? Availability{ get; set; }
[JsonProperty("isMature")]
public bool IsMature{ get; set; }
[JsonProperty("maturityRatings")]
public object? MaturityRatings{ get; set; }
[JsonProperty("title")]
public string? Title{ get; set; }
[JsonProperty("artists")]
public object? Artists{ get; set; }
[JsonProperty("displayArtistNameRequired")]
public bool DisplayArtistNameRequired{ get; set; }
[JsonProperty("streams_link")]
public string? StreamsLink{ get; set; }
[JsonProperty("matureBlocked")]
public bool MatureBlocked{ get; set; }
[JsonProperty("originalRelease")]
public DateTime OriginalRelease{ get; set; }
[JsonProperty("sequenceNumber")]
public int SequenceNumber{ get; set; }
[JsonProperty("type")]
public string? Type{ get; set; }
[JsonProperty("animeIds")]
public List<string>? AnimeIds{ get; set; }
[JsonProperty("description")]
public string? Description{ get; set; }
[JsonProperty("durationMs")]
public int DurationMs{ get; set; }
[JsonProperty("licensor")]
public string? Licensor{ get; set; }
[JsonProperty("slug")]
public string? Slug{ get; set; }
[JsonProperty("artist")]
public MusicVideoArtist? Artist{ get; set; }
public required MusicVideoArtist Artist{ get; set; }
[JsonProperty("isPremiumOnly")]
public bool IsPremiumOnly{ get; set; }
[JsonProperty("isPublic")]
public bool IsPublic{ get; set; }
[JsonProperty("publishDate")]
public DateTime PublishDate{ get; set; }
[JsonProperty("displayArtistName")]
public string? DisplayArtistName{ get; set; }
[JsonProperty("genres")]
public object? genres{ get; set; }
public object? Genres{ get; set; }
[JsonProperty("readyToPublish")]
public bool ReadyToPublish{ get; set; }
[JsonProperty("id")]
public string? Id{ get; set; }
public required string Id{ get; set; }
[JsonProperty("createdAt")]
public DateTime CreatedAt{ get; set; }
public MusicImages? Images{ get; set; }
[JsonProperty("updatedAt")]
public DateTime UpdatedAt{ get; set; }
[JsonIgnore]
public EpisodeType EpisodeType{ get; set; } = EpisodeType.MusicVideo;
#region Interface
public string GetSeriesId(){
return Artist.Id;
}
public string GetSeriesTitle(){
return Artist.Name;
}
public string GetSeasonTitle(){
return EpisodeType == EpisodeType.MusicVideo ? "Music Videos" : "Concerts";
}
public string GetSeasonNum(){
return EpisodeType == EpisodeType.MusicVideo ? "1" : "2";
}
public string GetSeasonId(){
return EpisodeType == EpisodeType.MusicVideo ? "MusicVideos" : "Concerts";
}
public string GetEpisodeId(){
return Id;
}
public string GetEpisodeNumber(){
return SequenceNumber + "";
}
public string GetEpisodeTitle(){
return Title ?? "";
}
public string GetEpisodeDescription(){
return Description ?? "";
}
public bool IsSpecialSeason(){
return false;
}
public bool IsSpecialEpisode(){
return false;
}
public List<string> GetAnimeIds(){
return AnimeIds ?? [];
}
public List<string> GetEpisodeAvailableDubLang(){
return[];
}
public List<string> GetEpisodeAvailableSoftSubs(){
return[];
}
public DateTime GetAvailableDate(){
return PublishDate;
}
public SeriesType GetSeriesType(){
return SeriesType.Artist;
}
public EpisodeType GetEpisodeType(){
return EpisodeType;
}
#endregion
}
public struct MusicImages{
public class MusicImages{
[JsonProperty("poster_tall")]
public List<Image>? PosterTall{ get; set; }
public List<Image> PosterTall{ get; set; } =[];
[JsonProperty("poster_wide")]
public List<Image>? PosterWide{ get; set; }
public List<Image> PosterWide{ get; set; } =[];
[JsonProperty("promo_image")]
public List<Image>? PromoImage{ get; set; }
public List<Image> PromoImage{ get; set; } =[];
public List<Image>? Thumbnail{ get; set; }
public List<Image> Thumbnail{ get; set; } =[];
}
public struct MusicVideoArtist{
public class MusicVideoArtist{
[JsonProperty("id")]
public string? Id{ get; set; }
public required string Id{ get; set; }
[JsonProperty("name")]
public string? Name{ get; set; }
public required string Name{ get; set; }
[JsonProperty("slug")]
public string? Slug{ get; set; }
}
public struct MusicVideoAvailability{
public class MusicVideoAvailability{
[JsonProperty("endDate")]
public DateTime EndDate{ get; set; }
[JsonProperty("startDate")]
public DateTime StartDate{ get; set; }
}

View file

@ -9,7 +9,7 @@ public class CrSeriesBase{
public Meta Meta{ get; set; }
}
public struct SeriesBaseItem{
public class SeriesBaseItem{
[JsonProperty("extended_maturity_rating")]
public Dictionary<object, object>
ExtendedMaturityRating{ get; set; }

View file

@ -9,7 +9,7 @@ public class CrSeriesSearch{
public Meta Meta{ get; set; }
}
public struct SeriesSearchItem{
public class SeriesSearchItem{
public string Description{ get; set; }
[JsonProperty("seo_description")]
@ -91,7 +91,7 @@ public struct SeriesSearchItem{
public string SeoTitle{ get; set; }
}
public struct Version{
public class Version{
[JsonProperty("audio_locale")]
public string? AudioLocale{ get; set; }

View file

@ -6,7 +6,7 @@ using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public struct AuthData{
public class AuthData{
public string Username{ get; set; }
public string Password{ get; set; }
}
@ -18,12 +18,12 @@ public class DrmAuthData{
public string? Token{ get; set; }
}
public struct Meta{
public class Meta{
[JsonProperty("versions_considered")]
public bool? VersionsConsidered{ get; set; }
}
public struct LanguageItem{
public class LanguageItem{
[JsonProperty("cr_locale")]
public string CrLocale{ get; set; }
@ -33,12 +33,12 @@ public struct LanguageItem{
public string Language{ get; set; }
}
public struct EpisodeAndLanguage{
public class EpisodeAndLanguage{
public List<CrunchyEpisode> Items{ get; set; }
public List<LanguageItem> Langs{ get; set; }
}
public struct CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool? but = null, List<string>? e = null, string? s = null){
public class CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool? but = null, List<string>? e = null, string? s = null){
public List<string> DubLang{ get; set; } = dubLang; //lang code
public bool? AllEpisodes{ get; set; } = all; // download all episodes
public bool? But{ get; set; } = but; //download all except selected episodes
@ -46,12 +46,12 @@ public struct CrunchyMultiDownload(List<string> dubLang, bool? all = null, bool?
public string? S{ get; set; } = s; //season id
}
public struct CrunchySeriesList{
public class CrunchySeriesList{
public List<Episode> List{ get; set; }
public Dictionary<string, EpisodeAndLanguage> Data{ get; set; }
}
public struct Episode{
public class Episode{
public string E{ get; set; }
public List<string> Lang{ get; set; }
public string Name{ get; set; }
@ -63,9 +63,11 @@ public struct Episode{
public string Img{ get; set; }
public string Description{ get; set; }
public string Time{ get; set; }
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown;
}
public struct DownloadResponse{
public class DownloadResponse{
public List<DownloadedMedia> Data{ get; set; }
public string? FileName{ get; set; }

View file

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Files;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
@ -32,12 +33,18 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_special_episode")]
public bool SpecialEpisode{ get; set; }
[JsonProperty("episode_type")]
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown;
[JsonProperty("sonarr_episode_id")]
public string? SonarrEpisodeId{ get; set; }
[JsonProperty("sonarr_has_file")]
public bool SonarrHasFile{ get; set; }
[JsonProperty("sonarr_is_monitored")]
public bool SonarrIsMonitored{ get; set; }
[JsonProperty("sonarr_episode_number")]
public string? SonarrEpisodeNumber{ get; set; }
@ -77,8 +84,22 @@ public class HistoryEpisode : INotifyPropertyChanged{
}
public async Task DownloadEpisode(bool onlySubs = false){
await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId,
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang,
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
switch (EpisodeType){
case EpisodeType.MusicVideo:
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty);
break;
case EpisodeType.Concert:
await QueueManager.Instance.CrAddConcertToQueue(EpisodeId ?? string.Empty);
break;
case EpisodeType.Episode:
case EpisodeType.Unknown:
default:
await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId ?? string.Empty,
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang,
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
break;
}
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using CRD.Utils.Files;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@ -8,11 +8,18 @@ using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.CustomList;
using CRD.Utils.Files;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_streaming_service")]
public StreamingService SeriesStreamingService{ get; set; } = StreamingService.Unknown;
[JsonProperty("series_type")]
public SeriesType SeriesType{ get; set; } = SeriesType.Unknown;
[JsonProperty("series_title")]
public string? SeriesTitle{ get; set; }
@ -96,7 +103,7 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonIgnore]
private bool Loading = false;
[JsonIgnore]
public StringItem? _selectedVideoQualityItem;
@ -111,7 +118,6 @@ public class HistorySeries : INotifyPropertyChanged{
if (!Loading){
CfgManager.UpdateHistoryFile();
}
}
}
@ -230,16 +236,48 @@ public class HistorySeries : INotifyPropertyChanged{
int count = 0;
bool foundWatched = false;
var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials;
var sonarrEnabled = CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled &&
!string.IsNullOrEmpty(SonarrSeriesId);
if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr && !string.IsNullOrEmpty(SonarrSeriesId)){
var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored;
if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
if (season.SpecialSeason == true){
if (historyAddSpecials){
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){
count++;
}
}
}
continue;
}
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
continue;
}
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.SonarrHasFile){
count++;
}
continue;
}
if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){
count++;
}
@ -253,6 +291,10 @@ public class HistorySeries : INotifyPropertyChanged{
if (historyAddSpecials){
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (sonarrEnabled && sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!episodes[j].WasDownloaded){
count++;
}
@ -266,6 +308,10 @@ public class HistorySeries : INotifyPropertyChanged{
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
continue;
}
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){
count++;
@ -303,47 +349,103 @@ public class HistorySeries : INotifyPropertyChanged{
public async Task AddNewMissingToDownloads(){
bool foundWatched = false;
var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials;
var sonarrEnabled = SeriesType != SeriesType.Artist && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled &&
!string.IsNullOrEmpty(SonarrSeriesId);
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
var sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored;
if (season.SpecialSeason == true){
if (historyAddSpecials){
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (!episodes[j].WasDownloaded){
await Seasons[i].EpisodesList[j].DownloadEpisode();
if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
if (season.SpecialSeason == true){
if (historyAddSpecials){
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!string.IsNullOrEmpty(episodes[j].SonarrEpisodeId) && !episodes[j].SonarrHasFile){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
}
}
}
continue;
}
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
continue;
}
if (!episode.WasDownloaded && !foundWatched){
await Seasons[i].EpisodesList[j].DownloadEpisode();
} else{
foundWatched = true;
if (!historyAddSpecials){
break;
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
continue;
}
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.SonarrHasFile){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
continue;
}
if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
}
}
} else{
for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[i];
if (foundWatched && !historyAddSpecials){
break;
if (season.SpecialSeason == true){
if (historyAddSpecials){
var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){
if (sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!episodes[j].WasDownloaded){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
}
}
continue;
}
var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j];
if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
continue;
}
if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){
await Seasons[i].EpisodesList[j].DownloadEpisode();
}
continue;
}
if (!episode.WasDownloaded && !foundWatched){
await Seasons[i].EpisodesList[j].DownloadEpisode();
} else{
foundWatched = true;
if (!historyAddSpecials){
break;
}
}
}
if (foundWatched && !historyAddSpecials){
break;
}
}
}
}
@ -352,13 +454,32 @@ public class HistorySeries : INotifyPropertyChanged{
Console.WriteLine($"Fetching Data for: {SeriesTitle}");
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
try{
await CrunchyrollManager.Instance.History.CRUpdateSeries(SeriesId, seasonId);
} catch (Exception e){
Console.Error.WriteLine("Failed to update History series");
Console.Error.WriteLine(e);
switch (SeriesType){
case SeriesType.Artist:
try{
await CrunchyrollManager.Instance.CrMusic.ParseArtistVideosByIdAsync(SeriesId,
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, true, true);
} catch (Exception e){
Console.Error.WriteLine("Failed to update History artist");
Console.Error.WriteLine(e);
}
break;
case SeriesType.Series:
case SeriesType.Unknown:
default:
try{
await CrunchyrollManager.Instance.History.CrUpdateSeries(SeriesId, seasonId);
} catch (Exception e){
Console.Error.WriteLine("Failed to update History series");
Console.Error.WriteLine(e);
}
break;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesTitle)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesDescription)));
UpdateNewEpisodes();
@ -384,6 +505,15 @@ public class HistorySeries : INotifyPropertyChanged{
}
public void OpenCrPage(){
Helpers.OpenUrl($"https://www.crunchyroll.com/series/{SeriesId}");
switch (SeriesType){
case SeriesType.Artist:
Helpers.OpenUrl($"https://www.crunchyroll.com/artist/{SeriesId}");
break;
case SeriesType.Series:
case SeriesType.Unknown:
default:
Helpers.OpenUrl($"https://www.crunchyroll.com/series/{SeriesId}");
break;
}
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace CRD.Utils.Structs.History;
public interface IHistorySource{
string GetSeriesId();
string GetSeriesTitle();
string GetSeasonTitle();
string GetSeasonNum();
string GetSeasonId();
string GetEpisodeId();
string GetEpisodeNumber();
string GetEpisodeTitle();
string GetEpisodeDescription();
bool IsSpecialSeason();
bool IsSpecialEpisode();
List<string> GetAnimeIds();
List<string> GetEpisodeAvailableDubLang();
List<string> GetEpisodeAvailableSoftSubs();
DateTime GetAvailableDate();
SeriesType GetSeriesType();
EpisodeType GetEpisodeType();
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace CRD.Utils.Structs.History;
public class SeriesDataCache{
public string SeriesId{ get; set; } = "";
public string SeriesTitle{ get; set; } = "";
public string SeriesDescription{ get; set; } = "";
public string ThumbnailImageUrl{ get; set; } = "";
public List<string> HistorySeriesAvailableDubLang{ get; set; } =[];
public List<string> HistorySeriesAvailableSoftSubs{ get; set; } =[];
}

View file

@ -54,7 +54,7 @@ public class Languages{
else if (yExists)
return 1; // y comes before any missing value
else
return string.Compare(x, y); // Sort alphabetically or by another logic for missing values
return string.CompareOrdinal(x, y); // Sort alphabetically or by another logic for missing values
});
return langList;
@ -116,8 +116,8 @@ public class Languages{
}
public static LanguageItem FindLang(string crLocale){
LanguageItem lang = languages.FirstOrDefault(l => l.CrLocale == crLocale);
if (lang.CrLocale != null){
LanguageItem? lang = languages.FirstOrDefault(l => l.CrLocale == crLocale);
if (lang?.CrLocale != null){
return lang;
} else{
return new LanguageItem{
@ -159,7 +159,7 @@ public class Languages{
var property = typeof(T).GetProperty(sortKey, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (property == null) throw new ArgumentException($"Property '{sortKey}' not found on type '{typeof(T).Name}'.");
var value = property.GetValue(item) as string;
var value = property.GetValue(item) as string ?? string.Empty;
int index = idx.ContainsKey(value) ? idx[value] : 50;
return index;
}).ToList();

View file

@ -0,0 +1,26 @@
using System;
using System.Collections;
using System.Globalization;
using Avalonia.Data.Converters;
namespace CRD.Utils.UI;
public class UiListHasElementsConverter : IValueConverter{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture){
if (value is IEnumerable enumerable){
// Check if the collection has any elements
foreach (var _ in enumerable){
return true; // At least one element exists
}
return false; // No elements
}
// Return false if the input is not a collection or is null
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){
throw new NotSupportedException("ListToBooleanConverter does not support ConvertBack.");
}
}

View file

@ -9,6 +9,7 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CRD.Utils.Files;
namespace CRD.Utils.Updater;

View file

@ -29,19 +29,19 @@ public partial class AccountPageViewModel : ViewModelBase{
private static DispatcherTimer? _timer;
private DateTime _targetTime;
private bool IsCancelled = false;
private bool UnknownEndDate = false;
private bool EndedButMaybeActive = false;
private bool IsCancelled;
private bool UnknownEndDate;
private bool EndedButMaybeActive;
public AccountPageViewModel(){
UpdatetProfile();
}
private void Timer_Tick(object sender, EventArgs e){
private void Timer_Tick(object? sender, EventArgs e){
var remaining = _targetTime - DateTime.Now;
if (remaining <= TimeSpan.Zero){
RemainingTime = "No active Subscription";
_timer.Stop();
_timer?.Stop();
if (UnknownEndDate){
RemainingTime = "Unknown Subscription end date";
}

View file

@ -32,28 +32,28 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private string _buttonTextSelectSeason = "Select Season";
[ObservableProperty]
private bool _addAllEpisodes = false;
private bool _addAllEpisodes;
[ObservableProperty]
private bool _buttonEnabled = false;
private bool _buttonEnabled;
[ObservableProperty]
private bool _allButtonEnabled = false;
private bool _allButtonEnabled;
[ObservableProperty]
private bool _showLoading = false;
private bool _showLoading;
[ObservableProperty]
private bool _searchEnabled = false;
private bool _searchEnabled;
[ObservableProperty]
private bool _searchVisible = true;
[ObservableProperty]
private bool _slectSeasonVisible = false;
private bool _slectSeasonVisible;
[ObservableProperty]
private bool _searchPopupVisible = false;
private bool _searchPopupVisible;
public ObservableCollection<ItemModel> Items{ get; set; } = new();
public ObservableCollection<CrBrowseSeries> SearchItems{ get; set; } = new();
@ -69,13 +69,13 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private Dictionary<string, List<ItemModel>> episodesBySeason = new();
private List<string> selectedEpisodes = new();
private List<ItemModel> selectedEpisodes = new();
private CrunchySeriesList? currentSeriesList;
private CrunchyMusicVideoList? currentMusicVideoList;
private bool CurrentSeasonFullySelected = false;
private bool CurrentSeasonFullySelected;
public AddDownloadPageViewModel(){
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
@ -98,9 +98,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (episode.ImageBitmap == null){
if (episode.Images.PosterTall != null){
var posterTall = episode.Images.PosterTall.First();
var imageUrl = posterTall.Find(ele => ele.Height == 180).Source
?? (posterTall.Count >= 2 ? posterTall[1].Source : posterTall.FirstOrDefault().Source);
episode.LoadImage(imageUrl);
var imageUrl = posterTall.Find(ele => ele.Height == 180)?.Source
?? (posterTall.Count >= 2 ? posterTall[1].Source : posterTall.FirstOrDefault()?.Source);
episode.LoadImage(imageUrl ?? string.Empty);
}
}
@ -171,14 +171,16 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
#region OnButtonPress
[RelayCommand]
public async void OnButtonPress(){
public async Task OnButtonPress(){
if (HasSelectedItemsOrEpisodes()){
Console.WriteLine("Added to Queue");
if (currentMusicVideoList != null){
AddSelectedMusicVideosToQueue();
} else{
AddSelectedEpisodesToQueue();
}
if (currentSeriesList != null){
await AddSelectedEpisodesToQueue();
}
ResetState();
@ -194,10 +196,12 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
private void AddSelectedMusicVideosToQueue(){
if (SelectedItems.Count > 0){
AddItemsToSelectedEpisodes();
if (selectedEpisodes.Count > 0){
var musicClass = CrunchyrollManager.Instance.CrMusic;
foreach (var selectedItem in SelectedItems){
var music = currentMusicVideoList.Value.Data?.FirstOrDefault(ele => ele.Id == selectedItem.Id);
foreach (var selectedItem in selectedEpisodes){
var music = currentMusicVideoList?.Data?.FirstOrDefault(ele => ele.Id == selectedItem.Id);
if (music != null){
var meta = musicClass.EpisodeMeta(music);
@ -207,24 +211,24 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
}
private async void AddSelectedEpisodesToQueue(){
private async Task AddSelectedEpisodesToQueue(){
AddItemsToSelectedEpisodes();
if (currentSeriesList != null){
await QueueManager.Instance.CrAddSeriesToQueue(
currentSeriesList.Value,
currentSeriesList,
new CrunchyMultiDownload(
CrunchyrollManager.Instance.CrunOptions.DubLang,
AddAllEpisodes,
false,
selectedEpisodes));
selectedEpisodes.Select(selectedEpisode => selectedEpisode.AbsolutNum).ToList()));
}
}
private void AddItemsToSelectedEpisodes(){
foreach (var selectedItem in SelectedItems){
if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){
selectedEpisodes.Add(selectedItem.AbsolutNum);
if (!selectedEpisodes.Contains(selectedItem)){
selectedEpisodes.Add(selectedItem);
}
}
}
@ -256,7 +260,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
var matchResult = ExtractLocaleAndIdFromUrl();
if (matchResult is (string locale, string id)){
if (matchResult is ({ } locale, { } id)){
switch (GetUrlType()){
case CrunchyUrlType.Artist:
await HandleArtistUrlAsync(locale, id);
@ -299,8 +303,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private async Task HandleArtistUrlAsync(string locale, string id){
SetLoadingState(true);
var list = await CrunchyrollManager.Instance.CrMusic.ParseArtistMusicVideosByIdAsync(
id, DetermineLocale(locale), true);
var list = await CrunchyrollManager.Instance.CrMusic.ParseArtistVideosByIdAsync(
id, DetermineLocale(locale), true, true);
SetLoadingState(false);
@ -335,6 +339,16 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
id, DetermineLocale(locale),
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true);
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(id, DetermineLocale(locale), true);
if (musicList != null){
currentMusicVideoList = musicList;
PopulateItemsFromMusicVideoList();
}
}
SetLoadingState(false);
if (list != null){
@ -348,16 +362,37 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private void PopulateItemsFromMusicVideoList(){
if (currentMusicVideoList?.Data != null){
foreach (var episode in currentMusicVideoList.Value.Data){
var imageUrl = episode.Images?.Thumbnail?.FirstOrDefault().Source ?? "";
foreach (var episode in currentMusicVideoList.Data){
string seasonKey;
switch (episode.EpisodeType){
case EpisodeType.MusicVideo:
seasonKey = "Music Videos ";
break;
case EpisodeType.Concert:
seasonKey = "Concerts ";
break;
case EpisodeType.Episode:
case EpisodeType.Unknown:
default:
seasonKey = "Unknown ";
break;
}
var imageUrl = episode.Images?.Thumbnail.FirstOrDefault()?.Source ?? "";
var time = $"{(episode.DurationMs / 1000) / 60}:{(episode.DurationMs / 1000) % 60:D2}";
var newItem = new ItemModel(episode.Id ?? "", imageUrl, episode.Description ?? "", time, episode.Title ?? "", "",
episode.SequenceNumber.ToString(), episode.SequenceNumber.ToString(), new List<string>());
var newItem = new ItemModel(episode.Id, imageUrl, episode.Description ?? "", time, episode.Title ?? "", seasonKey,
episode.SequenceNumber.ToString(), episode.Id, new List<string>(), episode.EpisodeType);
newItem.LoadImage(imageUrl);
Items.Add(newItem);
if (!episodesBySeason.ContainsKey(seasonKey)){
episodesBySeason[seasonKey] = new List<ItemModel>{ newItem };
SeasonList.Add(new ComboBoxItem{ Content = seasonKey });
} else{
episodesBySeason[seasonKey].Add(newItem);
}
}
CurrentSelectedSeason = SeasonList.First();
}
}
@ -367,7 +402,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
var itemModel = new ItemModel(
episode.Id, episode.Img, episode.Description, episode.Time, episode.Name, seasonKey,
episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum,
episode.E, episode.Lang);
episode.E, episode.Lang, episode.EpisodeType);
if (!episodesBySeason.ContainsKey(seasonKey)){
episodesBySeason[seasonKey] = new List<ItemModel>{ itemModel };
@ -407,7 +442,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
public void OnSelectSeasonPressed(){
if (CurrentSeasonFullySelected){
foreach (var item in Items){
selectedEpisodes.Remove(item.AbsolutNum);
selectedEpisodes.Remove(item);
SelectedItems.Remove(item);
}
@ -426,10 +461,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
partial void OnCurrentSelectedSeasonChanging(ComboBoxItem? oldValue, ComboBoxItem newValue){
if (SelectedItems == null) return;
foreach (var selectedItem in SelectedItems){
if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){
selectedEpisodes.Add(selectedItem.AbsolutNum);
if (!selectedEpisodes.Contains(selectedItem)){
selectedEpisodes.Add(selectedItem);
}
}
@ -443,7 +477,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
if (Items == null) return;
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
@ -486,7 +519,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (list != null){
currentSeriesList = list;
SearchPopulateEpisodesBySeason();
await SearchPopulateEpisodesBySeason(value.Id);
UpdateUiForEpisodeSelection();
} else{
ButtonEnabled = true;
@ -514,7 +547,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true);
}
private void SearchPopulateEpisodesBySeason(){
private async Task SearchPopulateEpisodesBySeason(string seriesId){
if (currentSeriesList?.List == null){
return;
}
@ -528,8 +561,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
foreach (var episode in currentSeriesList.Value.List){
foreach (var episode in currentSeriesList.List){
var seasonKey = "S" + episode.Season;
var episodeModel = new ItemModel(
episode.Id,
@ -540,7 +572,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
seasonKey,
episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum,
episode.E,
episode.Lang);
episode.Lang, episode.EpisodeType);
if (!episodesBySeason.ContainsKey(seasonKey)){
episodesBySeason[seasonKey] = new List<ItemModel>{ episodeModel };
@ -549,6 +581,19 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
episodesBySeason[seasonKey].Add(episodeModel);
}
}
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
var locale = string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
? CrunchyrollManager.Instance.DefaultLocale
: CrunchyrollManager.Instance.CrunOptions.HistoryLang;
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(seriesId, DetermineLocale(locale), true);
if (musicList != null){
currentMusicVideoList = musicList;
PopulateItemsFromMusicVideoList();
}
}
CurrentSelectedSeason = SeasonList.First();
}
@ -575,12 +620,12 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (episode.ImageBitmap == null){
episode.LoadImage(episode.ImageUrl);
Items.Add(episode);
if (selectedEpisodes.Contains(episode.AbsolutNum)){
if (selectedEpisodes.Contains(episode)){
SelectedItems.Add(episode);
}
} else{
Items.Add(episode);
if (selectedEpisodes.Contains(episode.AbsolutNum)){
if (selectedEpisodes.Contains(episode)){
SelectedItems.Add(episode);
}
}
@ -604,21 +649,16 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
// Clear collections and other managed resources
Items.Clear();
Items = null;
SearchItems.Clear();
SearchItems = null;
SelectedItems.Clear();
SelectedItems = null;
SeasonList.Clear();
SeasonList = null;
episodesBySeason.Clear();
episodesBySeason = null;
selectedEpisodes.Clear();
selectedEpisodes = null;
}
}
public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios) : INotifyPropertyChanged{
public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios, EpisodeType epType)
: INotifyPropertyChanged{
public string Id{ get; set; } = id;
public string ImageUrl{ get; set; } = imageUrl;
public Bitmap? ImageBitmap{ get; set; }
@ -633,6 +673,9 @@ public class ItemModel(string id, string imageUrl, string description, string ti
public string TitleFull{ get; set; } = season + episode + " - " + title;
public List<string> AvailableAudios{ get; set; } = availableAudios;
public EpisodeType EpisodeType{ get; set; } = epType;
public bool HasDubs{ get; set; } = availableAudios.Count != 0;
public event PropertyChangedEventHandler? PropertyChanged;

View file

@ -2,21 +2,15 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using DynamicData;
using DynamicData.Kernel;
using Newtonsoft.Json;
namespace CRD.ViewModels;
@ -70,9 +64,10 @@ public partial class CalendarPageViewModel : ViewModelBase{
private CalendarWeek? currentWeek;
private bool loading = true;
private bool loading;
public CalendarPageViewModel(){
loading = true;
CalendarDays = new ObservableCollection<CalendarDay>();
foreach (var languageItem in Languages.languages){
@ -138,7 +133,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
currentWeek = week;
CalendarDays.Clear();
CalendarDays.AddRange(week.CalendarDays);
if (week.CalendarDays != null) CalendarDays.AddRange(week.CalendarDays);
RaisePropertyChanged(nameof(CalendarDays));
ShowLoading = false;
if (CustomCalendar){
@ -146,9 +141,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
if (calendarDayCalendarEpisode.ImageBitmap == null){
if (calendarDayCalendarEpisode.AnilistEpisode){
calendarDayCalendarEpisode.LoadImage(100,150);
_ = calendarDayCalendarEpisode.LoadImage(100,150);
} else{
calendarDayCalendarEpisode.LoadImage();
_ = calendarDayCalendarEpisode.LoadImage();
}
}
@ -165,9 +160,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
if (calendarDayCalendarEpisode.ImageBitmap == null){
if (calendarDayCalendarEpisode.AnilistEpisode){
calendarDayCalendarEpisode.LoadImage(100,150);
_ = calendarDayCalendarEpisode.LoadImage(100,150);
} else{
calendarDayCalendarEpisode.LoadImage();
_ = calendarDayCalendarEpisode.LoadImage();
}
}
}
@ -263,7 +258,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
if (value?.Content != null){
CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage = value.Content.ToString();
Refresh();
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
}
@ -276,7 +271,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true);
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
partial void OnHideDubsChanged(bool value){
@ -285,7 +280,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
}
CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs = value;
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
partial void OnFilterByAirDateChanged(bool value){
@ -294,7 +289,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
}
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
partial void OnShowUpcomingEpisodesChanged(bool value){
@ -303,7 +298,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
}
CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes = value;
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
partial void OnCurrentCalendarDubFilterChanged(ComboBoxItem? value){
@ -313,7 +308,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
if (!string.IsNullOrEmpty(value?.Content + "")){
CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter = value?.Content + "";
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
}

View file

@ -10,7 +10,9 @@ using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
namespace CRD.ViewModels;
@ -36,12 +38,12 @@ public partial class DownloadsPageViewModel : ViewModelBase{
QueueManager.Instance.UpdateDownloadListItems();
}
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
partial void OnRemoveFinishedChanged(bool value){
CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload = value;
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
}
@ -67,9 +69,10 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
public DownloadItemModel(CrunchyEpMeta epMetaF){
epMeta = epMetaF;
ImageUrl = epMeta.Image;
ImageUrl = epMeta.Image ?? string.Empty;
Title = epMeta.SeriesTitle + (!string.IsNullOrEmpty(epMeta.Season) ? " - S" + epMeta.Season + "E" + (epMeta.EpisodeNumber != string.Empty ? epMeta.EpisodeNumber : epMeta.AbsolutEpisodeNumberE) : "") + " - " +
epMeta.EpisodeTitle;
isDownloading = epMeta.DownloadProgress.IsDownloading || Done;
Done = epMeta.DownloadProgress.Done;
@ -81,11 +84,19 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting";
if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString() + (!string.IsNullOrEmpty(epMeta.Resolution) ? "- " + epMeta.Resolution : "");
InfoText = JoinWithSeparator(
GetDubString(),
GetSubtitleString(),
epMeta.Resolution
);
Error = epMeta.DownloadProgress.Error;
}
string JoinWithSeparator(params string[] parts){
return string.Join(" - ", parts.Where(part => !string.IsNullOrEmpty(part)));
}
private string GetDubString(){
if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){
return "";
@ -138,10 +149,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting";
if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString() + (!string.IsNullOrEmpty(epMeta.Resolution) ? "- " + epMeta.Resolution : "");
InfoText = JoinWithSeparator(
GetDubString(),
GetSubtitleString(),
epMeta.Resolution
);
Error = epMeta.DownloadProgress.Error;
if (PropertyChanged != null){
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Percent)));
@ -186,15 +202,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
epMeta.DownloadProgress.IsDownloading = true;
Paused = !epMeta.Paused && !isDownloading || epMeta.Paused;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
CrDownloadOptions newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
if (epMeta.OnlySubs){
newOptions.Novids = true;
newOptions.Noaudio = true;
}
await CrunchyrollManager.Instance.DownloadEpisode(epMeta,newOptions );
await CrunchyrollManager.Instance.DownloadEpisode(epMeta, newOptions);
}
}
@ -209,7 +225,8 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
if (File.Exists(downloadItemDownloadedFile)){
File.Delete(downloadItemDownloadedFile);
}
} catch (Exception e){
} catch (Exception){
// ignored
}
}
}

View file

@ -13,6 +13,7 @@ using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
@ -29,7 +30,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
private ProgramManager _programManager;
[ObservableProperty]
private HistorySeries _selectedSeries;
private HistorySeries? _selectedSeries;
[ObservableProperty]
private static bool _editMode;
@ -72,10 +73,16 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _isPosterViewSelected = false;
private bool _isPosterViewSelected;
[ObservableProperty]
private bool _isTableViewSelected = false;
private bool _isTableViewSelected;
[ObservableProperty]
private bool _showSeries = true;
[ObservableProperty]
private bool _showArtists;
[ObservableProperty]
private static bool _viewSelectionOpen;
@ -98,7 +105,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
private FilterType currentFilterType;
[ObservableProperty]
private static bool _sortDir = false;
private static bool _sortDir;
[ObservableProperty]
private static bool _sonarrAvailable;
@ -127,6 +134,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
currentFilterType = properties?.SelectedFilter ?? FilterType.All;
ScaleValue = properties?.ScaleValue ?? 0.73;
SortDir = properties?.Ascending ?? false;
ShowSeries = properties?.ShowSeries ?? true;
ShowArtists = properties?.ShowArtists ?? false;
foreach (HistoryViewType viewType in Enum.GetValues(typeof(HistoryViewType))){
var combobox = new ComboBoxItem{ Content = viewType };
@ -137,7 +146,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
foreach (SortingType sortingType in Enum.GetValues(typeof(SortingType))){
var combobox = new SortingListElement(){ SortingTitle = sortingType.GetEnumMemberValue(), SelectedSorting = sortingType };
var combobox = new SortingListElement{ SortingTitle = sortingType.GetEnumMemberValue(), SelectedSorting = sortingType };
SortingList.Add(combobox);
if (sortingType == currentSortingType){
SelectedSorting = combobox;
@ -149,7 +158,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
continue;
}
var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType };
var item = new FilterListElement{ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType };
FilterList.Add(item);
if (filterType == currentFilterType){
SelectedFilter = item;
@ -162,7 +171,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
foreach (var historySeries in Items){
if (historySeries.ThumbnailImage == null){
historySeries.LoadImage();
_ = historySeries.LoadImage();
}
historySeries.UpdateNewEpisodes();
@ -179,15 +188,15 @@ public partial class HistoryPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.Ascending = SortDir;
} else{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties()
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties
{ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType, Ascending = SortDir };
}
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
partial void OnSelectedViewChanged(ComboBoxItem value){
if (Enum.TryParse(value.Content + "", out HistoryViewType viewType)){
partial void OnSelectedViewChanged(ComboBoxItem? value){
if (Enum.TryParse(value?.Content + "", out HistoryViewType viewType)){
currentViewType = viewType;
IsPosterViewSelected = currentViewType == HistoryViewType.Posters;
IsTableViewSelected = currentViewType == HistoryViewType.Table;
@ -214,21 +223,33 @@ public partial class HistoryPageViewModel : ViewModelBase{
return;
}
if (newValue.SelectedSorting != null){
currentSortingType = newValue.SelectedSorting;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.History.SortItems();
if (SelectedFilter != null){
OnSelectedFilterChanged(SelectedFilter);
}
} else{
Console.Error.WriteLine("Invalid viewtype selected");
currentSortingType = newValue.SelectedSorting;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.History.SortItems();
if (SelectedFilter != null){
OnSelectedFilterChanged(SelectedFilter);
}
SortingSelectionOpen = false;
UpdateSettings();
}
partial void OnShowArtistsChanged(bool value){
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.ShowArtists = ShowArtists;
CfgManager.WriteCrSettings();
ApplyFilter();
}
partial void OnShowSeriesChanged(bool value){
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.ShowSeries = ShowSeries;
CfgManager.WriteCrSettings();
ApplyFilter();
}
partial void OnSelectedFilterChanged(FilterListElement? value){
if (value == null){
@ -237,37 +258,52 @@ public partial class HistoryPageViewModel : ViewModelBase{
currentFilterType = value.SelectedType;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType;
CfgManager.WriteCrSettings();
ApplyFilter();
}
private void ApplyFilter(){
List<HistorySeries> filteredItems;
switch (currentFilterType){
case FilterType.All:
FilteredItems.Clear();
FilteredItems.AddRange(Items);
filteredItems = Items.ToList();
break;
case FilterType.MissingEpisodes:
List<HistorySeries> filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList();
FilteredItems.Clear();
FilteredItems.AddRange(filteredItems);
filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList();
break;
case FilterType.MissingEpisodesSonarr:
var missingSonarrFiltered = Items.Where(historySeries =>
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) && // Check series ID
historySeries.Seasons.Any(season => // Check each season
season.EpisodesList.Any(historyEpisode => // Check each episode
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile))) // Filter condition
filteredItems = Items.Where(historySeries =>
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) &&
historySeries.Seasons.Any(season =>
season.EpisodesList.Any(historyEpisode =>
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile)))
.ToList();
FilteredItems.Clear();
FilteredItems.AddRange(missingSonarrFiltered);
break;
case FilterType.ContinuingOnly:
List<HistorySeries> continuingFiltered = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
FilteredItems.Clear();
FilteredItems.AddRange(continuingFiltered);
filteredItems = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
break;
default:
filteredItems = new List<HistorySeries>();
break;
}
if (!ShowArtists){
filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Artist);
}
if (!ShowSeries){
filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Series);
}
FilteredItems.Clear();
FilteredItems.AddRange(filteredItems);
}
@ -298,12 +334,12 @@ public partial class HistoryPageViewModel : ViewModelBase{
NavToSeries();
if (!string.IsNullOrEmpty(value.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
if (SelectedSeries != null) _ = CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
CfgManager.UpdateHistoryFile();
}
SelectedSeries = null;
_selectedSeries = null;
}
[RelayCommand]
@ -348,7 +384,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
[RelayCommand]
public async void AddMissingToQueue(){
public async Task AddMissingToQueue(){
var tasks = FilteredItems
.Select(item => item.AddNewMissingToDownloads());
@ -408,7 +444,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
ProgressText = $"{count + 1}/{totalSeries}";
// Await the CRUpdateSeries task for each seriesId
await crInstance.History.CRUpdateSeries(seriesIds[count], "");
await crInstance.History.CrUpdateSeries(seriesIds[count], "");
RaisePropertyChanged(nameof(ProgressText));
}
@ -500,27 +536,30 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
}
public class HistoryPageProperties(){
public class HistoryPageProperties{
public SortingType? SelectedSorting{ get; set; }
public HistoryViewType SelectedView{ get; set; }
public FilterType SelectedFilter{ get; set; }
public double? ScaleValue{ get; set; }
public bool Ascending{ get; set; }
public bool ShowSeries{ get; set; } = true;
public bool ShowArtists{ get; set; } = true;
}
public class SeasonsPageProperties(){
public class SeasonsPageProperties{
public SortingType? SelectedSorting{ get; set; }
public bool Ascending{ get; set; }
}
public class SortingListElement(){
public class SortingListElement{
public SortingType SelectedSorting{ get; set; }
public string? SortingTitle{ get; set; }
}
public class FilterListElement(){
public class FilterListElement{
public FilterType SelectedType{ get; set; }
public string? FilterTitle{ get; set; }
}

View file

@ -29,6 +29,9 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty]
public static bool _sonarrAvailable;
[ObservableProperty]
public static bool _showMonitoredBookmark;
[ObservableProperty]
public static bool _sonarrConnected;
@ -53,7 +56,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
_selectedSeries = CrunchyrollManager.Instance.SelectedSeries;
if (_selectedSeries.ThumbnailImage == null){
_selectedSeries.LoadImage();
_ = _selectedSeries.LoadImage();
}
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
@ -61,13 +64,18 @@ public partial class SeriesPageViewModel : ViewModelBase{
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected;
if (SonarrAvailable){
ShowMonitoredBookmark = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored;
}
} else{
SonarrAvailable = false;
}
} else{
SonarrConnected = SonarrAvailable = false;
}
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
@ -87,17 +95,19 @@ public partial class SeriesPageViewModel : ViewModelBase{
var seasonPath = season.SeasonDownloadPath;
var directoryInfo = new DirectoryInfo(seasonPath);
string parentFolderPath = directoryInfo.Parent?.FullName;
if (!string.IsNullOrEmpty(directoryInfo.Parent?.FullName)){
string parentFolderPath = directoryInfo.Parent?.FullName ?? string.Empty;
if (Directory.Exists(parentFolderPath)){
SeriesFolderPath = parentFolderPath;
SeriesFolderPathExists = true;
if (Directory.Exists(parentFolderPath)){
SeriesFolderPath = parentFolderPath;
SeriesFolderPathExists = true;
}
}
} catch (Exception e){
Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}");
}
} else{
var customPath = string.Empty;
string customPath;
if (string.IsNullOrEmpty(SelectedSeries.SeriesTitle))
return;
@ -110,10 +120,10 @@ public partial class SeriesPageViewModel : ViewModelBase{
// Check Crunchyroll download directory
var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath;
if (!string.IsNullOrEmpty(downloadDirPath)){
customPath = System.IO.Path.Combine(downloadDirPath, seriesTitle);
customPath = Path.Combine(downloadDirPath, seriesTitle);
} else{
// Fallback to configured VIDEOS_DIR path
customPath = System.IO.Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle);
customPath = Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle);
}
// Check if custom path exists
@ -186,7 +196,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
SonarrConnected = SonarrAvailable = false;
}
UpdateData("");
_ = UpdateData("");
}
}

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll.ViewModels;
using CRD.Downloader.Crunchyroll.Views;
@ -15,14 +14,14 @@ using Image = Avalonia.Controls.Image;
namespace CRD.ViewModels;
public partial class SettingsPageViewModel : ViewModelBase{
public class SettingsPageViewModel : ViewModelBase{
public ObservableCollection<TabViewItem> Tabs{ get; } = new();
private TabViewItem CreateTab(string header, string iconPath, UserControl content, object viewModel){
content.DataContext = viewModel;
Bitmap bitmap = null;
Bitmap? bitmap = null;
try{
// Load the image using AssetLoader.Open
bitmap = new Bitmap(Avalonia.Platform.AssetLoader.Open(new Uri(iconPath)));

View file

@ -1,30 +1,24 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views;
using Newtonsoft.Json;
using ReactiveUI;
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace CRD.ViewModels;
@ -147,7 +141,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
#endregion
[ObservableProperty]
private AnilistSeries _selectedSeries;
private AnilistSeries? _selectedSeries;
[ObservableProperty]
private int _selectedIndex;
@ -164,7 +158,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
private SortingType currentSortingType;
[ObservableProperty]
private static bool _sortDir = false;
private static bool _sortDir;
public ObservableCollection<SortingListElement> SortingList{ get; } =[];
@ -236,12 +230,12 @@ public partial class UpcomingPageViewModel : ViewModelBase{
}
[RelayCommand]
public async void AddToHistory(AnilistSeries series){
public async Task AddToHistory(AnilistSeries series){
if (!string.IsNullOrEmpty(series.CrunchyrollID)){
if (CrunchyrollManager.Instance.CrunOptions.History){
series.IsInHistory = true;
RaisePropertyChanged(nameof(series.IsInHistory));
var sucess = await CrunchyrollManager.Instance.History.CRUpdateSeries(series.CrunchyrollID, "");
var sucess = await CrunchyrollManager.Instance.History.CrUpdateSeries(series.CrunchyrollID, "");
series.IsInHistory = sucess;
RaisePropertyChanged(nameof(series.IsInHistory));
@ -435,7 +429,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties = new SeasonsPageProperties(){ SelectedSorting = currentSortingType, Ascending = SortDir };
}
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
partial void OnSelectedSortingChanged(SortingListElement? oldValue, SortingListElement? newValue){
@ -452,11 +446,9 @@ public partial class UpcomingPageViewModel : ViewModelBase{
return;
}
if (newValue.SelectedSorting != null){
currentSortingType = newValue.SelectedSorting;
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null) CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType;
SortItems();
}
currentSortingType = newValue.SelectedSorting;
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null) CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType;
SortItems();
SortingSelectionOpen = false;
UpdateSettings();

View file

@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Views;
using DynamicData;

View file

@ -17,8 +17,9 @@ using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using CRD.Utils.Structs.History;
using FluentAvalonia.Styling;
@ -35,8 +36,14 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
[ObservableProperty]
private bool _history;
[ObservableProperty]
private bool _historyIncludeCrArtists;
[ObservableProperty]
private bool _historyAddSpecials;
[ObservableProperty]
private bool _historySkipUnmonitored;
[ObservableProperty]
private bool _historyCountSonarr;
@ -236,7 +243,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
ProxyUsername = options.ProxyUsername ?? "";
ProxyPassword = options.ProxyPassword ?? "";
ProxyPort = options.ProxyPort;
HistoryIncludeCrArtists = options.HistoryIncludeCrArtists;
HistoryAddSpecials = options.HistoryAddSpecials;
HistorySkipUnmonitored = options.HistorySkipUnmonitored;
HistoryCountSonarr = options.HistoryCountSonarr;
DownloadSpeed = options.DownloadSpeedLimit;
DownloadToTempFolder = options.DownloadToTempFolder;
@ -265,6 +274,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder;
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials;
CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists = HistoryIncludeCrArtists;
CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored = HistorySkipUnmonitored;
CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr;
CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
@ -309,7 +320,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.LogMode = LogMode;
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
[RelayCommand]
@ -364,7 +375,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
pathSetter(selectedFolder.Path.LocalPath);
var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter();
pathSetter(finalPath);
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
}
@ -411,7 +422,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
pathSetter(selectedFile.Path.LocalPath);
var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter();
pathSetter(finalPath);
CfgManager.WriteSettingsToFile();
CfgManager.WriteCrSettings();
}
}
@ -490,9 +501,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
decompressedJson,
CrunchyrollManager.Instance.SettingsJsonSerializerSettings
) ?? new ObservableCollection<HistorySeries>();
CrunchyrollManager.Instance.HistoryList = historyList;
Parallel.ForEach(historyList, historySeries => {
historySeries.Init();
@ -500,14 +511,13 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
historySeriesSeason.Init();
}
});
} else{
CrunchyrollManager.Instance.HistoryList = new ObservableCollection<HistorySeries>();
}
} else{
CrunchyrollManager.Instance.HistoryList = new ObservableCollection<HistorySeries>();
}
_ = Task.Run(() => SonarrClient.Instance.RefreshSonarrLite());
} else{
CrunchyrollManager.Instance.HistoryList = new ObservableCollection<HistorySeries>();

View file

@ -212,6 +212,7 @@
</Grid>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" Margin="10">
<TextBlock FontStyle="Italic"
IsVisible="{Binding HasDubs}"
Opacity="0.8" Text="Dubs: ">
</TextBlock>
<TextBlock Text="{Binding AvailableAudios, Converter={StaticResource UiListToStringConverter}}"

View file

@ -186,15 +186,44 @@
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonFilter}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Single" Width="210"
MaxHeight="400"
ItemsSource="{Binding FilterList}" SelectedItem="{Binding SelectedFilter}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FilterTitle}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Orientation="Vertical">
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- TextBlock takes remaining space -->
<ColumnDefinition Width="Auto" /> <!-- ToggleSwitch takes only needed space -->
</Grid.ColumnDefinitions>
<TextBlock Text="Series" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding ShowSeries}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Artists" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding ShowArtists}" VerticalAlignment="Center" />
</Grid>
<Rectangle Height="1" Fill="Gray" Margin="0,8,0,8" />
<ListBox SelectionMode="Single" Width="210"
MaxHeight="400"
ItemsSource="{Binding FilterList}" SelectedItem="{Binding SelectedFilter}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FilterTitle}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Border>
</Popup>
</StackPanel>

View file

@ -13,6 +13,7 @@
<UserControl.Resources>
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
</UserControl.Resources>
<Grid>
@ -57,8 +58,8 @@
<TextBlock Grid.Row="0" FontSize="45" Text="{Binding SelectedSeries.SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock>
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock>
<TextBlock Grid.Row="3" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableDubs}"></TextBlock>
<TextBlock Grid.Row="4" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableSubs}"></TextBlock>
<TextBlock Grid.Row="3" IsVisible="{Binding SelectedSeries.HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableDubs}"></TextBlock>
<TextBlock Grid.Row="4" IsVisible="{Binding SelectedSeries.HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableSubs}"></TextBlock>
<StackPanel Grid.Row="5" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
@ -183,7 +184,7 @@
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10" >
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -313,14 +314,15 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel>
<StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="E"></TextBlock>
<TextBlock Text="{Binding Episode}"></TextBlock>
<TextBlock Text=" - "></TextBlock>
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontStyle="Italic"
FontSize="12"
Opacity="0.8" Text="Dubs: ">
@ -343,10 +345,30 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0"
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
<StackPanel IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).ShowMonitoredBookmark}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<controls:SymbolIcon IsVisible="{Binding !SonarrIsMonitored}" Margin="0 0 5 0 " Symbol="Bookmark" FontSize="18" HorizontalAlignment="Center"
VerticalAlignment="Center">
<ToolTip.Tip>
<TextBlock Text="Unmonitored" FontSize="15" />
</ToolTip.Tip>
</controls:SymbolIcon>
<controls:SymbolIcon IsVisible="{Binding SonarrIsMonitored}" Margin="0 0 5 0 " Symbol="BookmarkFilled" FontSize="18" HorizontalAlignment="Center"
VerticalAlignment="Center">
<ToolTip.Tip>
<TextBlock Text="Monitored" FontSize="15" />
</ToolTip.Tip>
</controls:SymbolIcon>
</StackPanel>
<controls:ImageIcon IsVisible="{Binding SonarrHasFile}"
Source="../Assets/sonarr.png" Width="25"
Height="25" />
@ -382,7 +404,7 @@
</Button>
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding DownloadEpisode}"
CommandParameter="false">
<StackPanel Orientation="Horizontal">
@ -392,9 +414,10 @@
<TextBlock Text="Download Episode" FontSize="15" />
</ToolTip.Tip>
</Button>
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
VerticalAlignment="Center"
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
Command="{Binding DownloadEpisode}"
CommandParameter="true">
<StackPanel Orientation="Horizontal">
@ -404,8 +427,6 @@
<TextBlock Text="Download Subs" FontSize="15" />
</ToolTip.Tip>
</Button>
</StackPanel>

View file

@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace CRD.Views;

View file

@ -1,8 +1,4 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CRD.Utils.Sonarr;
using Avalonia.Controls;
using CRD.Utils.Structs;
using CRD.ViewModels;

View file

@ -33,17 +33,29 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Include CR Artists" Description="Add Crunchyroll artists (music) to the history">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryIncludeCrArtists}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Add Specials" Description="Add specials to the queue if they weren't downloaded before">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Missing/New Count from Sonarr" Description="The missing count (number in the orange corner) will count the episodes missing from sonarr">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryCountSonarr}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Skip Sonarr Unmonitored" Description="Skips unmonitored sonarr episodes when counting Missing/New">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistorySkipUnmonitored}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>