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;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using DynamicData;
using HtmlAgilityPack; using HtmlAgilityPack;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -90,56 +89,60 @@ public class CalendarManager{
foreach (var day in dayNodes){ foreach (var day in dayNodes){
// Extract the date and day name // Extract the date and day name
var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date"); 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){ if (week.FirstDayOfWeek == DateTime.MinValue){
week.FirstDayOfWeek = dayDateTime; week.FirstDayOfWeek = dayDateTime;
week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd"); 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);
} }
}
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{ } else{
Console.Error.WriteLine("No days found in the HTML document."); Console.Error.WriteLine("No days found in the HTML document.");
@ -260,7 +263,7 @@ public class CalendarManager{
calEpisode.EpisodeName = crBrowseEpisode.Title; calEpisode.EpisodeName = crBrowseEpisode.Title;
calEpisode.SeriesUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId; 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.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.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1"; calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle; calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
@ -268,10 +271,10 @@ public class CalendarManager{
calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId; calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId;
var existingEpisode = calendarDay.CalendarEpisodes var existingEpisode = calendarDay.CalendarEpisodes
?.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName); .FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName);
if (existingEpisode != null){ if (existingEpisode != null){
if (!int.TryParse(existingEpisode.EpisodeNumber, out var num)){ if (!int.TryParse(existingEpisode.EpisodeNumber, out _)){
existingEpisode.EpisodeNumber = "..."; existingEpisode.EpisodeNumber = "...";
} else{ } else{
var existingNumbers = existingEpisode.EpisodeNumber var existingNumbers = existingEpisode.EpisodeNumber
@ -300,7 +303,7 @@ public class CalendarManager{
existingEpisode.CalendarEpisodes.Add(calEpisode); existingEpisode.CalendarEpisodes.Add(calEpisode);
} else{ } else{
calendarDay.CalendarEpisodes?.Add(calEpisode); calendarDay.CalendarEpisodes.Add(calEpisode);
} }
} }
} }
@ -429,8 +432,6 @@ public class CalendarManager{
calEp.EpisodeNumber = anilistEle.Episode.ToString(); calEp.EpisodeNumber = anilistEle.Episode.ToString();
calEp.AnilistEpisode = true; calEp.AnilistEpisode = true;
var crunchyrollID = "";
if (anilistEle.Media?.ExternalLinks != null){ if (anilistEle.Media?.ExternalLinks != null){
var url = anilistEle.Media.ExternalLinks.First(external => var url = anilistEle.Media.ExternalLinks.First(external =>
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase)).Url; string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase)).Url;
@ -438,10 +439,11 @@ public class CalendarManager{
string pattern = @"series\/([^\/]+)"; string pattern = @"series\/([^\/]+)";
Match match = Regex.Match(url, pattern); Match match = Regex.Match(url, pattern);
string crunchyrollId;
if (match.Success){ if (match.Success){
crunchyrollID = match.Groups[1].Value; crunchyrollId = match.Groups[1].Value;
AdjustReleaseTimeToHistory(calEp, crunchyrollID); AdjustReleaseTimeToHistory(calEp, crunchyrollId);
} else{ } else{
Uri uri = new Uri(url); Uri uri = new Uri(url);
@ -462,9 +464,9 @@ public class CalendarManager{
Match match2 = Regex.Match(finalUrl ?? string.Empty, pattern); Match match2 = Regex.Match(finalUrl ?? string.Empty, pattern);
if (match2.Success){ 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.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.Crunchyroll;
using CRD.Views; using CRD.Views;
@ -15,6 +16,11 @@ namespace CRD.Downloader.Crunchyroll;
public class CrAuth{ public class CrAuth{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; 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(){ public async Task AuthAnonymous(){
string uuid = Guid.NewGuid().ToString(); string uuid = Guid.NewGuid().ToString();
@ -22,17 +28,18 @@ public class CrAuth{
{ "grant_type", "client_id" }, { "grant_type", "client_id" },
{ "scope", "offline_access" }, { "scope", "offline_access" },
{ "device_id", uuid }, { "device_id", uuid },
{ "device_type", "Chrome on Windows" } { "device_name", DeviceName },
{ "device_type", DeviceType },
}; };
var requestContent = new FormUrlEncodedContent(formData); var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{ var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch }, { "Authorization", authorization },
{ "User-Agent", ApiUrls.ChromeUserAgent } { "User-Agent", userAgent }
}; };
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent Content = requestContent
}; };
@ -63,7 +70,7 @@ public class CrAuth{
crunInstance.Token.device_id = deviceId; crunInstance.Token.device_id = deviceId;
crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in); 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" }, { "grant_type", "password" },
{ "scope", "offline_access" }, { "scope", "offline_access" },
{ "device_id", uuid }, { "device_id", uuid },
{ "device_type", "Chrome on Windows" } { "device_name", DeviceName },
{ "device_type", DeviceType },
}; };
var requestContent = new FormUrlEncodedContent(formData); var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{ var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch }, { "Authorization", authorization },
{ "User-Agent", ApiUrls.ChromeUserAgent } { "User-Agent", userAgent }
}; };
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent Content = requestContent
}; };
@ -120,7 +128,7 @@ public class CrAuth{
return; 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); var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -183,18 +191,19 @@ public class CrAuth{
{ "refresh_token", crunInstance.Token.refresh_token }, { "refresh_token", crunInstance.Token.refresh_token },
{ "scope", "offline_access" }, { "scope", "offline_access" },
{ "device_id", uuid }, { "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 requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{ var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch }, { "Authorization", authorization },
{ "User-Agent", ApiUrls.ChromeUserAgent } { "User-Agent", userAgent }
}; };
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent Content = requestContent
}; };
@ -237,21 +246,22 @@ public class CrAuth{
string uuid = Guid.NewGuid().ToString(); string uuid = Guid.NewGuid().ToString();
var formData = new Dictionary<string, string>{ var formData = new Dictionary<string, string>{
{ "refresh_token", crunInstance.Token.refresh_token }, { "refresh_token", crunInstance.Token?.refresh_token ?? "" },
{ "grant_type", "refresh_token" }, { "grant_type", "refresh_token" },
{ "scope", "offline_access" }, { "scope", "offline_access" },
{ "device_id", uuid }, { "device_id", uuid },
{ "device_type", "Chrome on Windows" } { "device_name", DeviceName },
{ "device_type", DeviceType },
}; };
var requestContent = new FormUrlEncodedContent(formData); var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{ var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch }, { "Authorization", authorization },
{ "User-Agent", ApiUrls.ChromeUserAgent } { "User-Agent", userAgent }
}; };
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){ var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
Content = requestContent Content = requestContent
}; };
@ -259,7 +269,7 @@ public class CrAuth{
request.Headers.Add(header.Key, header.Value); 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); var response = await HttpClientReq.Instance.SendHttpRequest(request);

View file

@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
namespace CRD.Downloader.Crunchyroll; namespace CRD.Downloader.Crunchyroll;
@ -35,9 +36,9 @@ public class CrEpisode(){
return null; 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; return null;
} }
@ -59,7 +60,7 @@ public class CrEpisode(){
CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData(); CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData();
if (crunInstance.CrunOptions.History && updateHistory){ 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); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == dlEpisode.SeriesId);
if (historySeries != null){ if (historySeries != null){
CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(false); CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(false);
@ -81,7 +82,13 @@ public class CrEpisode(){
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){
// Push to arrays if there are no duplicates of the same language // Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode); 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{ } else{
@ -91,7 +98,13 @@ public class CrEpisode(){
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){ if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){
// Push to arrays if there are no duplicates of the same language // Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode); 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; 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; string newKey;
if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){ if (isSpecial && !string.IsNullOrEmpty(episode.EpisodeAndLanguages.Items[0].Episode)){
newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id); newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id);
@ -110,14 +123,14 @@ public class CrEpisode(){
episode.Key = newKey; 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(); ?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
var title = episode.EpisodeAndLanguages.Items[0].Title; var title = episode.EpisodeAndLanguages.Items[0].Title;
var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString(); var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString();
var languages = episode.EpisodeAndLanguages.Items.Select((a, index) => 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)}]"); Console.WriteLine($"[{episode.Key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
@ -125,7 +138,7 @@ public class CrEpisode(){
if (!serieshasversions){ if (!serieshasversions){
Console.WriteLine("Couldn\'t find versions on episode, added languages with language array."); Console.WriteLine("Couldn\'t find versions on episode, added languages with language array.");
} }
return episode; return episode;
} }
@ -160,17 +173,17 @@ public class CrEpisode(){
var epMeta = new CrunchyEpMeta(); var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } }; 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(); 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(); Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.EpisodeNumber = item.Episode; epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title; epMeta.EpisodeTitle = item.Title;
epMeta.SeasonId = item.SeasonId; epMeta.SeasonId = item.SeasonId;
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
epMeta.ShowId = item.SeriesId; epMeta.SeriesId = item.SeriesId;
epMeta.AbsolutEpisodeNumberE = epNum; epMeta.AbsolutEpisodeNumberE = epNum;
epMeta.Image = images[images.Count / 2].FirstOrDefault().Source; epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source;
epMeta.DownloadProgress = new DownloadProgress(){ epMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = false, IsDownloading = false,
Done = 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]; epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
retMeta.Data.Add(epMetaData); retMeta.Data.Add(epMetaData);
} else{ } else{
@ -215,7 +228,7 @@ public class CrEpisode(){
return retMeta; 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); await crunInstance.CrAuth.RefreshToken(true);
CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase(); CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase();
complete.Data =[]; complete.Data =[];
@ -257,7 +270,6 @@ public class CrEpisode(){
requestAmount += 50; requestAmount += 50;
} }
} }
} }
} else{ } else{
break; break;

View file

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

View file

@ -14,44 +14,119 @@ namespace CRD.Downloader.Crunchyroll;
public class CrMusic{ public class CrMusic{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task<CrunchyMusicVideo?> ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false){ public async Task<CrunchyMusicVideoList?> ParseFeaturedMusicVideoByIdAsync(string seriesId, string crLocale, bool forcedLang = false, bool updateHistory = false){
return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos"); 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){ public async Task<CrunchyMusicVideo?> ParseConcertByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){
return await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/concerts"); 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){ public async Task<CrunchyMusicVideoList?> ParseArtistMusicVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/music_videos", crLocale, forcedLang); var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang);
var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/concerts", 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); await Task.WhenAll(musicVideosTask, concertsTask);
var musicVideos = await musicVideosTask; var musicVideos = await musicVideosTask;
var concerts = await concertsTask; 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); musicVideos.Data.AddRange(concerts.Data);
} }
if (updateHistory){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
}
return musicVideos; 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){ private async Task<CrunchyMusicVideo?> ParseMediaByIdAsync(string id, string crLocale, bool forcedLang, string endpoint){
var mediaList = await FetchMediaListAsync($"{ApiUrls.Content}/{endpoint}/{id}", crLocale, forcedLang); var mediaList = await FetchMediaListAsync($"{ApiUrls.Content}/{endpoint}/{id}", crLocale, forcedLang);
switch (mediaList.Total){ switch (mediaList.Total){
case < 1: case < 1:
return null; return null;
case 1 when mediaList.Data != null: case 1 when mediaList.Data.Count > 0:
return mediaList.Data.First(); return mediaList.Data.First();
default: default:
Console.Error.WriteLine($"Multiple items returned for endpoint {endpoint} with ID {id}"); 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 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){ private NameValueCollection CreateQueryParameters(string crLocale, bool forcedLang){
@ -88,15 +163,16 @@ public class CrMusic{
public CrunchyEpMeta EpisodeMeta(CrunchyMusicVideo episodeP){ public CrunchyEpMeta EpisodeMeta(CrunchyMusicVideo episodeP){
var images = (episodeP.Images?.Thumbnail ?? new List<Image>{ new Image{ Source = "/notFound.png" } }); var images = (episodeP.Images?.Thumbnail ?? new List<Image>{ new Image{ Source = "/notFound.png" } });
var epMeta = new CrunchyEpMeta(); var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = episodeP.Id, Versions = null } }; epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = episodeP.Id, Versions = null } };
epMeta.SeriesTitle = "Music"; epMeta.SeriesTitle = episodeP.GetSeriesTitle();
epMeta.SeasonTitle = episodeP.DisplayArtistName; epMeta.SeasonTitle = episodeP.GetSeasonTitle();
epMeta.EpisodeNumber = episodeP.SequenceNumber + ""; epMeta.EpisodeNumber = episodeP.SequenceNumber + "";
epMeta.EpisodeTitle = episodeP.Title; epMeta.EpisodeTitle = episodeP.GetEpisodeTitle();
epMeta.SeasonId = ""; epMeta.SeasonId = episodeP.GetSeasonId();
epMeta.Season = ""; epMeta.Season = "";
epMeta.ShowId = ""; epMeta.SeriesId = episodeP.GetSeriesId();
epMeta.AbsolutEpisodeNumberE = ""; epMeta.AbsolutEpisodeNumberE = "";
epMeta.Image = images[images.Count / 2].Source; epMeta.Image = images[images.Count / 2].Source;
epMeta.DownloadProgress = new DownloadProgress(){ epMeta.DownloadProgress = new DownloadProgress(){

View file

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

View file

@ -21,6 +21,7 @@ using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.Crunchyroll;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.ViewModels;
using CRD.ViewModels.Utils; using CRD.ViewModels.Utils;
using CRD.Views; using CRD.Views;
using CRD.Views.Utils; using CRD.Views.Utils;
@ -130,9 +131,75 @@ public class CrunchyrollManager{
options.BackgroundImageOpacity = 0.5; options.BackgroundImageOpacity = 0.5;
options.BackgroundImageBlurRadius = 10; 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; 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; return options;
} }
@ -147,7 +214,6 @@ public class CrunchyrollManager{
CrMusic = new CrMusic(); CrMusic = new CrMusic();
History = new History(); History = new History();
Profile = new CrProfile{ Profile = new CrProfile{
Username = "???", Username = "???",
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
@ -155,6 +221,11 @@ public class CrunchyrollManager{
PreferredContentSubtitleLanguage = DefaultLocale, PreferredContentSubtitleLanguage = DefaultLocale,
HasPremium = false, HasPremium = false,
}; };
if (Path.Exists(CfgManager.PathCrDownloadOptionsOld)){
CfgManager.WriteCrSettings();
Helpers.DeleteFile(CfgManager.PathCrDownloadOptionsOld);
}
} }
public async Task Init(){ public async Task Init(){
@ -183,8 +254,11 @@ public class CrunchyrollManager{
} }
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){ if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
Token = CfgManager.DeserializeFromFile<CrToken>(CfgManager.PathCrToken); Token = CfgManager.ReadJsonFromFile<CrToken>(CfgManager.PathCrToken);
await CrAuth.LoginWithToken(); await CrAuth.LoginWithToken();
if (Path.Exists(CfgManager.PathCrTokenOld)){
Helpers.DeleteFile(CfgManager.PathCrTokenOld);
}
} else{ } else{
await CrAuth.AuthAnonymous(); await CrAuth.AuthAnonymous();
} }
@ -200,12 +274,11 @@ public class CrunchyrollManager{
); );
if (historyList != null){ if (historyList != null){
HistoryList = historyList; HistoryList = historyList;
Parallel.ForEach(historyList, historySeries => { Parallel.ForEach(historyList, historySeries => {
historySeries.Init(); historySeries.Init();
foreach (var historySeriesSeason in historySeries.Seasons){ foreach (var historySeriesSeason in historySeries.Seasons){
historySeriesSeason.Init(); historySeriesSeason.Init();
} }
@ -318,11 +391,13 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); 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){ 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{ } else{
@ -364,11 +439,12 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); 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){ 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.ActiveDownloads--;
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
if (options.History && data.Data != null && data.Data.Count > 0){ if (options.History && data.Data is{ Count: > 0 } && (options.HistoryIncludeCrArtists && data.Music || !data.Music)){
History.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId); History.SetAsDownloaded(data.SeriesId, data.SeasonId, data.Data.First().MediaId);
} }
@ -473,9 +549,9 @@ public class CrunchyrollManager{
? options.DownloadDirPath ? options.DownloadDirPath
: CfgManager.PathVIDEOS_DIR; : 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)){ if (string.IsNullOrEmpty(destinationDirectory)){
Console.WriteLine("Invalid destination directory path."); Console.WriteLine("Invalid destination directory path.");
return; return;
@ -519,7 +595,7 @@ public class CrunchyrollManager{
foreach (var downloadedMedia in subs){ foreach (var downloadedMedia in subs){
var subt = new SubtitleFonts(); var subt = new SubtitleFonts();
subt.Language = downloadedMedia.Language; subt.Language = downloadedMedia.Language;
subt.Fonts = downloadedMedia.Fonts; subt.Fonts = downloadedMedia.Fonts ??[];
subsList.Add(subt); 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){ if (!options.Mp4 && !muxToMp3){
@ -725,7 +803,15 @@ public class CrunchyrollManager{
bool dlVideoOnce = false; bool dlVideoOnce = false;
string fileDir = CfgManager.PathVIDEOS_DIR; 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){ foreach (CrunchyEpMetaData epMeta in data.Data){
Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}"); Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}");
@ -753,10 +839,10 @@ public class CrunchyrollManager{
string mediaGuid = currentMediaId; string mediaGuid = currentMediaId;
if (epMeta.Versions != null){ if (epMeta.Versions != null){
if (epMeta.Lang != 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 }){ } else if (data.SelectedDubs is{ Count: 1 }){
LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]); LanguageItem? lang = Array.Find(Languages.languages, a => a.CrLocale == data.SelectedDubs[0]);
currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang.CrLocale); currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang?.CrLocale) ?? currentVersion;
} else if (epMeta.Versions.Count == 1){ } else if (epMeta.Versions.Count == 1){
currentVersion = epMeta.Versions[0]; currentVersion = epMeta.Versions[0];
} }
@ -772,7 +858,7 @@ public class CrunchyrollManager{
mediaGuid = currentVersion.Guid; mediaGuid = currentVersion.Guid;
if (!isPrimary){ if (!isPrimary){
primaryVersion = epMeta.Versions.Find(a => a.Original); primaryVersion = epMeta.Versions.Find(a => a.Original) ?? currentVersion;
} else{ } else{
primaryVersion = currentVersion; primaryVersion = currentVersion;
} }
@ -816,12 +902,12 @@ public class CrunchyrollManager{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
FileName = "./unknown", 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{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, 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 = 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()); .ToArray());
string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile); string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile);
@ -1207,7 +1294,7 @@ public class CrunchyrollManager{
} else if (options.Novids){ } else if (options.Novids){
Console.WriteLine("Skipping video download..."); Console.WriteLine("Skipping video download...");
} else{ } 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; tsFile = videoDownloadResult.tsFile;
@ -1226,7 +1313,7 @@ public class CrunchyrollManager{
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){ 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; tsFile = audioDownloadResult.tsFile;
@ -1403,7 +1490,7 @@ public class CrunchyrollManager{
videoDownloadMedia = new DownloadedMedia{ videoDownloadMedia = new DownloadedMedia{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s", Path = $"{tsFile}.video.m4s",
Lang = lang.Value, Lang = lang,
IsPrimary = isPrimary IsPrimary = isPrimary
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
@ -1471,7 +1558,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{ files.Add(new DownloadedMedia{
Type = DownloadMediaType.Audio, Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s", Path = $"{tsFile}.audio.m4s",
Lang = lang.Value, Lang = lang,
IsPrimary = isPrimary IsPrimary = isPrimary
}); });
data.downloadedFiles.Add($"{tsFile}.audio.m4s"); data.downloadedFiles.Add($"{tsFile}.audio.m4s");
@ -1487,7 +1574,7 @@ public class CrunchyrollManager{
videoDownloadMedia = new DownloadedMedia{ videoDownloadMedia = new DownloadedMedia{
Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video, Type = syncTimingDownload ? DownloadMediaType.SyncVideo : DownloadMediaType.Video,
Path = $"{tsFile}.video.m4s", Path = $"{tsFile}.video.m4s",
Lang = lang.Value, Lang = lang,
IsPrimary = isPrimary IsPrimary = isPrimary
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
@ -1498,7 +1585,7 @@ public class CrunchyrollManager{
files.Add(new DownloadedMedia{ files.Add(new DownloadedMedia{
Type = DownloadMediaType.Audio, Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s", Path = $"{tsFile}.audio.m4s",
Lang = lang.Value, Lang = lang,
IsPrimary = isPrimary IsPrimary = isPrimary
}); });
data.downloadedFiles.Add($"{tsFile}.audio.m4s"); data.downloadedFiles.Add($"{tsFile}.audio.m4s");
@ -1547,7 +1634,13 @@ public class CrunchyrollManager{
} }
// Finding language by code // 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"){ if (lang.Code == "und"){
Console.Error.WriteLine($"Unable to find language for code {curStream?.AudioLang}"); 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){ 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{ } else{
Console.WriteLine("Subtitles downloading skipped!"); 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){ DownloadedMedia videoDownloadMedia){
if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){ if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[]; 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){ string fileDir){
// Prepare for video download // Prepare for video download
int totalParts = chosenVideoSegments.segments.Count; int totalParts = chosenVideoSegments.segments.Count;
int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize); int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
string mathMsg = $"({mathParts}*{options.Partsize})"; string mathMsg = $"({mathParts}*{options.Partsize})";
string tsFile;
Console.WriteLine($"Total parts in video stream: {totalParts} {mathMsg}"); Console.WriteLine($"Total parts in video stream: {totalParts} {mathMsg}");
if (Path.IsPathRooted(outFile)){ if (Path.IsPathRooted(outFile)){
@ -1843,9 +1937,10 @@ public class CrunchyrollManager{
return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile); 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){ string fileDir){
// Prepare for audio download // Prepare for audio download
string tsFile;
int totalParts = chosenAudioSegments.segments.Count; int totalParts = chosenAudioSegments.segments.Count;
int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize); int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
string mathMsg = $"({mathParts}*{options.Partsize})"; string mathMsg = $"({mathParts}*{options.Partsize})";
@ -1911,7 +2006,7 @@ public class CrunchyrollManager{
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){ if (!playbackRequestResponse.IsOk){
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint); playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint);
} }
if (playbackRequestResponse.IsOk){ if (playbackRequestResponse.IsOk){
@ -1922,7 +2017,7 @@ public class CrunchyrollManager{
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){ if (!playbackRequestResponse.IsOk){
playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, mediaGuidId, playbackEndpoint); playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint);
} }
if (playbackRequestResponse.IsOk){ 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){ private async Task<(bool IsOk, string ResponseContent)> SendPlaybackRequestAsync(string endpoint){
@ -1940,7 +2035,7 @@ public class CrunchyrollManager{
return await HttpClientReq.Instance.SendHttpRequest(request); 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; if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response;
var error = StreamError.FromJson(response.ResponseContent); var error = StreamError.FromJson(response.ResponseContent);
@ -1990,7 +2085,7 @@ public class CrunchyrollManager{
temppbData.Meta = new PlaybackMeta{ temppbData.Meta = new PlaybackMeta{
AudioLocale = playStream.AudioLocale, AudioLocale = playStream.AudioLocale,
Versions = playStream.Versions, Versions = playStream.Versions,
Bifs = new List<string>{ playStream.Bifs }, Bifs = new List<string>{ playStream.Bifs ?? "" },
MediaId = mediaId, MediaId = mediaId,
Captions = playStream.Captions, Captions = playStream.Captions,
Subtitles = new Subtitles() Subtitles = new Subtitles()
@ -2012,7 +2107,7 @@ public class CrunchyrollManager{
#endregion #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 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); var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
@ -2024,11 +2119,11 @@ public class CrunchyrollManager{
try{ try{
JObject jObject = JObject.Parse(showRequestResponse.ResponseContent); 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>(); chapterData.lastUpdate = lastUpdateToken.ToObject<DateTime>();
} }
if (jObject.TryGetValue("mediaId", out JToken mediaIdToken)){ if (jObject.TryGetValue("mediaId", out JToken? mediaIdToken)){
chapterData.mediaId = mediaIdToken.ToObject<string>(); chapterData.mediaId = mediaIdToken.ToObject<string>();
} }
@ -2037,7 +2132,7 @@ public class CrunchyrollManager{
foreach (var property in jObject.Properties()){ foreach (var property in jObject.Properties()){
if (property.Value.Type == JTokenType.Object && property.Name != "lastUpdate" && property.Name != "mediaId"){ if (property.Value.Type == JTokenType.Object && property.Name != "lastUpdate" && property.Name != "mediaId"){
try{ try{
CrunchyChapter chapter = property.Value.ToObject<CrunchyChapter>(); CrunchyChapter chapter = property.Value.ToObject<CrunchyChapter>() ?? new CrunchyChapter();
chapterData.Chapters.Add(chapter); chapterData.Chapters.Add(chapter);
} catch (Exception ex){ } catch (Exception ex){
Console.Error.WriteLine($"Error parsing chapter: {ex.Message}"); Console.Error.WriteLine($"Error parsing chapter: {ex.Message}");
@ -2046,7 +2141,7 @@ public class CrunchyrollManager{
} }
} catch (Exception ex){ } catch (Exception ex){
Console.Error.WriteLine($"Error parsing JSON response: {ex.Message}"); Console.Error.WriteLine($"Error parsing JSON response: {ex.Message}");
return false; return;
} }
if (chapterData.Chapters.Count > 0){ if (chapterData.Chapters.Count > 0){
@ -2098,8 +2193,6 @@ public class CrunchyrollManager{
compiledChapters.Add($"CHAPTER{chapterNumber}NAME={formattedChapterType} End"); compiledChapters.Add($"CHAPTER{chapterNumber}NAME={formattedChapterType} End");
} }
} }
return true;
} }
} else{ } else{
Console.WriteLine("Chapter request failed, attempting old API "); Console.WriteLine("Chapter request failed, attempting old API ");
@ -2109,7 +2202,7 @@ public class CrunchyrollManager{
showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true); showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
if (showRequestResponse.IsOk){ 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); DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@ -2137,13 +2230,10 @@ public class CrunchyrollManager{
chapterNumber = (compiledChapters.Count / 2) + 1; chapterNumber = (compiledChapters.Count / 2) + 1;
compiledChapters.Add($"CHAPTER{chapterNumber}={endFormatted}"); compiledChapters.Add($"CHAPTER{chapterNumber}={endFormatted}");
compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Episode"); compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Episode");
return true; return;
} }
Console.Error.WriteLine("Chapter request failed"); 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 CommunityToolkit.Mvvm.Input;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding; using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.ViewModels; using CRD.ViewModels;
using CRD.ViewModels.Utils; using CRD.ViewModels.Utils;
@ -42,7 +44,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _includeCcSubs; private bool _includeCcSubs;
[ObservableProperty] [ObservableProperty]
private ComboBoxItem _selectedScaledBorderAndShadow; private ComboBoxItem _selectedScaledBorderAndShadow;
@ -77,10 +79,10 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _skipSubMux; private bool _skipSubMux;
[ObservableProperty] [ObservableProperty]
private double? _leadingNumbers; private double? _leadingNumbers;
[ObservableProperty] [ObservableProperty]
private double? _partSize; private double? _partSize;
@ -107,7 +109,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private ComboBoxItem _selectedHSLang; private ComboBoxItem _selectedHSLang;
[ObservableProperty] [ObservableProperty]
private ComboBoxItem _selectedDescriptionLang; private ComboBoxItem _selectedDescriptionLang;
@ -131,81 +133,78 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private ComboBoxItem? _selectedAudioQuality; private ComboBoxItem? _selectedAudioQuality;
[ObservableProperty] [ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedSubLang = new(); private ObservableCollection<ListBoxItem> _selectedSubLang =[];
[ObservableProperty] [ObservableProperty]
private Color _listBoxColor; 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(){ public ObservableCollection<ComboBoxItem> VideoQualityList{ get; } =[
new ComboBoxItem(){ Content = "best" }, new(){ Content = "best" },
new ComboBoxItem(){ Content = "128kB/s" }, new(){ Content = "1080" },
new ComboBoxItem(){ Content = "96kB/s" }, new(){ Content = "720" },
new ComboBoxItem(){ Content = "64kB/s" }, new(){ Content = "480" },
new ComboBoxItem(){ Content = "worst" }, new(){ Content = "360" },
}; new(){ Content = "240" },
new(){ Content = "worst" }
];
public ObservableCollection<ComboBoxItem> HardSubLangList{ get; } = new(){ public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } =[
new ComboBoxItem(){ Content = "none" }, new(){ Content = "best" },
}; new(){ Content = "128kB/s" },
new(){ Content = "96kB/s" },
public ObservableCollection<ComboBoxItem> DescriptionLangList{ get; } = new(){ new(){ Content = "64kB/s" },
new ComboBoxItem(){ Content = "default" }, new(){ Content = "worst" }
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<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(){ public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
new ListBoxItem(){ Content = "all" }, new(){ Content = "all" },
new ListBoxItem(){ Content = "none" }, new(){ Content = "none" }
}; ];
public ObservableCollection<ComboBoxItem> StreamEndpoints{ get; } = new(){ public ObservableCollection<ComboBoxItem> StreamEndpoints{ get; } =[
new ComboBoxItem(){ Content = "web/firefox" }, new(){ Content = "web/firefox" },
new ComboBoxItem(){ Content = "console/switch" }, new(){ Content = "console/switch" },
new ComboBoxItem(){ Content = "console/ps4" }, new(){ Content = "console/ps4" },
new ComboBoxItem(){ Content = "console/ps5" }, new(){ Content = "console/ps5" },
new ComboBoxItem(){ Content = "console/xbox_one" }, new(){ Content = "console/xbox_one" },
new ComboBoxItem(){ Content = "web/edge" }, new(){ Content = "web/edge" },
// new ComboBoxItem(){ Content = "web/safari" }, // new (){ Content = "web/safari" },
new ComboBoxItem(){ Content = "web/chrome" }, new(){ Content = "web/chrome" },
new ComboBoxItem(){ Content = "web/fallback" }, new(){ Content = "web/fallback" },
// new ComboBoxItem(){ Content = "ios/iphone" }, // new (){ Content = "ios/iphone" },
// new ComboBoxItem(){ Content = "ios/ipad" }, // new (){ Content = "ios/ipad" },
new ComboBoxItem(){ Content = "android/phone" }, new(){ Content = "android/phone" },
new ComboBoxItem(){ Content = "tv/samsung" }, new(){ Content = "tv/samsung" }
}; ];
[ObservableProperty] [ObservableProperty]
private bool _isEncodeEnabled; private bool _isEncodeEnabled;
@ -214,8 +213,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
private StringItem _selectedEncodingPreset; private StringItem _selectedEncodingPreset;
public ObservableCollection<StringItem> EncodingPresetsList{ get; } = new(); public ObservableCollection<StringItem> EncodingPresetsList{ get; } = new();
[ObservableProperty] [ObservableProperty]
private bool _cCSubsMuxingFlag; private bool _cCSubsMuxingFlag;
@ -224,11 +223,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _signsSubsAsForced; private bool _signsSubsAsForced;
[ObservableProperty]
private bool _searchFetchFeaturedMusic;
private bool settingsLoaded; private bool settingsLoaded;
public CrunchyrollSettingsViewModel(){ public CrunchyrollSettingsViewModel(){
foreach (var languageItem in Languages.languages){ foreach (var languageItem in Languages.languages){
HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale }); HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale }); SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
@ -244,7 +245,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrDownloadOptions options = CrunchyrollManager.Instance.CrunOptions; 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]; SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null; 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){ foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem); SelectedDubLang.Add(listBoxItem);
} }
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options); SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
@ -301,22 +302,23 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
SkipSubMux = options.SkipSubsMux; SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers; LeadingNumbers = options.Numbers;
FileName = options.FileName; FileName = options.FileName;
SearchFetchFeaturedMusic = options.SearchFetchFeaturedMusic;
ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null; ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null;
SelectedAudioQuality = qualityAudio ?? AudioQualityList[0]; SelectedAudioQuality = qualityAudio ?? AudioQualityList[0];
ComboBoxItem? qualityVideo = VideoQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityVideo) ?? null; ComboBoxItem? qualityVideo = VideoQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityVideo) ?? null;
SelectedVideoQuality = qualityVideo ?? VideoQualityList[0]; SelectedVideoQuality = qualityVideo ?? VideoQualityList[0];
MkvMergeOptions.Clear(); MkvMergeOptions.Clear();
if (options.MkvmergeOptions != null){ if (options.MkvmergeOptions is{ Count: > 0 }){
foreach (var mkvmergeParam in options.MkvmergeOptions){ foreach (var mkvmergeParam in options.MkvmergeOptions){
MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam }); MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam });
} }
} }
FfmpegOptions.Clear(); FfmpegOptions.Clear();
if (options.FfmpegOptions != null){ if (options.FfmpegOptions is{ Count: > 0 }){
foreach (var ffmpegParam in options.FfmpegOptions){ foreach (var ffmpegParam in options.FfmpegOptions){
FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam }); FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam });
} }
@ -341,7 +343,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
if (!settingsLoaded){ if (!settingsLoaded){
return; return;
} }
CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced = SignsSubsAsForced; CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced = SignsSubsAsForced;
CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag = CCSubsMuxingFlag; CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag = CCSubsMuxingFlag;
CrunchyrollManager.Instance.CrunOptions.CcSubsFont = CCSubsFont; CrunchyrollManager.Instance.CrunOptions.CcSubsFont = CCSubsFont;
@ -364,8 +366,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.FileName = FileName; CrunchyrollManager.Instance.CrunOptions.FileName = FileName;
CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs;
CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; 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(); CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
List<string> softSubs = new List<string>(); List<string> softSubs = new List<string>();
@ -378,7 +381,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
string descLang = SelectedDescriptionLang.Content + ""; string descLang = SelectedDescriptionLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale; CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
string hslang = SelectedHSLang.Content + ""; string hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang; 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.QualityAudio = SelectedAudioQuality?.Content + "";
CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + ""; CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + "";
List<string> mkvmergeParams = new List<string>(); List<string> mkvmergeParams = new List<string>();
foreach (var mkvmergeParam in MkvMergeOptions){ foreach (var mkvmergeParam in MkvMergeOptions){
mkvmergeParams.Add(mkvmergeParam.stringValue); mkvmergeParams.Add(mkvmergeParam.stringValue);
@ -413,7 +416,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams; CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams;
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
@ -469,7 +472,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
FfmpegOptions.Remove(param); FfmpegOptions.Remove(param);
RaisePropertyChanged(nameof(FfmpegOptions)); RaisePropertyChanged(nameof(FfmpegOptions));
} }
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){ private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings(); UpdateSettings();
@ -515,7 +518,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.HistoryList =[]; CrunchyrollManager.Instance.HistoryList =[];
} }
} }
} }
[RelayCommand] [RelayCommand]
@ -542,9 +544,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
} }
settingsLoaded = true; 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]; SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
} }
} }
} }

View file

@ -191,6 +191,22 @@
</controls:SettingsExpander> </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" <controls:SettingsExpander Header="Download Settings"
IconSource="Download" IconSource="Download"
Description="Adjust download settings" Description="Adjust download settings"

View file

@ -2,13 +2,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models; using CRD.Utils.Sonarr.Models;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.Views; using CRD.Views;
using DynamicData; using DynamicData;
@ -16,10 +17,10 @@ using ReactiveUI;
namespace CRD.Downloader; namespace CRD.Downloader;
public class History(){ public class History{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; 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); await crunInstance.CrAuth.RefreshToken(true);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", 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); 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){ public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); 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){ private SeriesDataCache? cachedSeries;
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 async Task RefreshSeriesData(string seriesId, HistorySeries historySeries){ private async Task RefreshSeriesData(string seriesId, HistorySeries historySeries){
if (cachedSeries == null || (cachedSeries.Data != null && cachedSeries.Data.First().Id != seriesId)){ if (cachedSeries == null || (!string.IsNullOrEmpty(cachedSeries.SeriesId) && cachedSeries.SeriesId != seriesId)){
cachedSeries = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true); if (historySeries.SeriesType == SeriesType.Series){
} else{ var seriesData = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
if (cachedSeries?.Data != null){ if (seriesData is{ Data: not null }){
var series = cachedSeries.Data.First(); var firstEpisode = seriesData.Data.First();
historySeries.SeriesDescription = series.Description; cachedSeries = new SeriesDataCache{
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries); SeriesDescription = firstEpisode.Description,
historySeries.SeriesTitle = series.Title; SeriesId = seriesId,
historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales); SeriesTitle = firstEpisode.Title,
historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales); 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 var sortedSeriesDates = sortingDir
? CrunchyrollManager.Instance.HistoryList ? CrunchyrollManager.Instance.HistoryList
.OrderByDescending(s => { .OrderByDescending(s => {
var date = ParseDate(s.SonarrNextAirDate, today); var date = ParseDate(s.SonarrNextAirDate ?? string.Empty, today);
return date.HasValue ? date.Value : DateTime.MinValue; return date ?? DateTime.MinValue;
}) })
.ThenByDescending(s => s.SonarrNextAirDate == "Today" ? 1 : 0) .ThenByDescending(s => s.SonarrNextAirDate == "Today" ? 1 : 0)
.ThenBy(s => string.IsNullOrEmpty(s.SonarrNextAirDate) ? 1 : 0) .ThenBy(s => string.IsNullOrEmpty(s.SonarrNextAirDate) ? 1 : 0)
@ -452,8 +471,8 @@ public class History(){
.OrderByDescending(s => s.SonarrNextAirDate == "Today") .OrderByDescending(s => s.SonarrNextAirDate == "Today")
.ThenBy(s => s.SonarrNextAirDate == "Today" ? s.SeriesTitle : null) .ThenBy(s => s.SonarrNextAirDate == "Today" ? s.SeriesTitle : null)
.ThenBy(s => { .ThenBy(s => {
var date = ParseDate(s.SonarrNextAirDate, today); var date = ParseDate(s.SonarrNextAirDate ?? string.Empty, today);
return date.HasValue ? date.Value : DateTime.MaxValue; return date ?? DateTime.MaxValue;
}) })
.ThenBy(s => s.SeriesTitle) .ThenBy(s => s.SeriesTitle)
.ToList(); .ToList();
@ -499,55 +518,40 @@ public class History(){
private string GetSeriesThumbnail(CrSeriesBase series){ private string GetSeriesThumbnail(CrSeriesBase series){
// var series = await crunInstance.CrSeries.SeriesById(seriesId); // var series = await crunInstance.CrSeries.SeriesById(seriesId);
if ((series.Data ?? Array.Empty<SeriesBaseItem>()).First().Images.PosterTall?.Count > 0){ if (series.Data != null && series.Data.First().Images.PosterTall?.Count > 0){
return series.Data.First().Images.PosterTall.First().First(e => e.Height == 360).Source; var imagesPosterTall = series.Data.First().Images.PosterTall;
if (imagesPosterTall != null) return imagesPosterTall.First().First(e => e.Height == 360).Source;
} }
return ""; 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 private HistorySeason NewHistorySeason(List<IHistorySource> episodeList, IHistorySource firstEpisode){
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){
var newSeason = new HistorySeason{ var newSeason = new HistorySeason{
SeasonTitle = firstEpisode.SeasonTitle, SeasonTitle = firstEpisode.GetSeasonTitle(),
SeasonId = firstEpisode.SeasonId, SeasonId = firstEpisode.GetSeasonId(),
SeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "", SeasonNum = firstEpisode.GetSeasonNum(),
EpisodesList =[], EpisodesList =[],
SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier) SpecialSeason = firstEpisode.IsSpecialSeason()
}; };
foreach (var crunchyEpisode in seasonData){ foreach (var historySource in episodeList){
var langList = new List<string>(); if (historySource.GetSeasonId() != newSeason.SeasonId){
continue;
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
} }
Languages.SortListByLangList(langList);
var newHistoryEpisode = new HistoryEpisode{ var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode), EpisodeTitle = historySource.GetEpisodeTitle(),
EpisodeDescription = crunchyEpisode.Description, EpisodeDescription = historySource.GetEpisodeDescription(),
EpisodeId = crunchyEpisode.Id, EpisodeId = historySource.GetEpisodeId(),
Episode = crunchyEpisode.Episode, Episode = historySource.GetEpisodeNumber(),
EpisodeSeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "", EpisodeSeasonNum = historySource.GetSeasonNum(),
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), SpecialEpisode = historySource.IsSpecialEpisode(),
HistoryEpisodeAvailableDubLang = langList, HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales, HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType()
}; };
newSeason.EpisodesList.Add(newHistoryEpisode); newSeason.EpisodesList.Add(newHistoryEpisode);
@ -563,7 +567,7 @@ public class History(){
foreach (var historySeries in crunInstance.HistoryList){ foreach (var historySeries in crunInstance.HistoryList){
if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ if (updateAll || string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle); var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty);
if (sonarrSeries != null){ if (sonarrSeries != null){
historySeries.SonarrSeriesId = sonarrSeries.Id + ""; historySeries.SonarrSeriesId = sonarrSeries.Id + "";
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + ""; historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
@ -581,7 +585,7 @@ public class History(){
} }
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ 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); historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
@ -603,8 +607,10 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode.Id + ""; historyEpisode.SonarrEpisodeId = episode.Id + "";
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + ""; historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode.HasFile; historyEpisode.SonarrHasFile = episode.HasFile;
historyEpisode.SonarrIsMonitored = episode.Monitored;
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
lock (_lock){ lock (_lock){
episodes.Remove(episode); episodes.Remove(episode);
} }
@ -622,8 +628,8 @@ public class History(){
return false; return false;
} }
var episodeNumberStr = ele.EpisodeNumber.ToString() ?? string.Empty; var episodeNumberStr = ele.EpisodeNumber.ToString();
var seasonNumberStr = ele.SeasonNumber.ToString() ?? string.Empty; var seasonNumberStr = ele.SeasonNumber.ToString();
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum; return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
}); });
@ -631,6 +637,7 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode.Id + ""; historyEpisode.SonarrEpisodeId = episode.Id + "";
historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + ""; historyEpisode.SonarrEpisodeNumber = episode.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode.HasFile; historyEpisode.SonarrHasFile = episode.HasFile;
historyEpisode.SonarrIsMonitored = episode.Monitored;
historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + ""; historyEpisode.SonarrSeasonNumber = episode.SeasonNumber + "";
lock (_lock){ lock (_lock){
@ -649,6 +656,7 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode1.Id + ""; historyEpisode.SonarrEpisodeId = episode1.Id + "";
historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + ""; historyEpisode.SonarrEpisodeNumber = episode1.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode1.HasFile; historyEpisode.SonarrHasFile = episode1.HasFile;
historyEpisode.SonarrIsMonitored = episode1.Monitored;
historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrAbsolutNumber = episode1.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + ""; historyEpisode.SonarrSeasonNumber = episode1.SeasonNumber + "";
lock (_lock){ lock (_lock){
@ -666,6 +674,7 @@ public class History(){
historyEpisode.SonarrEpisodeId = episode2.Id + ""; historyEpisode.SonarrEpisodeId = episode2.Id + "";
historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + ""; historyEpisode.SonarrEpisodeNumber = episode2.EpisodeNumber + "";
historyEpisode.SonarrHasFile = episode2.HasFile; historyEpisode.SonarrHasFile = episode2.HasFile;
historyEpisode.SonarrIsMonitored = episode2.Monitored;
historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + ""; historyEpisode.SonarrAbsolutNumber = episode2.AbsoluteEpisodeNumber + "";
historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + ""; historyEpisode.SonarrSeasonNumber = episode2.SeasonNumber + "";
lock (_lock){ lock (_lock){
@ -677,8 +686,6 @@ public class History(){
} }
} }
}); });
} }
} }
@ -706,6 +713,10 @@ public class History(){
} }
private SonarrSeries? FindClosestMatch(string title){ private SonarrSeries? FindClosestMatch(string title){
if (string.IsNullOrEmpty(title)){
return null;
}
SonarrSeries? closestMatch = null; SonarrSeries? closestMatch = null;
double highestSimilarity = 0.0; double highestSimilarity = 0.0;
@ -748,7 +759,7 @@ public class History(){
Parallel.ForEach(episodeList, episode => { Parallel.ForEach(episodeList, episode => {
if (episode != null){ 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 lock (lockObject) // Ensure thread-safe access to shared variables
{ {
if (similarity > highestSimilarity){ if (similarity > highestSimilarity){
@ -810,13 +821,13 @@ public class History(){
} }
public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{ public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
public int Compare(HistoryEpisode x, HistoryEpisode y){ public int Compare(HistoryEpisode? x, HistoryEpisode? y){
if (double.TryParse(x.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) && if (double.TryParse(x?.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double xDouble) &&
double.TryParse(y.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){ double.TryParse(y?.Episode, NumberStyles.Any, CultureInfo.InvariantCulture, out double yDouble)){
return xDouble.CompareTo(yDouble); return xDouble.CompareTo(yDouble);
} }
// Fall back to string comparison if not parseable as doubles // 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,7 +12,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Utils.Updater; using CRD.Utils.Updater;
using FluentAvalonia.Styling; using FluentAvalonia.Styling;
@ -51,10 +49,10 @@ public partial class ProgramManager : ObservableObject{
private bool _updateAvailable = true; private bool _updateAvailable = true;
[ObservableProperty] [ObservableProperty]
private bool _finishedLoading = false; private bool _finishedLoading;
[ObservableProperty] [ObservableProperty]
private bool _navigationLock = false; private bool _navigationLock;
#endregion #endregion
@ -66,7 +64,7 @@ public partial class ProgramManager : ObservableObject{
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>(); private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
private bool exitOnTaskFinish = false; private bool exitOnTaskFinish;
public IStorageProvider StorageProvider; public IStorageProvider StorageProvider;
@ -111,7 +109,7 @@ public partial class ProgramManager : ObservableObject{
await Task.WhenAll(tasks); 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..."); Console.WriteLine("Waiting for downloads to complete...");
await Task.Delay(2000); // Wait for 2 second before checking again 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.Action == NotifyCollectionChangedAction.Remove){
if (e.OldItems != null) if (e.OldItems != null)
foreach (var eOldItem in e.OldItems){ 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){ if (downloadItem != null){
DownloadItemModels.Remove(downloadItem); DownloadItemModels.Remove(downloadItem);
} else{ } 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){ 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); await CrunchyrollManager.Instance.CrAuth.RefreshToken(true);
var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale); var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale);
if (episodeL != null){ 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)); 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; return;
} }
@ -206,6 +210,15 @@ public class QueueManager{
if (musicVideo != null){ if (musicVideo != null){
var musicVideoMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(musicVideo); 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); Queue.Add(musicVideoMeta);
MessageBus.Current.SendMessage(new ToastMessage($"Added music video to the queue", ToastType.Information, 1)); MessageBus.Current.SendMessage(new ToastMessage($"Added music video to the queue", ToastType.Information, 1));
} }
@ -218,6 +231,14 @@ public class QueueManager{
if (concert != null){ if (concert != null){
var concertMeta = CrunchyrollManager.Instance.CrMusic.EpisodeMeta(concert); 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); Queue.Add(concertMeta);
MessageBus.Current.SendMessage(new ToastMessage($"Added concert to the queue", ToastType.Information, 1)); 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()){ foreach (var crunchyEpMeta in selected.Values.ToList()){
if (crunchyEpMeta.Data?.First() != null){ if (crunchyEpMeta.Data?.First() != null){
if (CrunchyrollManager.Instance.CrunOptions.History){ 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 (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true, UseSonarrNumbering: true }){
if (historyEpisode.historyEpisode != null){ if (historyEpisode.historyEpisode != null){
if (!string.IsNullOrEmpty(historyEpisode.historyEpisode.SonarrEpisodeNumber)){ 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.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;

View file

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

View file

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

View file

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

View file

@ -4,21 +4,25 @@ using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.Crunchyroll;
using Newtonsoft.Json; using Newtonsoft.Json;
using YamlDotNet.RepresentationModel; using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
namespace CRD.Utils; namespace CRD.Utils.Files;
public class CfgManager{ public class CfgManager{
private static string WorkingDirectory = AppContext.BaseDirectory; private static string WorkingDirectory = AppContext.BaseDirectory;
public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.yml"); public static readonly string PathCrTokenOld = Path.Combine(WorkingDirectory, "config", "cr_token.yml");
public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.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 PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json");
public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.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){ public static void WriteCrSettings(){
// Convert JSON to an object WriteJsonToFile(PathCrDownloadOptions, CrunchyrollManager.Instance.CrunOptions);
var deserializer = new DeserializerBuilder() }
.WithNamingConvention(UnderscoredNamingConvention.Instance) // Adjust this as needed
.Build();
var jsonObject = deserializer.Deserialize<object>(jsonResponse);
// Convert the object to YAML // public static void WriteTokenToYamlFile(CrToken token, string filePath){
var serializer = new SerializerBuilder() // // Convert the object to YAML
.WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention // var serializer = new SerializerBuilder()
.Build(); // .WithNamingConvention(UnderscoredNamingConvention.Instance) // Ensure consistent naming convention
var yaml = serializer.Serialize(jsonObject); // .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; string dirPath = Path.GetDirectoryName(filePath) ?? string.Empty;
@ -103,74 +125,81 @@ public class CfgManager{
} }
if (!File.Exists(filePath)){ if (!File.Exists(filePath)){
// Create the file if it doesn't exist
using (var fileStream = File.Create(filePath)){ 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; 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){ if (input.Length <= 0){
return; return;
@ -180,17 +209,18 @@ public class CfgManager{
.WithNamingConvention(UnderscoredNamingConvention.Instance) .WithNamingConvention(UnderscoredNamingConvention.Instance)
.IgnoreUnmatchedProperties() .IgnoreUnmatchedProperties()
.Build(); .Build();
var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input); var propertiesPresentInYaml = GetTopLevelPropertiesInYaml(input);
var loadedOptions = deserializer.Deserialize<CrDownloadOptions>(new StringReader(input)); var loadedOptions = deserializer.Deserialize<CrDownloadOptionsYaml>(new StringReader(input));
var instanceOptions = options; var instanceOptions = options;
foreach (PropertyInfo property in typeof(CrDownloadOptions).GetProperties()){ foreach (PropertyInfo property in typeof(CrDownloadOptionsYaml).GetProperties()){
var yamlMemberAttribute = property.GetCustomAttribute<YamlMemberAttribute>(); 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)){ if (propertiesPresentInYaml.Contains(yamlPropertyName)){
PropertyInfo instanceProperty = instanceOptions.GetType().GetProperty(property.Name); PropertyInfo? instanceProperty = instanceOptions.GetType().GetProperty(property.Name);
if (instanceProperty != null && instanceProperty.CanWrite){ if (instanceProperty != null && instanceProperty.CanWrite){
instanceProperty.SetValue(instanceOptions, property.GetValue(loadedOptions)); instanceProperty.SetValue(instanceOptions, property.GetValue(loadedOptions));
} }
@ -216,6 +246,9 @@ public class CfgManager{
return properties; return properties;
} }
#endregion
public static void UpdateHistoryFile(){ public static void UpdateHistoryFile(){
if (!CrunchyrollManager.Instance.CrunOptions.History){ if (!CrunchyrollManager.Instance.CrunOptions.History){
return; return;
@ -308,12 +341,32 @@ public class CfgManager{
return Directory.Exists(dirPath) && File.Exists(filePath); return Directory.Exists(dirPath) && File.Exists(filePath);
} }
public static T DeserializeFromFile<T>(string filePath){ // public static T DeserializeFromFile<T>(string filePath){
var deserializer = new DeserializerBuilder() // var deserializer = new DeserializerBuilder()
.Build(); // .Build();
//
// using (var reader = new StreamReader(filePath)){
// return deserializer.Deserialize<T>(reader);
// }
// }
using (var reader = new StreamReader(filePath)){ public static T? ReadJsonFromFile<T>(string pathToFile) where T : class{
return deserializer.Deserialize<T>(reader); 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 // Check if the file exists and it is not a resume download
if (File.Exists(fn) && !_data.IsResume){ if (File.Exists(fn) && !_data.IsResume){
string rwts = _data.Override ?? "Y"; string rwts = !string.IsNullOrEmpty(_data.Override) ? _data.Override : "Y";
rwts = rwts.ToUpper(); // ?? "N" rwts = rwts.ToUpper(); // ?? "N"
if (rwts.StartsWith("Y")){ if (rwts.StartsWith("Y")){
@ -304,7 +304,7 @@ public class HlsDownloader{
double downloadSpeed = downloadedBytes / (dateElapsed / 1000); double downloadSpeed = downloadedBytes / (dateElapsed / 1000);
int partsLeft = partsTotal - partsDownloaded; int partsLeft = partsTotal - partsDownloaded;
double remainingTime = (partsLeft * (totalDownloadedBytes / partsDownloaded)) / downloadSpeed; double remainingTime = (partsLeft * ((double)totalDownloadedBytes / partsDownloaded)) / downloadSpeed;
return new Info{ return new Info{
Percent = percent, Percent = percent,

View file

@ -15,8 +15,10 @@ using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CRD.Downloader; using CRD.Downloader;
using CRD.Utils.Ffmpeg_Encoding; using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
using CRD.Utils.JsonConv; using CRD.Utils.JsonConv;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using Microsoft.Win32; using Microsoft.Win32;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -747,4 +749,95 @@ public class Helpers{
return false; 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 ApiBeta = "https://beta-api.crunchyroll.com";
public static readonly string ApiN = "https://www.crunchyroll.com"; public static readonly string ApiN = "https://www.crunchyroll.com";
public static readonly string Anilist = "https://graphql.anilist.co"; public static readonly string Anilist = "https://graphql.anilist.co";
public static readonly string Auth = ApiN + "/auth/v1/token"; public static readonly string Auth = ApiN + "/auth/v1/token";
public static readonly string BetaAuth = ApiBeta + "/auth/v1/token"; public static readonly string Profile = ApiN + "/accounts/v1/me/profile";
public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile"; public static readonly string CmsToken = ApiN + "/index/v2";
public static readonly string BetaCmsToken = ApiBeta + "/index/v2"; public static readonly string Search = ApiN + "/content/v2/discover/search";
public static readonly string Search = ApiBeta + "/content/v2/discover/search"; public static readonly string Browse = ApiN + "/content/v2/discover/browse";
public static readonly string Browse = ApiBeta + "/content/v2/discover/browse"; public static readonly string Cms = ApiN + "/content/v2/cms";
public static readonly string Cms = ApiBeta + "/content/v2/cms"; public static readonly string Content = ApiN + "/content/v2";
public static readonly string Content = ApiBeta + "/content/v2";
public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse"; public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse";
public static readonly string BetaCms = ApiBeta + "/cms/v2"; public static readonly string BetaCms = ApiBeta + "/cms/v2";
public static readonly string DRM = ApiBeta + "/drm/v1/auth"; public static readonly string DRM = ApiBeta + "/drm/v1/auth";
public static readonly string Subscription = ApiBeta + "/subs/v3/subscriptions/"; public static readonly string Subscription = ApiN + "/subs/v3/subscriptions/";
public static readonly string CmsN = ApiN + "/content/v2/cms";
public static readonly string authBasic = "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6"; 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 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 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.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
namespace CRD.Utils.Muxing; namespace CRD.Utils.Muxing;

View file

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

View file

@ -1,10 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -16,7 +17,7 @@ namespace CRD.Utils.Muxing;
public class SyncingHelper{ public class SyncingHelper{
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){ public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
var ffmpegPath = CfgManager.PathFFMPEG; 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 = ""; var output = "";
@ -68,7 +69,7 @@ public class SyncingHelper{
return 0; 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 mean1 = pixels1.Average();
double mean2 = pixels2.Average(); double mean2 = pixels2.Average();
@ -110,7 +111,7 @@ public class SyncingHelper{
return pixels; 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 image1 = Image.Load<Rgba32>(imagePath1))
using (var image2 = Image.Load<Rgba32>(imagePath2)){ using (var image2 = Image.Load<Rgba32>(imagePath2)){
// Preprocess images (resize and convert to grayscale) // 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 // Check if any frame is completely black, if so, skip SSIM calculation
if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2)){ if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2)){
// Return a negative value or zero to indicate no SSIM comparison for black frames. // Return a negative value or zero to indicate no SSIM comparison for black frames.
return -1.0; return (-1.0,99);
} }
// Compute SSIM // 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){ private static bool IsBlackFrame(float[] pixels, float threshold = 1.0f){
// Check if all pixel values are below the threshold, indicating a black frame. // 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){ 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}"); // 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){ 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){ foreach (var baseFrame in baseFrames){
var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold)); var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold));
if (matchingFrame != null){ 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; return baseFrame.Time - matchingFrame.Time;
} else{ } else{
// Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}"); // Console.WriteLine($"No Match Found for Base Frame Time: {baseFrame.Time}");

View file

@ -87,7 +87,7 @@ public static class MPDParser{
throw new NotImplementedException(); 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); LanguageItem? audioLang = item.language != null ? foundLanguage : (language != null ? language : foundLanguage);
var pItem = new AudioPlaylist{ var pItem = new AudioPlaylist{

View file

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

View file

@ -1,13 +1,261 @@
using System.Collections.Generic; using System.Collections.Generic;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.ViewModels; using CRD.ViewModels;
using Newtonsoft.Json;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs.Crunchyroll;
public class CrDownloadOptions{ public class CrDownloadOptions{
#region General Settings #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)] [YamlMember(Alias = "auto_download", ApplyNamingConventions = false)]
public bool AutoDownload{ get; set; } public bool AutoDownload{ get; set; }
@ -21,13 +269,13 @@ public class CrDownloadOptions{
public int FsRetryTime{ get; set; } public int FsRetryTime{ get; set; }
[YamlIgnore] [YamlIgnore]
public string Force{ get; set; } public string Force{ get; set; } = "";
[YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)] [YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)]
public int SimultaneousDownloads{ get; set; } public int SimultaneousDownloads{ get; set; }
[YamlMember(Alias = "theme", ApplyNamingConventions = false)] [YamlMember(Alias = "theme", ApplyNamingConventions = false)]
public string Theme{ get; set; } public string Theme{ get; set; } = "";
[YamlMember(Alias = "accent_color", ApplyNamingConventions = false)] [YamlMember(Alias = "accent_color", ApplyNamingConventions = false)]
public string? AccentColor{ get; set; } public string? AccentColor{ get; set; }
@ -43,23 +291,29 @@ public class CrDownloadOptions{
[YamlIgnore] [YamlIgnore]
public List<string> Override{ get; set; } public List<string> Override{ get; } =[];
[YamlIgnore] [YamlIgnore]
public string CcTag{ get; set; } public string CcTag{ get; set; } = "";
[YamlIgnore] [YamlIgnore]
public bool Nocleanup{ get; set; } public bool Nocleanup{ get; } = false;
[YamlMember(Alias = "history", ApplyNamingConventions = false)] [YamlMember(Alias = "history", ApplyNamingConventions = false)]
public bool History{ get; set; } public bool History{ get; set; }
[YamlMember(Alias = "history_include_cr_artists", ApplyNamingConventions = false)]
public bool HistoryIncludeCrArtists{ get; set; }
[YamlMember(Alias = "history_lang", ApplyNamingConventions = false)] [YamlMember(Alias = "history_lang", ApplyNamingConventions = false)]
public string? HistoryLang{ get; set; } public string? HistoryLang{ get; set; }
[YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)] [YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)]
public bool HistoryAddSpecials{ get; set; } public bool HistoryAddSpecials{ get; set; }
[YamlMember(Alias = "history_skip_unmonitored", ApplyNamingConventions = false)]
public bool HistorySkipUnmonitored{ get; set; }
[YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)] [YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)]
public bool HistoryCountSonarr{ get; set; } public bool HistoryCountSonarr{ get; set; }
@ -80,7 +334,7 @@ public class CrDownloadOptions{
[YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)] [YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
public HistoryPageProperties? HistoryPageProperties{ get; set; } public HistoryPageProperties? HistoryPageProperties{ get; set; }
[YamlMember(Alias = "seasons_page_properties", ApplyNamingConventions = false)] [YamlMember(Alias = "seasons_page_properties", ApplyNamingConventions = false)]
public SeasonsPageProperties? SeasonsPageProperties{ get; set; } public SeasonsPageProperties? SeasonsPageProperties{ get; set; }
@ -89,7 +343,7 @@ public class CrDownloadOptions{
[YamlMember(Alias = "proxy_enabled", ApplyNamingConventions = false)] [YamlMember(Alias = "proxy_enabled", ApplyNamingConventions = false)]
public bool ProxyEnabled{ get; set; } public bool ProxyEnabled{ get; set; }
[YamlMember(Alias = "proxy_socks", ApplyNamingConventions = false)] [YamlMember(Alias = "proxy_socks", ApplyNamingConventions = false)]
public bool ProxySocks{ get; set; } public bool ProxySocks{ get; set; }
@ -98,20 +352,20 @@ public class CrDownloadOptions{
[YamlMember(Alias = "proxy_port", ApplyNamingConventions = false)] [YamlMember(Alias = "proxy_port", ApplyNamingConventions = false)]
public int ProxyPort{ get; set; } public int ProxyPort{ get; set; }
[YamlMember(Alias = "proxy_username", ApplyNamingConventions = false)] [YamlMember(Alias = "proxy_username", ApplyNamingConventions = false)]
public string? ProxyUsername{ get; set; } public string? ProxyUsername{ get; set; }
[YamlMember(Alias = "proxy_password", ApplyNamingConventions = false)] [YamlMember(Alias = "proxy_password", ApplyNamingConventions = false)]
public string? ProxyPassword{ get; set; } public string? ProxyPassword{ get; set; }
#endregion #endregion
#region Crunchyroll Settings #region Crunchyroll Settings
[YamlMember(Alias = "hard_sub_lang", ApplyNamingConventions = false)] [YamlMember(Alias = "hard_sub_lang", ApplyNamingConventions = false)]
public string Hslang{ get; set; } public string Hslang{ get; set; } = "";
[YamlIgnore] [YamlIgnore]
public int Kstream{ get; set; } public int Kstream{ get; set; }
@ -126,23 +380,23 @@ public class CrDownloadOptions{
public int StreamServer{ get; set; } public int StreamServer{ get; set; }
[YamlMember(Alias = "quality_video", ApplyNamingConventions = false)] [YamlMember(Alias = "quality_video", ApplyNamingConventions = false)]
public string QualityVideo{ get; set; } public string QualityVideo{ get; set; } = "";
[YamlMember(Alias = "quality_audio", ApplyNamingConventions = false)] [YamlMember(Alias = "quality_audio", ApplyNamingConventions = false)]
public string QualityAudio{ get; set; } public string QualityAudio{ get; set; } = "";
[YamlMember(Alias = "file_name", ApplyNamingConventions = false)] [YamlMember(Alias = "file_name", ApplyNamingConventions = false)]
public string FileName{ get; set; } public string FileName{ get; set; } = "";
[YamlMember(Alias = "leading_numbers", ApplyNamingConventions = false)] [YamlMember(Alias = "leading_numbers", ApplyNamingConventions = false)]
public int Numbers{ get; set; } public int Numbers{ get; set; }
[YamlIgnore] [YamlMember(Alias = "download_part_size", ApplyNamingConventions = false)]
public int Partsize{ get; set; } public int Partsize{ get; set; }
[YamlMember(Alias = "soft_subs", ApplyNamingConventions = false)] [YamlMember(Alias = "soft_subs", ApplyNamingConventions = false)]
public List<string> DlSubs{ get; set; } public List<string> DlSubs{ get; set; } =[];
[YamlIgnore] [YamlIgnore]
public bool SkipSubs{ get; set; } public bool SkipSubs{ get; set; }
@ -181,13 +435,13 @@ public class CrDownloadOptions{
public string? DescriptionLang{ get; set; } public string? DescriptionLang{ get; set; }
[YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)]
public List<string> FfmpegOptions{ get; set; } public List<string> FfmpegOptions{ get; set; } =[];
[YamlMember(Alias = "mux_mkvmerge", ApplyNamingConventions = false)] [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)] [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)] [YamlMember(Alias = "mux_default_sub_signs", ApplyNamingConventions = false)]
public bool DefaultSubSigns{ get; set; } public bool DefaultSubSigns{ get; set; }
@ -196,7 +450,7 @@ public class CrDownloadOptions{
public bool DefaultSubForcedDisplay{ get; set; } public bool DefaultSubForcedDisplay{ get; set; }
[YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)]
public string DefaultAudio{ get; set; } public string DefaultAudio{ get; set; } = "";
[YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)] [YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)]
public bool DlVideoOnce{ get; set; } public bool DlVideoOnce{ get; set; }
@ -220,8 +474,7 @@ public class CrDownloadOptions{
public bool Chapters{ get; set; } public bool Chapters{ get; set; }
[YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)] [YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)]
public List<string> DubLang{ get; set; } public List<string> DubLang{ get; set; } =[];
[YamlMember(Alias = "calendar_language", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_language", ApplyNamingConventions = false)]
public string? SelectedCalendarLanguage{ get; set; } public string? SelectedCalendarLanguage{ get; set; }
@ -237,12 +490,15 @@ public class CrDownloadOptions{
[YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)]
public bool CalendarFilterByAirDate{ get; set; } public bool CalendarFilterByAirDate{ get; set; }
[YamlMember(Alias = "calendar_show_upcoming_episodes", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_show_upcoming_episodes", ApplyNamingConventions = false)]
public bool CalendarShowUpcomingEpisodes{ get; set; } public bool CalendarShowUpcomingEpisodes{ get; set; }
[YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)] [YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)]
public string? StreamEndpoint{ get; set; } public string? StreamEndpoint{ get; set; }
[YamlMember(Alias = "search_fetch_featured_music", ApplyNamingConventions = false)]
public bool SearchFetchFeaturedMusic{ get; set; }
#endregion #endregion
} }

View file

@ -4,7 +4,7 @@ using Newtonsoft.Json;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs;
public struct CrunchyMovieList{ public class CrunchyMovieList{
public int Total{ get; set; } public int Total{ get; set; }
public List<CrunchyMovie>? Data{ get; set; } public List<CrunchyMovie>? Data{ get; set; }
public Meta Meta{ 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")] [JsonProperty("audio_locale")]
public Locale? AudioLocale{ get; set; } public Locale? AudioLocale{ get; set; }

View file

@ -1,16 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using CRD.Utils.Structs.History;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs;
public struct CrunchyEpisodeList{ public class CrunchyEpisodeList{
public int Total{ get; set; } public int Total{ get; set; }
public List<CrunchyEpisode>? Data{ get; set; } public List<CrunchyEpisode>? Data{ get; set; }
public Meta Meta{ get; set; } public Meta Meta{ get; set; }
} }
public struct CrunchyEpisode{ public class CrunchyEpisode : IHistorySource{
[JsonProperty("next_episode_id")] [JsonProperty("next_episode_id")]
public string NextEpisodeId{ get; set; } public string NextEpisodeId{ get; set; }
@ -81,7 +84,7 @@ public struct CrunchyEpisode{
[JsonProperty("audio_locale")] [JsonProperty("audio_locale")]
public string AudioLocale{ get; set; } public string AudioLocale{ get; set; }
public string Id{ get; set; } public required string Id{ get; set; }
[JsonProperty("media_type")] [JsonProperty("media_type")]
public string? MediaType{ get; set; } public string? MediaType{ get; set; }
@ -173,9 +176,123 @@ public struct CrunchyEpisode{
[JsonProperty("__links__")] [JsonProperty("__links__")]
public Links? Links{ get; set; } 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")] [JsonProperty("poster_tall")]
public List<List<Image>>? PosterTall{ get; set; } public List<List<Image>>? PosterTall{ get; set; }
@ -188,14 +305,14 @@ public struct Images{
public List<List<Image>>? Thumbnail{ get; set; } public List<List<Image>>? Thumbnail{ get; set; }
} }
public struct Image{ public class Image{
public int Height{ get; set; } public int Height{ get; set; }
public string Source{ get; set; } public string Source{ get; set; }
public ImageType Type{ get; set; } public ImageType Type{ get; set; }
public int Width{ get; set; } public int Width{ get; set; }
} }
public struct EpisodeVersion{ public class EpisodeVersion{
[JsonProperty("audio_locale")] [JsonProperty("audio_locale")]
public string AudioLocale{ get; set; } public string AudioLocale{ get; set; }
@ -215,11 +332,11 @@ public struct EpisodeVersion{
public string Variant{ get; set; } public string Variant{ get; set; }
} }
public struct Link{ public class Link{
public string Href{ get; set; } public string Href{ get; set; }
} }
public struct Links(){ public class Links(){
public Dictionary<string, Link> LinkMappings{ get; set; } = new(){ public Dictionary<string, Link> LinkMappings{ get; set; } = new(){
{ "episode/channel", default }, { "episode/channel", default },
{ "episode/next_episode", default }, { "episode/next_episode", default },
@ -230,7 +347,7 @@ public struct Links(){
} }
public class CrunchyEpMeta{ public class CrunchyEpMeta{
public List<CrunchyEpMetaData>? Data{ get; set; } public List<CrunchyEpMetaData> Data{ get; set; } =[];
public string? SeriesTitle{ get; set; } public string? SeriesTitle{ get; set; }
public string? SeasonTitle{ get; set; } public string? SeasonTitle{ get; set; }
@ -239,11 +356,11 @@ public class CrunchyEpMeta{
public string? Description{ get; set; } public string? Description{ get; set; }
public string? SeasonId{ get; set; } public string? SeasonId{ get; set; }
public string? Season{ get; set; } public string? Season{ get; set; }
public string? ShowId{ get; set; } public string? SeriesId{ get; set; }
public string? AbsolutEpisodeNumberE{ get; set; } public string? AbsolutEpisodeNumberE{ get; set; }
public string? Image{ get; set; } public string? Image{ get; set; }
public bool Paused{ get; set; } public bool Paused{ get; set; }
public DownloadProgress? DownloadProgress{ get; set; } public DownloadProgress DownloadProgress{ get; set; } = new();
public List<string>? SelectedDubs{ get; set; } public List<string>? SelectedDubs{ get; set; }
@ -259,7 +376,7 @@ public class CrunchyEpMeta{
public string Resolution{ get; set; } public string Resolution{ get; set; }
public List<string> downloadedFiles{ get; set; } =[]; public List<string> downloadedFiles{ get; set; } =[];
public bool OnlySubs{ get; set; } public bool OnlySubs{ get; set; }
} }
@ -274,7 +391,7 @@ public class DownloadProgress{
public double DownloadSpeed{ get; set; } public double DownloadSpeed{ get; set; }
} }
public struct CrunchyEpMetaData{ public class CrunchyEpMetaData{
public string MediaId{ get; set; } public string MediaId{ get; set; }
public LanguageItem? Lang{ get; set; } public LanguageItem? Lang{ get; set; }
public string? Playback{ get; set; } public string? Playback{ get; set; }
@ -283,7 +400,7 @@ public struct CrunchyEpMetaData{
public bool IsDubbed{ get; set; } public bool IsDubbed{ get; set; }
} }
public struct CrunchyRollEpisodeData{ public class CrunchyRollEpisodeData{
public string Key{ get; set; } public string Key{ get; set; }
public EpisodeAndLanguage EpisodeAndLanguages{ 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;
using System.Collections.Generic; using System.Collections.Generic;
using CRD.Utils.Structs.History;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace CRD.Utils.Structs.Crunchyroll.Music; namespace CRD.Utils.Structs.Crunchyroll.Music;
public struct CrunchyMusicVideoList{ public class CrunchyMusicVideoList{
public int Total{ get; set; } public int Total{ get; set; }
public List<CrunchyMusicVideo>? Data{ get; set; } public List<CrunchyMusicVideo> Data{ get; set; } =[];
public Meta Meta{ get; set; } public Meta? Meta{ get; set; }
} }
public class CrunchyMusicVideo{ public class CrunchyMusicVideo : IHistorySource{
[JsonProperty("copyright")] [JsonProperty("copyright")]
public string? Copyright{ get; set; } public string? Copyright{ get; set; }
[JsonProperty("hash")] [JsonProperty("hash")]
public string? Hash{ get; set; } public string? Hash{ get; set; }
[JsonProperty("availability")] [JsonProperty("availability")]
public MusicVideoAvailability? Availability{ get; set; } public MusicVideoAvailability? Availability{ get; set; }
[JsonProperty("isMature")] [JsonProperty("isMature")]
public bool IsMature{ get; set; } public bool IsMature{ get; set; }
[JsonProperty("maturityRatings")] [JsonProperty("maturityRatings")]
public object? MaturityRatings{ get; set; } public object? MaturityRatings{ get; set; }
[JsonProperty("title")] [JsonProperty("title")]
public string? Title{ get; set; } public string? Title{ get; set; }
[JsonProperty("artists")] [JsonProperty("artists")]
public object? Artists{ get; set; } public object? Artists{ get; set; }
[JsonProperty("displayArtistNameRequired")] [JsonProperty("displayArtistNameRequired")]
public bool DisplayArtistNameRequired{ get; set; } public bool DisplayArtistNameRequired{ get; set; }
[JsonProperty("streams_link")] [JsonProperty("streams_link")]
public string? StreamsLink{ get; set; } public string? StreamsLink{ get; set; }
[JsonProperty("matureBlocked")] [JsonProperty("matureBlocked")]
public bool MatureBlocked{ get; set; } public bool MatureBlocked{ get; set; }
[JsonProperty("originalRelease")] [JsonProperty("originalRelease")]
public DateTime OriginalRelease{ get; set; } public DateTime OriginalRelease{ get; set; }
[JsonProperty("sequenceNumber")] [JsonProperty("sequenceNumber")]
public int SequenceNumber{ get; set; } public int SequenceNumber{ get; set; }
[JsonProperty("type")] [JsonProperty("type")]
public string? Type{ get; set; } public string? Type{ get; set; }
[JsonProperty("animeIds")] [JsonProperty("animeIds")]
public List<string>? AnimeIds{ get; set; } public List<string>? AnimeIds{ get; set; }
[JsonProperty("description")] [JsonProperty("description")]
public string? Description{ get; set; } public string? Description{ get; set; }
[JsonProperty("durationMs")] [JsonProperty("durationMs")]
public int DurationMs{ get; set; } public int DurationMs{ get; set; }
[JsonProperty("licensor")] [JsonProperty("licensor")]
public string? Licensor{ get; set; } public string? Licensor{ get; set; }
[JsonProperty("slug")] [JsonProperty("slug")]
public string? Slug{ get; set; } public string? Slug{ get; set; }
[JsonProperty("artist")] [JsonProperty("artist")]
public MusicVideoArtist? Artist{ get; set; } public required MusicVideoArtist Artist{ get; set; }
[JsonProperty("isPremiumOnly")] [JsonProperty("isPremiumOnly")]
public bool IsPremiumOnly{ get; set; } public bool IsPremiumOnly{ get; set; }
[JsonProperty("isPublic")] [JsonProperty("isPublic")]
public bool IsPublic{ get; set; } public bool IsPublic{ get; set; }
[JsonProperty("publishDate")] [JsonProperty("publishDate")]
public DateTime PublishDate{ get; set; } public DateTime PublishDate{ get; set; }
[JsonProperty("displayArtistName")] [JsonProperty("displayArtistName")]
public string? DisplayArtistName{ get; set; } public string? DisplayArtistName{ get; set; }
[JsonProperty("genres")] [JsonProperty("genres")]
public object? genres{ get; set; } public object? Genres{ get; set; }
[JsonProperty("readyToPublish")] [JsonProperty("readyToPublish")]
public bool ReadyToPublish{ get; set; } public bool ReadyToPublish{ get; set; }
[JsonProperty("id")] [JsonProperty("id")]
public string? Id{ get; set; } public required string Id{ get; set; }
[JsonProperty("createdAt")] [JsonProperty("createdAt")]
public DateTime CreatedAt{ get; set; } public DateTime CreatedAt{ get; set; }
public MusicImages? Images{ get; set; } public MusicImages? Images{ get; set; }
[JsonProperty("updatedAt")] [JsonProperty("updatedAt")]
public DateTime UpdatedAt{ get; set; } 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")] [JsonProperty("poster_tall")]
public List<Image>? PosterTall{ get; set; } public List<Image> PosterTall{ get; set; } =[];
[JsonProperty("poster_wide")] [JsonProperty("poster_wide")]
public List<Image>? PosterWide{ get; set; } public List<Image> PosterWide{ get; set; } =[];
[JsonProperty("promo_image")] [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")] [JsonProperty("id")]
public string? Id{ get; set; } public required string Id{ get; set; }
[JsonProperty("name")] [JsonProperty("name")]
public string? Name{ get; set; } public required string Name{ get; set; }
[JsonProperty("slug")] [JsonProperty("slug")]
public string? Slug{ get; set; } public string? Slug{ get; set; }
} }
public struct MusicVideoAvailability{ public class MusicVideoAvailability{
[JsonProperty("endDate")] [JsonProperty("endDate")]
public DateTime EndDate{ get; set; } public DateTime EndDate{ get; set; }
[JsonProperty("startDate")] [JsonProperty("startDate")]
public DateTime StartDate{ get; set; } public DateTime StartDate{ get; set; }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
@ -8,11 +8,18 @@ using System.Threading.Tasks;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.CustomList; using CRD.Utils.CustomList;
using CRD.Utils.Files;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace CRD.Utils.Structs.History; namespace CRD.Utils.Structs.History;
public class HistorySeries : INotifyPropertyChanged{ 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")] [JsonProperty("series_title")]
public string? SeriesTitle{ get; set; } public string? SeriesTitle{ get; set; }
@ -96,7 +103,7 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonIgnore] [JsonIgnore]
private bool Loading = false; private bool Loading = false;
[JsonIgnore] [JsonIgnore]
public StringItem? _selectedVideoQualityItem; public StringItem? _selectedVideoQualityItem;
@ -111,7 +118,6 @@ public class HistorySeries : INotifyPropertyChanged{
if (!Loading){ if (!Loading){
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
} }
} }
} }
@ -230,16 +236,48 @@ public class HistorySeries : INotifyPropertyChanged{
int count = 0; int count = 0;
bool foundWatched = false; bool foundWatched = false;
var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; 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--){ for (int i = Seasons.Count - 1; i >= 0; i--){
var season = Seasons[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; var episodesList = season.EpisodesList;
for (int j = episodesList.Count - 1; j >= 0; j--){ for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[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){ if (!string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile){
count++; count++;
} }
@ -253,6 +291,10 @@ public class HistorySeries : INotifyPropertyChanged{
if (historyAddSpecials){ if (historyAddSpecials){
var episodes = season.EpisodesList; var episodes = season.EpisodesList;
for (int j = episodes.Count - 1; j >= 0; j--){ for (int j = episodes.Count - 1; j >= 0; j--){
if (sonarrEnabled && sonarrSkipUnmonitored && !episodes[j].SonarrIsMonitored){
continue;
}
if (!episodes[j].WasDownloaded){ if (!episodes[j].WasDownloaded){
count++; count++;
} }
@ -266,6 +308,10 @@ public class HistorySeries : INotifyPropertyChanged{
for (int j = episodesList.Count - 1; j >= 0; j--){ for (int j = episodesList.Count - 1; j >= 0; j--){
var episode = episodesList[j]; var episode = episodesList[j];
if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
continue;
}
if (episode.SpecialEpisode){ if (episode.SpecialEpisode){
if (historyAddSpecials && !episode.WasDownloaded){ if (historyAddSpecials && !episode.WasDownloaded){
count++; count++;
@ -303,47 +349,103 @@ public class HistorySeries : INotifyPropertyChanged{
public async Task AddNewMissingToDownloads(){ public async Task AddNewMissingToDownloads(){
bool foundWatched = false; bool foundWatched = false;
var historyAddSpecials = CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials; 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 sonarrSkipUnmonitored = CrunchyrollManager.Instance.CrunOptions.HistorySkipUnmonitored;
var season = Seasons[i];
if (season.SpecialSeason == true){ if (sonarrEnabled && CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr){
if (historyAddSpecials){ for (int i = Seasons.Count - 1; i >= 0; i--){
var episodes = season.EpisodesList; var season = Seasons[i];
for (int j = episodes.Count - 1; j >= 0; j--){
if (!episodes[j].WasDownloaded){ if (season.SpecialSeason == true){
await Seasons[i].EpisodesList[j].DownloadEpisode(); 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; continue;
} }
if (!episode.WasDownloaded && !foundWatched){ var episodesList = season.EpisodesList;
await Seasons[i].EpisodesList[j].DownloadEpisode(); for (int j = episodesList.Count - 1; j >= 0; j--){
} else{ var episode = episodesList[j];
foundWatched = true;
if (!historyAddSpecials){ if (sonarrEnabled && sonarrSkipUnmonitored && !episode.SonarrIsMonitored){
break; 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){ if (season.SpecialSeason == true){
break; 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}"); Console.WriteLine($"Fetching Data for: {SeriesTitle}");
FetchingData = true; FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
try{
await CrunchyrollManager.Instance.History.CRUpdateSeries(SeriesId, seasonId); switch (SeriesType){
} catch (Exception e){ case SeriesType.Artist:
Console.Error.WriteLine("Failed to update History series"); try{
Console.Error.WriteLine(e); 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(SeriesTitle)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesDescription))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesDescription)));
UpdateNewEpisodes(); UpdateNewEpisodes();
@ -384,6 +505,15 @@ public class HistorySeries : INotifyPropertyChanged{
} }
public void OpenCrPage(){ 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) else if (yExists)
return 1; // y comes before any missing value return 1; // y comes before any missing value
else 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; return langList;
@ -116,8 +116,8 @@ public class Languages{
} }
public static LanguageItem FindLang(string crLocale){ public static LanguageItem FindLang(string crLocale){
LanguageItem lang = languages.FirstOrDefault(l => l.CrLocale == crLocale); LanguageItem? lang = languages.FirstOrDefault(l => l.CrLocale == crLocale);
if (lang.CrLocale != null){ if (lang?.CrLocale != null){
return lang; return lang;
} else{ } else{
return new LanguageItem{ return new LanguageItem{
@ -159,7 +159,7 @@ public class Languages{
var property = typeof(T).GetProperty(sortKey, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); 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}'."); 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; int index = idx.ContainsKey(value) ? idx[value] : 50;
return index; return index;
}).ToList(); }).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.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Utils.Files;
namespace CRD.Utils.Updater; namespace CRD.Utils.Updater;

View file

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

View file

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

View file

@ -2,21 +2,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using DynamicData; using DynamicData;
using DynamicData.Kernel;
using Newtonsoft.Json;
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -70,9 +64,10 @@ public partial class CalendarPageViewModel : ViewModelBase{
private CalendarWeek? currentWeek; private CalendarWeek? currentWeek;
private bool loading = true; private bool loading;
public CalendarPageViewModel(){ public CalendarPageViewModel(){
loading = true;
CalendarDays = new ObservableCollection<CalendarDay>(); CalendarDays = new ObservableCollection<CalendarDay>();
foreach (var languageItem in Languages.languages){ foreach (var languageItem in Languages.languages){
@ -138,7 +133,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
currentWeek = week; currentWeek = week;
CalendarDays.Clear(); CalendarDays.Clear();
CalendarDays.AddRange(week.CalendarDays); if (week.CalendarDays != null) CalendarDays.AddRange(week.CalendarDays);
RaisePropertyChanged(nameof(CalendarDays)); RaisePropertyChanged(nameof(CalendarDays));
ShowLoading = false; ShowLoading = false;
if (CustomCalendar){ if (CustomCalendar){
@ -146,9 +141,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){ foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
if (calendarDayCalendarEpisode.ImageBitmap == null){ if (calendarDayCalendarEpisode.ImageBitmap == null){
if (calendarDayCalendarEpisode.AnilistEpisode){ if (calendarDayCalendarEpisode.AnilistEpisode){
calendarDayCalendarEpisode.LoadImage(100,150); _ = calendarDayCalendarEpisode.LoadImage(100,150);
} else{ } else{
calendarDayCalendarEpisode.LoadImage(); _ = calendarDayCalendarEpisode.LoadImage();
} }
} }
@ -165,9 +160,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
if (calendarDayCalendarEpisode.ImageBitmap == null){ if (calendarDayCalendarEpisode.ImageBitmap == null){
if (calendarDayCalendarEpisode.AnilistEpisode){ if (calendarDayCalendarEpisode.AnilistEpisode){
calendarDayCalendarEpisode.LoadImage(100,150); _ = calendarDayCalendarEpisode.LoadImage(100,150);
} else{ } else{
calendarDayCalendarEpisode.LoadImage(); _ = calendarDayCalendarEpisode.LoadImage();
} }
} }
} }
@ -263,7 +258,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
if (value?.Content != null){ if (value?.Content != null){
CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage = value.Content.ToString(); CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage = value.Content.ToString();
Refresh(); Refresh();
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
} }
@ -276,7 +271,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true); LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true);
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
partial void OnHideDubsChanged(bool value){ partial void OnHideDubsChanged(bool value){
@ -285,7 +280,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
} }
CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs = value; CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs = value;
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
partial void OnFilterByAirDateChanged(bool value){ partial void OnFilterByAirDateChanged(bool value){
@ -294,7 +289,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
} }
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value; CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
partial void OnShowUpcomingEpisodesChanged(bool value){ partial void OnShowUpcomingEpisodesChanged(bool value){
@ -303,7 +298,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
} }
CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes = value; CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes = value;
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
partial void OnCurrentCalendarDubFilterChanged(ComboBoxItem? value){ partial void OnCurrentCalendarDubFilterChanged(ComboBoxItem? value){
@ -313,7 +308,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
if (!string.IsNullOrEmpty(value?.Content + "")){ if (!string.IsNullOrEmpty(value?.Content + "")){
CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter = 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;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -36,12 +38,12 @@ public partial class DownloadsPageViewModel : ViewModelBase{
QueueManager.Instance.UpdateDownloadListItems(); QueueManager.Instance.UpdateDownloadListItems();
} }
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
partial void OnRemoveFinishedChanged(bool value){ partial void OnRemoveFinishedChanged(bool value){
CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload = value; CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload = value;
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
} }
@ -67,9 +69,10 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
public DownloadItemModel(CrunchyEpMeta epMetaF){ public DownloadItemModel(CrunchyEpMeta epMetaF){
epMeta = 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) : "") + " - " + Title = epMeta.SeriesTitle + (!string.IsNullOrEmpty(epMeta.Season) ? " - S" + epMeta.Season + "E" + (epMeta.EpisodeNumber != string.Empty ? epMeta.EpisodeNumber : epMeta.AbsolutEpisodeNumberE) : "") + " - " +
epMeta.EpisodeTitle; epMeta.EpisodeTitle;
isDownloading = epMeta.DownloadProgress.IsDownloading || Done; isDownloading = epMeta.DownloadProgress.IsDownloading || Done;
Done = epMeta.DownloadProgress.Done; Done = epMeta.DownloadProgress.Done;
@ -81,11 +84,19 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") : Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting"; 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; Error = epMeta.DownloadProgress.Error;
} }
string JoinWithSeparator(params string[] parts){
return string.Join(" - ", parts.Where(part => !string.IsNullOrEmpty(part)));
}
private string GetDubString(){ private string GetDubString(){
if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){ if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){
return ""; return "";
@ -138,10 +149,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") : Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting"; 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; Error = epMeta.DownloadProgress.Error;
if (PropertyChanged != null){ if (PropertyChanged != null){
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(isDownloading)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Percent))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Percent)));
@ -186,15 +202,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
epMeta.DownloadProgress.IsDownloading = true; epMeta.DownloadProgress.IsDownloading = true;
Paused = !epMeta.Paused && !isDownloading || epMeta.Paused; Paused = !epMeta.Paused && !isDownloading || epMeta.Paused;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
CrDownloadOptions newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions); CrDownloadOptions newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
if (epMeta.OnlySubs){ if (epMeta.OnlySubs){
newOptions.Novids = true; newOptions.Novids = true;
newOptions.Noaudio = 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)){ if (File.Exists(downloadItemDownloadedFile)){
File.Delete(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;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
@ -29,7 +30,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
private ProgramManager _programManager; private ProgramManager _programManager;
[ObservableProperty] [ObservableProperty]
private HistorySeries _selectedSeries; private HistorySeries? _selectedSeries;
[ObservableProperty] [ObservableProperty]
private static bool _editMode; private static bool _editMode;
@ -72,10 +73,16 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _isPosterViewSelected = false; private bool _isPosterViewSelected;
[ObservableProperty] [ObservableProperty]
private bool _isTableViewSelected = false; private bool _isTableViewSelected;
[ObservableProperty]
private bool _showSeries = true;
[ObservableProperty]
private bool _showArtists;
[ObservableProperty] [ObservableProperty]
private static bool _viewSelectionOpen; private static bool _viewSelectionOpen;
@ -98,7 +105,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
private FilterType currentFilterType; private FilterType currentFilterType;
[ObservableProperty] [ObservableProperty]
private static bool _sortDir = false; private static bool _sortDir;
[ObservableProperty] [ObservableProperty]
private static bool _sonarrAvailable; private static bool _sonarrAvailable;
@ -127,6 +134,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
currentFilterType = properties?.SelectedFilter ?? FilterType.All; currentFilterType = properties?.SelectedFilter ?? FilterType.All;
ScaleValue = properties?.ScaleValue ?? 0.73; ScaleValue = properties?.ScaleValue ?? 0.73;
SortDir = properties?.Ascending ?? false; SortDir = properties?.Ascending ?? false;
ShowSeries = properties?.ShowSeries ?? true;
ShowArtists = properties?.ShowArtists ?? false;
foreach (HistoryViewType viewType in Enum.GetValues(typeof(HistoryViewType))){ foreach (HistoryViewType viewType in Enum.GetValues(typeof(HistoryViewType))){
var combobox = new ComboBoxItem{ Content = viewType }; var combobox = new ComboBoxItem{ Content = viewType };
@ -137,7 +146,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
} }
foreach (SortingType sortingType in Enum.GetValues(typeof(SortingType))){ 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); SortingList.Add(combobox);
if (sortingType == currentSortingType){ if (sortingType == currentSortingType){
SelectedSorting = combobox; SelectedSorting = combobox;
@ -149,7 +158,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
continue; continue;
} }
var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType }; var item = new FilterListElement{ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType };
FilterList.Add(item); FilterList.Add(item);
if (filterType == currentFilterType){ if (filterType == currentFilterType){
SelectedFilter = item; SelectedFilter = item;
@ -162,7 +171,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
foreach (var historySeries in Items){ foreach (var historySeries in Items){
if (historySeries.ThumbnailImage == null){ if (historySeries.ThumbnailImage == null){
historySeries.LoadImage(); _ = historySeries.LoadImage();
} }
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
@ -179,15 +188,15 @@ public partial class HistoryPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType; CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.Ascending = SortDir; CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.Ascending = SortDir;
} else{ } else{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties() CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties
{ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType, Ascending = SortDir }; { ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType, Ascending = SortDir };
} }
CfgManager.WriteSettingsToFile(); CfgManager.WriteCrSettings();
} }
partial void OnSelectedViewChanged(ComboBoxItem value){ partial void OnSelectedViewChanged(ComboBoxItem? value){
if (Enum.TryParse(value.Content + "", out HistoryViewType viewType)){ if (Enum.TryParse(value?.Content + "", out HistoryViewType viewType)){
currentViewType = viewType; currentViewType = viewType;
IsPosterViewSelected = currentViewType == HistoryViewType.Posters; IsPosterViewSelected = currentViewType == HistoryViewType.Posters;
IsTableViewSelected = currentViewType == HistoryViewType.Table; IsTableViewSelected = currentViewType == HistoryViewType.Table;
@ -214,21 +223,33 @@ public partial class HistoryPageViewModel : ViewModelBase{
return; return;
} }
if (newValue.SelectedSorting != null){ currentSortingType = newValue.SelectedSorting;
currentSortingType = newValue.SelectedSorting; if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType; CrunchyrollManager.Instance.History.SortItems();
CrunchyrollManager.Instance.History.SortItems(); if (SelectedFilter != null){
if (SelectedFilter != null){ OnSelectedFilterChanged(SelectedFilter);
OnSelectedFilterChanged(SelectedFilter);
}
} else{
Console.Error.WriteLine("Invalid viewtype selected");
} }
SortingSelectionOpen = false; SortingSelectionOpen = false;
UpdateSettings(); 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){ partial void OnSelectedFilterChanged(FilterListElement? value){
if (value == null){ if (value == null){
@ -237,37 +258,52 @@ public partial class HistoryPageViewModel : ViewModelBase{
currentFilterType = value.SelectedType; currentFilterType = value.SelectedType;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType; if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType;
CfgManager.WriteCrSettings();
ApplyFilter();
}
private void ApplyFilter(){
List<HistorySeries> filteredItems;
switch (currentFilterType){ switch (currentFilterType){
case FilterType.All: case FilterType.All:
FilteredItems.Clear(); filteredItems = Items.ToList();
FilteredItems.AddRange(Items);
break; break;
case FilterType.MissingEpisodes: case FilterType.MissingEpisodes:
List<HistorySeries> filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList(); filteredItems = Items.Where(item => item.NewEpisodes > 0).ToList();
FilteredItems.Clear();
FilteredItems.AddRange(filteredItems);
break; break;
case FilterType.MissingEpisodesSonarr: case FilterType.MissingEpisodesSonarr:
filteredItems = Items.Where(historySeries =>
var missingSonarrFiltered = Items.Where(historySeries => !string.IsNullOrEmpty(historySeries.SonarrSeriesId) &&
!string.IsNullOrEmpty(historySeries.SonarrSeriesId) && // Check series ID historySeries.Seasons.Any(season =>
historySeries.Seasons.Any(season => // Check each season season.EpisodesList.Any(historyEpisode =>
season.EpisodesList.Any(historyEpisode => // Check each episode !string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile)))
!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) && !historyEpisode.SonarrHasFile))) // Filter condition
.ToList(); .ToList();
FilteredItems.Clear();
FilteredItems.AddRange(missingSonarrFiltered);
break; break;
case FilterType.ContinuingOnly: case FilterType.ContinuingOnly:
List<HistorySeries> continuingFiltered = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList(); filteredItems = Items.Where(item => !string.IsNullOrEmpty(item.SonarrNextAirDate)).ToList();
FilteredItems.Clear(); break;
FilteredItems.AddRange(continuingFiltered);
default:
filteredItems = new List<HistorySeries>();
break; 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(); NavToSeries();
if (!string.IsNullOrEmpty(value.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){ 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(); CfgManager.UpdateHistoryFile();
} }
SelectedSeries = null;
_selectedSeries = null;
} }
[RelayCommand] [RelayCommand]
@ -348,7 +384,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
} }
[RelayCommand] [RelayCommand]
public async void AddMissingToQueue(){ public async Task AddMissingToQueue(){
var tasks = FilteredItems var tasks = FilteredItems
.Select(item => item.AddNewMissingToDownloads()); .Select(item => item.AddNewMissingToDownloads());
@ -408,7 +444,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
ProgressText = $"{count + 1}/{totalSeries}"; ProgressText = $"{count + 1}/{totalSeries}";
// Await the CRUpdateSeries task for each seriesId // Await the CRUpdateSeries task for each seriesId
await crInstance.History.CRUpdateSeries(seriesIds[count], ""); await crInstance.History.CrUpdateSeries(seriesIds[count], "");
RaisePropertyChanged(nameof(ProgressText)); RaisePropertyChanged(nameof(ProgressText));
} }
@ -500,27 +536,30 @@ public partial class HistoryPageViewModel : ViewModelBase{
} }
} }
public class HistoryPageProperties(){ public class HistoryPageProperties{
public SortingType? SelectedSorting{ get; set; } public SortingType? SelectedSorting{ get; set; }
public HistoryViewType SelectedView{ get; set; } public HistoryViewType SelectedView{ get; set; }
public FilterType SelectedFilter{ get; set; } public FilterType SelectedFilter{ get; set; }
public double? ScaleValue{ get; set; } public double? ScaleValue{ get; set; }
public bool Ascending{ 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 SortingType? SelectedSorting{ get; set; }
public bool Ascending{ get; set; } public bool Ascending{ get; set; }
} }
public class SortingListElement(){ public class SortingListElement{
public SortingType SelectedSorting{ get; set; } public SortingType SelectedSorting{ get; set; }
public string? SortingTitle{ get; set; } public string? SortingTitle{ get; set; }
} }
public class FilterListElement(){ public class FilterListElement{
public FilterType SelectedType{ get; set; } public FilterType SelectedType{ get; set; }
public string? FilterTitle{ get; set; } public string? FilterTitle{ get; set; }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -186,15 +186,44 @@
Placement="BottomEdgeAlignedRight" Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonFilter}"> PlacementTarget="{Binding ElementName=DropdownButtonFilter}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Single" Width="210" <StackPanel Orientation="Vertical">
MaxHeight="400"
ItemsSource="{Binding FilterList}" SelectedItem="{Binding SelectedFilter}"> <Grid Margin="8 0 5 0">
<ListBox.ItemTemplate> <Grid.ColumnDefinitions>
<DataTemplate> <ColumnDefinition Width="*" /> <!-- TextBlock takes remaining space -->
<TextBlock Text="{Binding FilterTitle}"></TextBlock> <ColumnDefinition Width="Auto" /> <!-- ToggleSwitch takes only needed space -->
</DataTemplate> </Grid.ColumnDefinitions>
</ListBox.ItemTemplate>
</ListBox> <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> </Border>
</Popup> </Popup>
</StackPanel> </StackPanel>

View file

@ -13,6 +13,7 @@
<UserControl.Resources> <UserControl.Resources>
<ui:UiListToStringConverter x:Key="UiListToStringConverter" /> <ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid>
@ -57,8 +58,8 @@
<TextBlock Grid.Row="0" FontSize="45" Text="{Binding SelectedSeries.SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock> <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="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="3" IsVisible="{Binding SelectedSeries.HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}" 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="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 Grid.Row="5" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0 10 10 10"> <StackPanel Orientation="Horizontal" Margin="0 10 10 10">
@ -183,7 +184,7 @@
</StackPanel> </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> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -313,14 +314,15 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel> <StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="E"></TextBlock> <TextBlock Text="E"></TextBlock>
<TextBlock Text="{Binding Episode}"></TextBlock> <TextBlock Text="{Binding Episode}"></TextBlock>
<TextBlock Text=" - "></TextBlock> <TextBlock Text=" - "></TextBlock>
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock> <TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontStyle="Italic" <TextBlock FontStyle="Italic"
FontSize="12" FontSize="12"
Opacity="0.8" Text="Dubs: "> Opacity="0.8" Text="Dubs: ">
@ -343,10 +345,30 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center"> <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}"> 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}" <controls:ImageIcon IsVisible="{Binding SonarrHasFile}"
Source="../Assets/sonarr.png" Width="25" Source="../Assets/sonarr.png" Width="25"
Height="25" /> Height="25" />
@ -382,7 +404,7 @@
</Button> </Button>
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right" <Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding DownloadEpisode}" Command="{Binding DownloadEpisode}"
CommandParameter="false"> CommandParameter="false">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@ -392,9 +414,10 @@
<TextBlock Text="Download Episode" FontSize="15" /> <TextBlock Text="Download Episode" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
</Button> </Button>
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right" <Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
Command="{Binding DownloadEpisode}" Command="{Binding DownloadEpisode}"
CommandParameter="true"> CommandParameter="true">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@ -404,8 +427,6 @@
<TextBlock Text="Download Subs" FontSize="15" /> <TextBlock Text="Download Subs" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
</Button> </Button>
</StackPanel> </StackPanel>

View file

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

View file

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

View file

@ -33,17 +33,29 @@
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </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 Content="History Add Specials" Description="Add specials to the queue if they weren't downloaded before">
<controls:SettingsExpanderItem.Footer> <controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox> <CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox>
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </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 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> <controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryCountSonarr}"> </CheckBox> <CheckBox IsChecked="{Binding HistoryCountSonarr}"> </CheckBox>
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </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> </controls:SettingsExpander>