- Added option to **update history from the calendar**

- Added **search for currently shown series** in the history view
- Changed **authentication tokens**
- Fixed **download info updates** where the resolution was shown incorrectly
This commit is contained in:
Elwador 2026-01-31 19:11:58 +01:00
parent 6abbc129b6
commit 973c45ce5c
15 changed files with 776 additions and 282 deletions

View file

@ -234,8 +234,16 @@ public class CalendarManager{
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true); var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true);
if (newEpisodesBase is{ Data.Count: > 0 }){ if (newEpisodesBase is{ Data.Count: > 0 }){
var newEpisodes = newEpisodesBase.Data; var newEpisodes = newEpisodesBase.Data ?? [];
if (CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar){
try{
await CrunchyrollManager.Instance.History.UpdateWithEpisode(newEpisodes);
} catch (Exception e){
Console.Error.WriteLine("Failed to update History from calendar");
}
}
//EpisodeAirDate //EpisodeAirDate
foreach (var crBrowseEpisode in newEpisodes){ foreach (var crBrowseEpisode in newEpisodes){
bool filtered = false; bool filtered = false;

View file

@ -6,6 +6,7 @@ using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using CRD.Downloader.Crunchyroll.Utils;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
@ -73,92 +74,102 @@ public class CrEpisode(){
public async Task<CrunchyRollEpisodeData> EpisodeData(CrunchyEpisode dlEpisode, bool updateHistory = false){ public async Task<CrunchyRollEpisodeData> EpisodeData(CrunchyEpisode dlEpisode, bool updateHistory = false){
bool serieshasversions = true; bool serieshasversions = true;
var episode = new CrunchyRollEpisodeData();
// Dictionary<string, EpisodeAndLanguage> episodes = new Dictionary<string, EpisodeAndLanguage>();
CrunchyRollEpisodeData episode = new CrunchyRollEpisodeData();
if (crunInstance.CrunOptions.History && updateHistory){ if (crunInstance.CrunOptions.History && updateHistory){
await crunInstance.History.UpdateWithEpisodeList([dlEpisode]); 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); crunInstance.History.MatchHistorySeriesWithSonarr(false);
await CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(false, historySeries); await crunInstance.History.MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
} }
} }
var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier) ? dlEpisode.Identifier.Split('|')[1] : $"S{dlEpisode.SeasonNumber}"; // initial key
episode.Key = $"{seasonIdentifier}E{dlEpisode.Episode ?? (dlEpisode.EpisodeNumber + "")}"; var seasonIdentifier = !string.IsNullOrEmpty(dlEpisode.Identifier)
episode.EpisodeAndLanguages = new EpisodeAndLanguage{ ? dlEpisode.Identifier.Split('|')[1]
Items = new List<CrunchyEpisode>(), : $"S{dlEpisode.SeasonNumber}";
Langs = new List<LanguageItem>()
};
episode.Key = $"{seasonIdentifier}E{dlEpisode.Episode ?? (dlEpisode.EpisodeNumber + "")}";
episode.EpisodeAndLanguages = new EpisodeAndLanguage();
// Build Variants
if (dlEpisode.Versions != null){ if (dlEpisode.Versions != null){
foreach (var version in dlEpisode.Versions){ foreach (var version in dlEpisode.Versions){
// Ensure there is only one of the same language var lang = Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale)
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != version.AudioLocale)){ ?? Languages.DEFAULT_lang;
// Push to arrays if there are no duplicates of the same language
episode.EpisodeAndLanguages.Items.Add(dlEpisode); episode.EpisodeAndLanguages.AddUnique(dlEpisode, lang);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? Languages.DEFAULT_lang);
}
} }
} else{ } else{
// Episode didn't have versions, mark it as such to be logged.
serieshasversions = false; serieshasversions = false;
// Ensure there is only one of the same language
if (episode.EpisodeAndLanguages.Langs.All(a => a.CrLocale != dlEpisode.AudioLocale)){ var lang = Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale)
// Push to arrays if there are no duplicates of the same language ?? Languages.DEFAULT_lang;
episode.EpisodeAndLanguages.Items.Add(dlEpisode);
episode.EpisodeAndLanguages.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == dlEpisode.AudioLocale) ?? Languages.DEFAULT_lang); episode.EpisodeAndLanguages.AddUnique(dlEpisode, lang);
}
} }
if (episode.EpisodeAndLanguages.Variants.Count == 0)
return episode;
int specialIndex = 1; var baseEp = episode.EpisodeAndLanguages.Variants[0].Item;
int epIndex = 1;
var isSpecial = baseEp.IsSpecialEpisode();
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(baseEp.Episode)){
newKey = episode.EpisodeAndLanguages.Items[0].Episode ?? "SP" + (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id); newKey = baseEp.Episode;
} else{ } else{
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + episode.EpisodeAndLanguages.Items[0].Id) : episode.EpisodeAndLanguages.Items[0].Episode ?? epIndex + "")}"; var epPart = baseEp.Episode ?? (baseEp.EpisodeNumber?.ToString() ?? "1");
newKey = isSpecial
? $"SP{epPart} {baseEp.Id}"
: $"E{epPart}";
} }
episode.Key = newKey; episode.Key = newKey;
var seasonTitle = episode.EpisodeAndLanguages.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)"))?.SeasonTitle var seasonTitle =
?? Regex.Replace(episode.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); episode.EpisodeAndLanguages.Variants
.Select(v => v.Item.SeasonTitle)
.FirstOrDefault(t => !DownloadQueueItemFactory.HasDubSuffix(t))
?? DownloadQueueItemFactory.StripDubSuffix(baseEp.SeasonTitle);
var title = episode.EpisodeAndLanguages.Items[0].Title; var title = baseEp.Title;
var seasonNumber = Helpers.ExtractNumberAfterS(episode.EpisodeAndLanguages.Items[0].Identifier) ?? episode.EpisodeAndLanguages.Items[0].SeasonNumber.ToString(); var seasonNumber = baseEp.GetSeasonNum();
var languages = episode.EpisodeAndLanguages.Items.Select((a, index) => var languages = episode.EpisodeAndLanguages.Variants
$"{(a.IsPremiumOnly ? "+ " : "")}{episode.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆ .Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.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)}]");
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;
} }
public CrunchyEpMeta EpisodeMeta(CrunchyRollEpisodeData episodeP, List<string> dubLang){ public CrunchyEpMeta EpisodeMeta(CrunchyRollEpisodeData episodeP, List<string> dubLang){
// var ret = new Dictionary<string, CrunchyEpMeta>(); CrunchyEpMeta? retMeta = null;
var retMeta = new CrunchyEpMeta(); var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key;
var hslang = crunInstance.CrunOptions.Hslang;
var selectedDubs = dubLang
.Where(d => episodeP.EpisodeAndLanguages.Variants.Any(v => v.Lang.CrLocale == d))
.ToList();
for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){ foreach (var v in episodeP.EpisodeAndLanguages.Variants){
var item = episodeP.EpisodeAndLanguages.Items[index]; var item = v.Item;
var lang = v.Lang;
if (!dubLang.Contains(episodeP.EpisodeAndLanguages.Langs[index].CrLocale)) if (!dubLang.Contains(lang.CrLocale))
continue; continue;
item.HideSeasonTitle = true; item.HideSeasonTitle = true;
@ -173,67 +184,54 @@ public class CrEpisode(){
item.SeriesTitle = "NO_TITLE"; item.SeriesTitle = "NO_TITLE";
} }
var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key;
var images = (item.Images?.Thumbnail ?? [new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
Regex dubPattern = new Regex(@"\(\w+ Dub\)");
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle))?.SeriesTitle ??
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle))?.SeasonTitle ??
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title;
epMeta.SeasonId = item.SeasonId;
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
epMeta.SeriesId = item.SeriesId;
epMeta.AbsolutEpisodeNumberE = epNum;
epMeta.Image = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty;
epMeta.ImageBig = images.FirstOrDefault()?.LastOrDefault()?.Source ?? string.Empty;
epMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = false,
Done = false,
Error = false,
Percent = 0,
Time = 0,
DownloadSpeedBytes = 0
};
epMeta.AvailableSubs = item.SubtitleLocales;
epMeta.Description = item.Description;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
if (episodeP.EpisodeAndLanguages.Langs.Count > 0){
epMeta.SelectedDubs = dubLang
.Where(language => episodeP.EpisodeAndLanguages.Langs.Any(epLang => epLang.CrLocale == language))
.ToList();
}
var epMetaData = epMeta.Data[0];
if (!string.IsNullOrEmpty(item.StreamsLink)){
epMetaData.Playback = item.StreamsLink;
if (string.IsNullOrEmpty(item.Playback)){
item.Playback = item.StreamsLink;
}
}
if (retMeta.Data is{ Count: > 0 }){
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
retMeta.Data.Add(epMetaData);
} else{
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
epMeta.Data[0] = epMetaData;
retMeta = epMeta;
}
// show ep
item.SeqId = epNum; item.SeqId = epNum;
if (retMeta == null){
var seriesTitle = DownloadQueueItemFactory.CanonicalTitle(
episodeP.EpisodeAndLanguages.Variants.Select(x => (string?)x.Item.SeriesTitle));
var seasonTitle = DownloadQueueItemFactory.CanonicalTitle(
episodeP.EpisodeAndLanguages.Variants.Select(x => (string?)x.Item.SeasonTitle));
var (img, imgBig) = DownloadQueueItemFactory.GetThumbSmallBig(item.Images);
retMeta = DownloadQueueItemFactory.CreateShell(
service: StreamingService.Crunchyroll,
seriesTitle: seriesTitle,
seasonTitle: seasonTitle,
episodeNumber: item.Episode,
episodeTitle: item.GetEpisodeTitle(),
description: item.Description,
seriesId: item.SeriesId,
seasonId: item.SeasonId,
season: item.GetSeasonNum(),
absolutEpisodeNumberE: epNum,
image: img,
imageBig: imgBig,
hslang: hslang,
availableSubs: item.SubtitleLocales,
selectedDubs: selectedDubs
);
}
var playback = item.Playback;
if (!string.IsNullOrEmpty(item.StreamsLink)){
playback = item.StreamsLink;
if (string.IsNullOrEmpty(item.Playback))
item.Playback = item.StreamsLink;
}
retMeta.Data.Add(DownloadQueueItemFactory.CreateVariant(
mediaId: item.Id,
lang: lang,
playback: playback,
versions: item.Versions,
isSubbed: item.IsSubbed,
isDubbed: item.IsDubbed
));
} }
return retMeta ?? new CrunchyEpMeta();
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){

View file

@ -6,6 +6,7 @@ using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using CRD.Downloader.Crunchyroll.Utils;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
@ -17,32 +18,44 @@ 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? all, List<string>? e){
var ret = new Dictionary<string, CrunchyEpMeta>(); var ret = new Dictionary<string, CrunchyEpMeta>();
var hasPremium = crunInstance.CrAuthEndpoint1.Profile.HasPremium;
foreach (var kvp in eps){ var hslang = crunInstance.CrunOptions.Hslang;
var key = kvp.Key;
var episode = kvp.Value;
for (int index = 0; index < episode.Items.Count; index++){ bool ShouldInclude(string epNum) =>
var item = episode.Items[index]; all is true || (e != null && e.Contains(epNum));
if (item.IsPremiumOnly && !crunInstance.CrAuthEndpoint1.Profile.HasPremium){ foreach (var (key, episode) in eps){
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)); var epNum = key.StartsWith('E') ? key[1..] : key;
foreach (var v in episode.Variants){
var item = v.Item;
var lang = v.Lang;
item.SeqId = epNum;
if (item.IsPremiumOnly && !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));
continue; continue;
} }
// history override
var effectiveDubs = dubLang;
if (crunInstance.CrunOptions.History){ if (crunInstance.CrunOptions.History){
var dubLangList = crunInstance.History.GetDubList(item.SeriesId, item.SeasonId); var dubLangList = crunInstance.History.GetDubList(item.SeriesId, item.SeasonId);
if (dubLangList.Count > 0){ if (dubLangList.Count > 0)
dubLang = dubLangList; effectiveDubs = dubLangList;
}
} }
if (!dubLang.Contains(episode.Langs[index].CrLocale)) if (!effectiveDubs.Contains(lang.CrLocale))
continue; continue;
// season title fallbacks (same behavior)
item.HideSeasonTitle = true; item.HideSeasonTitle = true;
if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){ if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){
item.SeasonTitle = item.SeriesTitle; item.SeasonTitle = item.SeriesTitle;
@ -55,66 +68,65 @@ public class CrSeries{
item.SeriesTitle = "NO_TITLE"; item.SeriesTitle = "NO_TITLE";
} }
var epNum = key.StartsWith('E') ? key[1..] : key; // selection gate
var images = (item.Images?.Thumbnail ??[new List<Image>{ new(){ Source = "/notFound.jpg" } }]); if (!ShouldInclude(epNum))
continue;
Regex dubPattern = new Regex(@"\(\w+ Dub\)"); // Create base queue item once per "key"
if (!ret.TryGetValue(key, out var qItem)){
var seriesTitle = DownloadQueueItemFactory.CanonicalTitle(
episode.Variants.Select(x => (string?)x.Item.SeriesTitle));
var epMeta = new CrunchyEpMeta(); var seasonTitle = DownloadQueueItemFactory.CanonicalTitle(
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } }; episode.Variants.Select(x => (string?)x.Item.SeasonTitle));
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(); var (img, imgBig) = DownloadQueueItemFactory.GetThumbSmallBig(item.Images);
epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title; var selectedDubs = effectiveDubs
epMeta.SeasonId = item.SeasonId; .Where(d => episode.Variants.Any(x => x.Lang.CrLocale == d))
epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + "";
epMeta.SeriesId = item.SeriesId;
epMeta.AbsolutEpisodeNumberE = epNum;
epMeta.Image = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty;
epMeta.ImageBig = images.FirstOrDefault()?.LastOrDefault()?.Source ?? string.Empty;
epMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = false,
Done = false,
Percent = 0,
Time = 0,
DownloadSpeedBytes = 0
};
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
epMeta.Description = item.Description;
epMeta.AvailableSubs = item.SubtitleLocales;
if (episode.Langs.Count > 0){
epMeta.SelectedDubs = dubLang
.Where(language => episode.Langs.Any(epLang => epLang.CrLocale == language))
.ToList(); .ToList();
qItem = DownloadQueueItemFactory.CreateShell(
service: StreamingService.Crunchyroll,
seriesTitle: seriesTitle,
seasonTitle: seasonTitle,
episodeNumber: item.Episode,
episodeTitle: item.Title,
description: item.Description,
seriesId: item.SeriesId,
seasonId: item.SeasonId,
season: Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber.ToString(),
absolutEpisodeNumberE: epNum,
image: img,
imageBig: imgBig,
hslang: hslang,
availableSubs: item.SubtitleLocales,
selectedDubs: selectedDubs
);
ret.Add(key, qItem);
} }
// playback preference
var epMetaData = epMeta.Data[0]; var playback = item.Playback;
if (!string.IsNullOrEmpty(item.StreamsLink)){ if (!string.IsNullOrEmpty(item.StreamsLink)){
epMetaData.Playback = item.StreamsLink; playback = item.StreamsLink;
if (string.IsNullOrEmpty(item.Playback)){ if (string.IsNullOrEmpty(item.Playback))
item.Playback = item.StreamsLink; item.Playback = item.StreamsLink;
}
} }
if (all is true || e != null && e.Contains(epNum)){ // Add variant
if (ret.TryGetValue(key, out var epMe)){ ret[key].Data.Add(DownloadQueueItemFactory.CreateVariant(
epMetaData.Lang = episode.Langs[index]; mediaId: item.Id,
epMe.Data.Add(epMetaData); lang: lang,
} else{ playback: playback,
epMetaData.Lang = episode.Langs[index]; versions: item.Versions,
epMeta.Data[0] = epMetaData; isSubbed: item.IsSubbed,
ret.Add(key, epMeta); isDubbed: item.IsDubbed
} ));
}
// show ep
item.SeqId = epNum;
} }
} }
return ret; return ret;
} }
@ -124,64 +136,58 @@ public class CrSeries{
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale, forcedLocale); CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale, forcedLocale);
if (parsedSeries == null || parsedSeries.Data == null){ if (parsedSeries?.Data == null){
Console.Error.WriteLine("Parse Data Invalid"); Console.Error.WriteLine("Parse Data Invalid");
return null; return null;
} }
// var result = ParseSeriesResult(parsedSeries); var 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.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){ if (seasonData.Data == null)
foreach (var episode in seasonData.Data){ continue;
// Prepare the episode array
EpisodeAndLanguage item;
foreach (var episode in seasonData.Data){
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.TryGetValue(episodeKey, out var item)){
item = new EpisodeAndLanguage{ item = new EpisodeAndLanguage(); // must have Variants
Items = new List<CrunchyEpisode>(), episodes[episodeKey] = item;
Langs = new List<LanguageItem>() }
};
episodes[episodeKey] = item; if (episode.Versions != null){
} else{ foreach (var version in episode.Versions){
item = episodes[episodeKey]; var lang = Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale) ?? new LanguageItem();
} item.AddUnique(episode, lang); // must enforce uniqueness by CrLocale
if (episode.Versions != null){
foreach (var version in episode.Versions){
if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
item.Items.Add(episode);
item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == 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());
}
} }
} else{
serieshasversions = false;
var lang = Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale) ?? new LanguageItem();
item.AddUnique(episode, lang);
} }
} }
} }
@ -198,22 +204,25 @@ public class CrSeries{
int specialIndex = 1; int specialIndex = 1;
int epIndex = 1; int epIndex = 1;
var keys = new List<string>(episodes.Keys); // Copying the keys to a new list to avoid modifying the collection while iterating. var keys = new List<string>(episodes.Keys);
foreach (var key in keys){ foreach (var key in keys){
EpisodeAndLanguage item = episodes[key]; var item = episodes[key];
var episode = item.Items[0].Episode; if (item.Variants.Count == 0)
var isSpecial = episode != null && !Regex.IsMatch(episode, @"^\d+(\.\d+)?$"); // Checking if the episode is not a number (i.e., special). continue;
// var newKey = $"{(isSpecial ? 'S' : 'E')}{(isSpecial ? specialIndex : epIndex).ToString()}";
var baseEp = item.Variants[0].Item;
var epStr = baseEp.Episode;
var isSpecial = epStr != null && !Regex.IsMatch(epStr, @"^\d+(\.\d+)?$");
string newKey; string newKey;
if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){ if (isSpecial && !string.IsNullOrEmpty(baseEp.Episode)){
newKey = $"SP{specialIndex}_" + item.Items[0].Episode;// ?? "SP" + (specialIndex + " " + item.Items[0].Id); newKey = $"SP{specialIndex}_" + baseEp.Episode;
} else{ } else{
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}"; newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + baseEp.Id) : epIndex + "")}";
} }
episodes.Remove(key); episodes.Remove(key);
int counter = 1; int counter = 1;
@ -225,63 +234,95 @@ public class CrSeries{
episodes.Add(newKey, item); episodes.Add(newKey, item);
if (isSpecial){ if (isSpecial) specialIndex++;
specialIndex++; else epIndex++;
} else{
epIndex++;
}
} }
var specials = episodes.Where(e => e.Key.StartsWith("S")).ToList(); var normal = episodes.Where(kvp => kvp.Key.StartsWith("E")).ToList();
var normal = episodes.Where(e => e.Key.StartsWith("E")).ToList(); var specials = episodes.Where(kvp => kvp.Key.StartsWith("SP")).ToList();
// Combining and sorting episodes with normal first, then specials.
var sortedEpisodes = new Dictionary<string, EpisodeAndLanguage>(normal.Concat(specials)); var sortedEpisodes = new Dictionary<string, EpisodeAndLanguage>(normal.Concat(specials));
foreach (var kvp in sortedEpisodes){ foreach (var kvp in sortedEpisodes){
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 if (item.Variants.Count == 0)
?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(); continue;
var title = item.Items[0].Title; var baseEp = item.Variants[0].Item;
var seasonNumber = Helpers.ExtractNumberAfterS(item.Items[0].Identifier) ?? item.Items[0].SeasonNumber.ToString();
var languages = item.Items.Select((a, index) => var seasonTitle = DownloadQueueItemFactory.CanonicalTitle(
$"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index)?.Name ?? "Unknown"}").ToArray(); //☆ item.Variants.Select(string? (v) => v.Item.SeasonTitle)
);
var title = baseEp.Title;
var seasonNumber = Helpers.ExtractNumberAfterS(baseEp.Identifier) ?? baseEp.SeasonNumber.ToString();
var languages = item.Variants
.Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.Name ?? "Unknown"}")
.ToArray();
Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]"); Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
} }
if (!serieshasversions){ if (!serieshasversions)
Console.WriteLine("Couldn\'t find versions on some episodes, added languages with language array."); Console.WriteLine("Couldn\'t find versions on some episodes, added languages with language array.");
}
CrunchySeriesList crunchySeriesList = new CrunchySeriesList(); var crunchySeriesList = new CrunchySeriesList{
crunchySeriesList.Data = sortedEpisodes; Data = sortedEpisodes
};
crunchySeriesList.List = sortedEpisodes.Select(kvp => { crunchySeriesList.List = sortedEpisodes.Select(kvp => {
var key = kvp.Key; var key = kvp.Key;
var value = kvp.Value; var value = kvp.Value;
var images = (value.Items.FirstOrDefault()?.Images?.Thumbnail ??[new List<Image>{ new(){ Source = "/notFound.jpg" } }]);
var seconds = (int)Math.Floor((value.Items.FirstOrDefault()?.DurationMs ?? 0) / 1000.0); if (value.Variants.Count == 0){
var langList = value.Langs.Select(a => a.CrLocale).ToList(); return new Episode{
E = key.StartsWith("E") ? key.Substring(1) : key,
Lang = new List<string>(),
Name = string.Empty,
Season = string.Empty,
SeriesTitle = string.Empty,
SeasonTitle = string.Empty,
EpisodeNum = key,
Id = string.Empty,
Img = string.Empty,
Description = string.Empty,
EpisodeType = EpisodeType.Episode,
Time = "0:00"
};
}
var baseEp = value.Variants[0].Item;
var thumbRow = baseEp.Images.Thumbnail.FirstOrDefault();
var img = thumbRow?.FirstOrDefault()?.Source ?? "/notFound.jpg";
var seconds = (int)Math.Floor((baseEp.DurationMs) / 1000.0);
var langList = value.Variants
.Select(v => v.Lang.CrLocale)
.Distinct()
.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,
Name = value.Items.FirstOrDefault()?.Title ?? string.Empty, Name = baseEp.Title ?? string.Empty,
Season = (Helpers.ExtractNumberAfterS(value.Items.FirstOrDefault()?.Identifier?? string.Empty) ?? value.Items.FirstOrDefault()?.SeasonNumber.ToString()) ?? string.Empty, Season = (Helpers.ExtractNumberAfterS(baseEp.Identifier) ?? baseEp.SeasonNumber.ToString()) ?? string.Empty,
SeriesTitle = Regex.Replace(value.Items.FirstOrDefault()?.SeriesTitle?? string.Empty, @"\(\w+ Dub\)", "").TrimEnd(), SeriesTitle = DownloadQueueItemFactory.StripDubSuffix(baseEp.SeriesTitle),
SeasonTitle = Regex.Replace(value.Items.FirstOrDefault()?.SeasonTitle?? string.Empty, @"\(\w+ Dub\)", "").TrimEnd(), SeasonTitle = DownloadQueueItemFactory.StripDubSuffix(baseEp.SeasonTitle),
EpisodeNum = key.StartsWith("SP") ? key : value.Items.FirstOrDefault()?.EpisodeNumber?.ToString() ?? value.Items.FirstOrDefault()?.Episode ?? "?", EpisodeNum = key.StartsWith("SP")
Id = value.Items.FirstOrDefault()?.SeasonId ?? string.Empty, ? key
Img = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty, : (baseEp.EpisodeNumber?.ToString() ?? baseEp.Episode ?? "?"),
Description = value.Items.FirstOrDefault()?.Description ?? string.Empty, Id = baseEp.SeasonId ?? string.Empty,
Img = img,
Description = baseEp.Description ?? string.Empty,
EpisodeType = EpisodeType.Episode, EpisodeType = EpisodeType.Episode,
Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds. Time = $"{seconds / 60}:{seconds % 60:D2}"
}; };
}).ToList(); }).ToList();
@ -333,7 +374,7 @@ public class CrSeries{
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() }; new CrunchyEpisodeList(){ Data = [], Total = 0, Meta = new Meta() };
} }
if (episodeList.Total < 1){ if (episodeList.Total < 1){
@ -377,8 +418,8 @@ public class CrSeries{
public async Task<CrSeriesBase?> SeriesById(string id, string? crLocale, bool forced = false){ public async Task<CrSeriesBase?> SeriesById(string id, string? crLocale, bool forced = false){
await crunInstance.CrAuthGuest.RefreshToken(true); await crunInstance.CrAuthGuest.RefreshToken(true);
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
query["preferred_audio_language"] = "ja-JP"; query["preferred_audio_language"] = "ja-JP";
@ -411,7 +452,7 @@ public class CrSeries{
public async Task<CrSearchSeriesBase?> Search(string searchString, string? crLocale, bool forced = false){ public async Task<CrSearchSeriesBase?> Search(string searchString, string? crLocale, bool forced = false){
await crunInstance.CrAuthGuest.RefreshToken(true); await crunInstance.CrAuthGuest.RefreshToken(true);
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
if (!string.IsNullOrEmpty(crLocale)){ if (!string.IsNullOrEmpty(crLocale)){
@ -456,7 +497,7 @@ public class CrSeries{
public async Task<CrBrowseSeriesBase?> GetAllSeries(string? crLocale){ public async Task<CrBrowseSeriesBase?> GetAllSeries(string? crLocale){
await crunInstance.CrAuthGuest.RefreshToken(true); await crunInstance.CrAuthGuest.RefreshToken(true);
CrBrowseSeriesBase complete = new CrBrowseSeriesBase(); CrBrowseSeriesBase complete = new CrBrowseSeriesBase();
complete.Data =[]; complete.Data = [];
var i = 0; var i = 0;
@ -495,7 +536,7 @@ public class CrSeries{
return complete; return complete;
} }
public async Task<CrBrowseSeriesBase?> GetSeasonalSeries(string season, string year, string? crLocale){ public async Task<CrBrowseSeriesBase?> GetSeasonalSeries(string season, string year, string? crLocale){
await crunInstance.CrAuthGuest.RefreshToken(true); await crunInstance.CrAuthGuest.RefreshToken(true);
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
@ -503,7 +544,7 @@ public class CrSeries{
if (!string.IsNullOrEmpty(crLocale)){ if (!string.IsNullOrEmpty(crLocale)){
query["locale"] = crLocale; query["locale"] = crLocale;
} }
query["seasonal_tag"] = season.ToLower() + "-" + year; query["seasonal_tag"] = season.ToLower() + "-" + year;
query["n"] = "100"; query["n"] = "100";
@ -520,5 +561,4 @@ public class CrSeries{
return series; return series;
} }
} }

View file

@ -1551,8 +1551,10 @@ public class CrunchyrollManager{
string qualityConsoleLog = sb.ToString(); string qualityConsoleLog = sb.ToString();
Console.WriteLine(qualityConsoleLog); Console.WriteLine(qualityConsoleLog);
data.AvailableQualities = qualityConsoleLog; if (!options.DlVideoOnce || string.IsNullOrEmpty(data.AvailableQualities)){
data.AvailableQualities = qualityConsoleLog;
}
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]); Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);

View file

@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using CRD.Utils;
using CRD.Utils.Structs;
namespace CRD.Downloader.Crunchyroll.Utils;
public static class DownloadQueueItemFactory{
private static readonly Regex DubSuffix = new(@"\(\w+ Dub\)", RegexOptions.Compiled);
public static bool HasDubSuffix(string? s)
=> !string.IsNullOrWhiteSpace(s) && DubSuffix.IsMatch(s);
public static string StripDubSuffix(string? s)
=> string.IsNullOrWhiteSpace(s) ? "" : DubSuffix.Replace(s, "").TrimEnd();
public static string CanonicalTitle(IEnumerable<string?> candidates){
var noDub = candidates.FirstOrDefault(t => !HasDubSuffix(t));
return !string.IsNullOrWhiteSpace(noDub)
? noDub!
: StripDubSuffix(candidates.FirstOrDefault());
}
public static (string small, string big) GetThumbSmallBig(Images? images){
var firstRow = images?.Thumbnail?.FirstOrDefault();
var small = firstRow?.FirstOrDefault()?.Source ?? "/notFound.jpg";
var big = firstRow?.LastOrDefault()?.Source ?? small;
return (small, big);
}
public static CrunchyEpMeta CreateShell(
StreamingService service,
string? seriesTitle,
string? seasonTitle,
string? episodeNumber,
string? episodeTitle,
string? description,
string? seriesId,
string? seasonId,
string? season,
string? absolutEpisodeNumberE,
string? image,
string? imageBig,
string hslang,
List<string>? availableSubs = null,
List<string>? selectedDubs = null,
bool music = false){
return new CrunchyEpMeta(){
SeriesTitle = seriesTitle,
SeasonTitle = seasonTitle,
EpisodeNumber = episodeNumber,
EpisodeTitle = episodeTitle,
Description = description,
SeriesId = seriesId,
SeasonId = seasonId,
Season = season,
AbsolutEpisodeNumberE = absolutEpisodeNumberE,
Image = image,
ImageBig = imageBig,
Hslang = hslang,
AvailableSubs = availableSubs,
SelectedDubs = selectedDubs,
Music = music
};
}
public static CrunchyEpMetaData CreateVariant(
string mediaId,
LanguageItem? lang,
string? playback,
List<EpisodeVersion>? versions,
bool isSubbed,
bool isDubbed,
bool isAudioRoleDescription = false){
return new CrunchyEpMetaData{
MediaId = mediaId,
Lang = lang,
Playback = playback,
Versions = versions,
IsSubbed = isSubbed,
IsDubbed = isDubbed,
IsAudioRoleDescription = isAudioRoleDescription
};
}
}

View file

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CRD.Utils;
using CRD.Utils.Structs;
namespace CRD.Downloader.Crunchyroll.Utils;
public static class EpisodeMapper{
public static CrunchyEpisode ToCrunchyEpisode(this CrBrowseEpisode src){
if (src == null) throw new ArgumentNullException(nameof(src));
var meta = src.EpisodeMetadata ?? new CrBrowseEpisodeMetaData();
return new CrunchyEpisode{
Id = src.Id ?? string.Empty,
Slug = src.Slug ?? string.Empty,
SlugTitle = src.SlugTitle ?? string.Empty,
Title = src.Title ?? string.Empty,
Description = src.Description ?? src.PromoDescription ?? string.Empty,
MediaType = src.Type,
ChannelId = src.ChannelId,
StreamsLink = src.StreamsLink,
Images = src.Images ?? new Images(),
SeoTitle = src.PromoTitle ?? string.Empty,
SeoDescription = src.PromoDescription ?? string.Empty,
ProductionEpisodeId = src.ExternalId ?? string.Empty,
ListingId = src.LinkedResourceKey ?? string.Empty,
SeriesId = meta.SeriesId ?? string.Empty,
SeasonId = meta.SeasonId ?? string.Empty,
SeriesTitle = meta.SeriesTitle ?? string.Empty,
SeriesSlugTitle = meta.SeriesSlugTitle ?? string.Empty,
SeasonTitle = meta.SeasonTitle ?? string.Empty,
SeasonSlugTitle = meta.SeasonSlugTitle ?? string.Empty,
SeasonNumber = SafeInt(meta.SeasonNumber),
SequenceNumber = (float)meta.SequenceNumber,
Episode = meta.Episode,
EpisodeNumber = meta.EpisodeCount,
DurationMs = meta.DurationMs,
Identifier = meta.Identifier ?? string.Empty,
AvailabilityNotes = meta.AvailabilityNotes ?? string.Empty,
EligibleRegion = meta.EligibleRegion ?? string.Empty,
AvailabilityStarts = meta.AvailabilityStarts,
AvailabilityEnds = meta.AvailabilityEnds,
PremiumAvailableDate = meta.PremiumAvailableDate,
FreeAvailableDate = meta.FreeAvailableDate,
AvailableDate = meta.AvailableDate,
PremiumDate = meta.PremiumDate,
UploadDate = meta.UploadDate,
EpisodeAirDate = meta.EpisodeAirDate,
IsDubbed = meta.IsDubbed,
IsSubbed = meta.IsSubbed,
IsMature = meta.IsMature,
IsClip = meta.IsClip,
IsPremiumOnly = meta.IsPremiumOnly,
MatureBlocked = meta.MatureBlocked,
AvailableOffline = meta.AvailableOffline,
ClosedCaptionsAvailable = meta.ClosedCaptionsAvailable,
MaturityRatings = meta.MaturityRatings ?? new List<string>(),
AudioLocale = (meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue(),
SubtitleLocales = (meta.SubtitleLocales ?? new List<Locale>())
.Select(l => l.GetEnumMemberValue())
.Where(s => !string.IsNullOrWhiteSpace(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(),
ExtendedMaturityRating = ToStringKeyDict(meta.ExtendedMaturityRating),
Versions = meta.versions?.Select(ToEpisodeVersion).ToList()
};
}
private static EpisodeVersion ToEpisodeVersion(CrBrowseEpisodeVersion v){
return new EpisodeVersion{
AudioLocale = (v.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue(),
Guid = v.Guid ?? string.Empty,
Original = v.Original,
Variant = v.Variant ?? string.Empty,
SeasonGuid = v.SeasonGuid ?? string.Empty,
MediaGuid = v.MediaGuid,
IsPremiumOnly = v.IsPremiumOnly,
roles = Array.Empty<string>()
};
}
private static int SafeInt(double value){
if (double.IsNaN(value) || double.IsInfinity(value)) return 0;
return (int)Math.Round(value, MidpointRounding.AwayFromZero);
}
private static Dictionary<string, object> ToStringKeyDict(Dictionary<object, object>? dict){
var result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (dict == null) return result;
foreach (var kv in dict){
var key = kv.Key?.ToString();
if (string.IsNullOrWhiteSpace(key)) continue;
result[key] = kv.Value ?? new object();
}
return result;
}
}

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Downloader.Crunchyroll.Utils;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
@ -38,7 +39,7 @@ public class History{
} }
} else{ } else{
var matchingSeason = historySeries.Seasons.FirstOrDefault(historySeason => historySeason.SeasonId == seasonId); var matchingSeason = historySeries.Seasons.FirstOrDefault(historySeason => historySeason.SeasonId == seasonId);
if (matchingSeason != null){ if (matchingSeason != null){
foreach (var historyEpisode in matchingSeason.EpisodesList){ foreach (var historyEpisode in matchingSeason.EpisodesList){
historyEpisode.IsEpisodeAvailableOnStreamingService = false; historyEpisode.IsEpisodeAvailableOnStreamingService = false;
@ -129,6 +130,129 @@ public class History{
} }
} }
public async Task UpdateWithEpisode(List<CrBrowseEpisode> episodes){
var historyIndex = crunInstance.HistoryList
.Where(h => !string.IsNullOrWhiteSpace(h.SeriesId))
.ToDictionary(
h => h.SeriesId!,
h => (h.Seasons)
.Where(s => !string.IsNullOrWhiteSpace(s.SeasonId))
.ToDictionary(
s => s.SeasonId ?? "UNKNOWN",
s => (s.EpisodesList)
.Select(ep => ep.EpisodeId)
.Where(id => !string.IsNullOrWhiteSpace(id))
.ToHashSet(StringComparer.Ordinal),
StringComparer.Ordinal
),
StringComparer.Ordinal
);
episodes = episodes
.Where(e => !string.IsNullOrWhiteSpace(e.EpisodeMetadata?.SeriesId) &&
historyIndex.ContainsKey(e.EpisodeMetadata!.SeriesId!))
.ToList();
foreach (var seriesGroup in episodes.GroupBy(e => e.EpisodeMetadata?.SeriesId ?? "UNKNOWN_SERIES")){
var seriesId = seriesGroup.Key;
var originalEntries = seriesGroup
.Select(e => new{ OriginalId = TryGetOriginalId(e), SeasonId = TryGetOriginalSeasonId(e) })
.Where(x => !string.IsNullOrWhiteSpace(x.OriginalId))
.GroupBy(x => x.OriginalId!, StringComparer.Ordinal)
.Select(g => new{
OriginalId = g.Key,
SeasonId = g.Select(x => x.SeasonId).FirstOrDefault(s => !string.IsNullOrWhiteSpace(s))
})
.ToList();
var hasAnyOriginalInfo = originalEntries.Count > 0;
var allOriginalsInHistory =
hasAnyOriginalInfo
&& originalEntries.All(x => IsOriginalInHistory(historyIndex, seriesId, x.SeasonId, x.OriginalId));
var originalItems = seriesGroup.Where(IsOriginalItem).ToList();
if (originalItems.Count > 0){
if (allOriginalsInHistory){
var sT = seriesGroup.Select(e => e.EpisodeMetadata?.SeriesTitle)
.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t)) ?? "";
// Console.WriteLine($"[INFO] Skipping SeriesId={seriesId} {sT} - all ORIGINAL episodes already in history.");
continue;
}
var convertedList = originalItems.Select(crBrowseEpisode => crBrowseEpisode.ToCrunchyEpisode()).ToList();
await crunInstance.History.UpdateWithSeasonData(convertedList.ToList<IHistorySource>());
continue;
}
var seriesTitle = seriesGroup.Select(e => e.EpisodeMetadata?.SeriesTitle)
.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t)) ?? "";
if (allOriginalsInHistory){
// Console.WriteLine($"[INFO] Skipping SeriesId={seriesId} - originals implied by Versions already in history.");
continue;
}
Console.WriteLine($"[WARN] No original ITEM found for SeriesId={seriesId} {seriesTitle}");
if (HasAllSeriesEpisodesInHistory(historyIndex, seriesId, seriesGroup)){
Console.WriteLine($"[History] Skip (already in history): {seriesId}");
} else{
await CrUpdateSeries(seriesId, null);
Console.WriteLine($"[History] Updating (full series): {seriesId}");
}
}
return;
}
private string? TryGetOriginalId(CrBrowseEpisode e) =>
e.EpisodeMetadata?.versions?
.FirstOrDefault(v => v.Original && !string.IsNullOrWhiteSpace(v.Guid))
?.Guid;
private string? TryGetOriginalSeasonId(CrBrowseEpisode e) =>
e.EpisodeMetadata?.versions?
.FirstOrDefault(v => v.Original && !string.IsNullOrWhiteSpace(v.SeasonGuid))
?.SeasonGuid
?? e.EpisodeMetadata?.SeasonId;
private bool IsOriginalItem(CrBrowseEpisode e){
var originalId = TryGetOriginalId(e);
return !string.IsNullOrWhiteSpace(originalId)
&& !string.IsNullOrWhiteSpace(e.Id)
&& string.Equals(e.Id, originalId, StringComparison.Ordinal);
}
private bool IsOriginalInHistory(Dictionary<string, Dictionary<string, HashSet<string?>>> historyIndex, string seriesId, string? seasonId, string originalEpisodeId){
if (!historyIndex.TryGetValue(seriesId, out var seasons)) return false;
if (!string.IsNullOrWhiteSpace(seasonId))
return seasons.TryGetValue(seasonId, out var eps) && eps.Contains(originalEpisodeId);
return seasons.Values.Any(eps => eps.Contains(originalEpisodeId));
}
private bool HasAllSeriesEpisodesInHistory(Dictionary<string, Dictionary<string, HashSet<string?>>> historyIndex, string seriesId, IEnumerable<CrBrowseEpisode> seriesEpisodes){
if (!historyIndex.TryGetValue(seriesId, out var seasons)) return false;
var allHistoryEpisodeIds = seasons.Values
.SelectMany(set => set)
.ToHashSet(StringComparer.Ordinal);
foreach (var e in seriesEpisodes){
if (string.IsNullOrWhiteSpace(e.Id)) return false;
if (!allHistoryEpisodeIds.Contains(e.Id)) return false;
}
return true;
}
/// <summary> /// <summary>
/// This method updates the History with a list of episodes. The episodes have to be from the same season. /// This method updates the History with a list of episodes. The episodes have to be from the same season.
/// </summary> /// </summary>
@ -206,7 +330,7 @@ public class History{
historySeries = new HistorySeries{ historySeries = new HistorySeries{
SeriesTitle = firstEpisode.GetSeriesTitle(), SeriesTitle = firstEpisode.GetSeriesTitle(),
SeriesId = firstEpisode.GetSeriesId(), SeriesId = firstEpisode.GetSeriesId(),
Seasons =[], Seasons = [],
HistorySeriesAddDate = DateTime.Now, HistorySeriesAddDate = DateTime.Now,
SeriesType = firstEpisode.GetSeriesType(), SeriesType = firstEpisode.GetSeriesType(),
SeriesStreamingService = StreamingService.Crunchyroll SeriesStreamingService = StreamingService.Crunchyroll
@ -302,8 +426,8 @@ public class History{
var downloadDirPath = ""; var downloadDirPath = "";
var videoQuality = ""; var videoQuality = "";
List<string> dublist =[]; List<string> dublist = [];
List<string> sublist =[]; List<string> sublist = [];
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
@ -353,7 +477,7 @@ public class History{
public List<string> GetDubList(string? seriesId, string? seasonId){ public List<string> GetDubList(string? seriesId, string? seasonId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
List<string> dublist =[]; List<string> dublist = [];
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
@ -372,7 +496,7 @@ public class History{
public (List<string> sublist, string videoQuality) GetSubList(string? seriesId, string? seasonId){ public (List<string> sublist, string videoQuality) GetSubList(string? seriesId, string? seasonId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
List<string> sublist =[]; List<string> sublist = [];
var videoQuality = ""; var videoQuality = "";
if (historySeries != null){ if (historySeries != null){
@ -430,8 +554,8 @@ public class History{
SeriesId = artisteData.Id, SeriesId = artisteData.Id,
SeriesTitle = artisteData.Name ?? "", SeriesTitle = artisteData.Name ?? "",
ThumbnailImageUrl = artisteData.Images.PosterTall.FirstOrDefault(e => e.Height == 360)?.Source ?? "", ThumbnailImageUrl = artisteData.Images.PosterTall.FirstOrDefault(e => e.Height == 360)?.Source ?? "",
HistorySeriesAvailableDubLang =[], HistorySeriesAvailableDubLang = [],
HistorySeriesAvailableSoftSubs =[] HistorySeriesAvailableSoftSubs = []
}; };
historySeries.SeriesDescription = cachedSeries.SeriesDescription; historySeries.SeriesDescription = cachedSeries.SeriesDescription;
@ -563,7 +687,7 @@ public class History{
SeasonTitle = firstEpisode.GetSeasonTitle(), SeasonTitle = firstEpisode.GetSeasonTitle(),
SeasonId = firstEpisode.GetSeasonId(), SeasonId = firstEpisode.GetSeasonId(),
SeasonNum = firstEpisode.GetSeasonNum(), SeasonNum = firstEpisode.GetSeasonNum(),
EpisodesList =[], EpisodesList = [],
SpecialSeason = firstEpisode.IsSpecialSeason() SpecialSeason = firstEpisode.IsSpecialSeason()
}; };
@ -631,7 +755,7 @@ public class History{
historySeries.SonarrNextAirDate = GetNextAirDate(episodes); historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
List<HistoryEpisode> allHistoryEpisodes =[]; List<HistoryEpisode> allHistoryEpisodes = [];
foreach (var historySeriesSeason in historySeries.Seasons){ foreach (var historySeriesSeason in historySeries.Seasons){
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList); allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
@ -659,7 +783,7 @@ public class History{
.ToList(); .ToList();
} }
List<HistoryEpisode> failedEpisodes =[]; List<HistoryEpisode> failedEpisodes = [];
Parallel.ForEach(allHistoryEpisodes, historyEpisode => { Parallel.ForEach(allHistoryEpisodes, historyEpisode => {
if (string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){ if (string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){

View file

@ -139,8 +139,8 @@ public partial class QueueManager : ObservableObject{
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", ""); (HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", "");
if (CrunchyrollManager.Instance.CrunOptions.History){ if (CrunchyrollManager.Instance.CrunOptions.History){
var episode = sList.EpisodeAndLanguages.Items.First(); var variant = sList.EpisodeAndLanguages.Variants.First();
historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(episode.SeriesId, episode.SeasonId, episode.Id); historyEpisode = CrunchyrollManager.Instance.History.GetHistoryEpisodeWithDubListAndDownloadDir(variant.Item.SeriesId, variant.Item.SeasonId, variant.Item.Id);
if (historyEpisode.dublist.Count > 0){ if (historyEpisode.dublist.Count > 0){
dubLang = historyEpisode.dublist; dubLang = historyEpisode.dublist;
} }
@ -238,8 +238,9 @@ public partial class QueueManager : ObservableObject{
Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs"); Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs");
Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: "); Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: ");
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) => var languages = sList.EpisodeAndLanguages.Variants
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.CrLocale ?? "Unknown"}").ToArray(); .Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.CrLocale ?? "Unknown"}")
.ToArray();
Console.Error.WriteLine( Console.Error.WriteLine(
$"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]"); $"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
@ -252,8 +253,9 @@ public partial class QueueManager : ObservableObject{
Console.WriteLine("Episode couldn't be added to Queue"); Console.WriteLine("Episode couldn't be added to Queue");
Console.Error.WriteLine("Episode couldn't be added to Queue - Available dubs/subs: "); Console.Error.WriteLine("Episode couldn't be added to Queue - Available dubs/subs: ");
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) => var languages = sList.EpisodeAndLanguages.Variants
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index)?.CrLocale ?? "Unknown"}").ToArray(); .Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.CrLocale ?? "Unknown"}")
.ToArray();
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]"); Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2)); MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
@ -374,7 +376,7 @@ public partial class QueueManager : ObservableObject{
public async Task CrAddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){ public async Task CrAddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){
var selected = CrunchyrollManager.Instance.CrSeries.ItemSelectMultiDub(list.Data, data.DubLang, data.But, data.AllEpisodes, data.E); var selected = CrunchyrollManager.Instance.CrSeries.ItemSelectMultiDub(list.Data, data.DubLang, data.AllEpisodes, data.E);
var failed = false; var failed = false;
var partialAdd = false; var partialAdd = false;

View file

@ -310,6 +310,9 @@ public class CrDownloadOptions{
[JsonProperty("calendar_show_upcoming_episodes")] [JsonProperty("calendar_show_upcoming_episodes")]
public bool CalendarShowUpcomingEpisodes{ get; set; } public bool CalendarShowUpcomingEpisodes{ get; set; }
[JsonProperty("calendar_update_history")]
public bool UpdateHistoryFromCalendar{ get; set; }
[JsonProperty("stream_endpoint_settings")] [JsonProperty("stream_endpoint_settings")]
public CrAuthSettings? StreamEndpoint{ get; set; } public CrAuthSettings? StreamEndpoint{ get; set; }

View file

@ -190,7 +190,7 @@ public class CrBrowseEpisodeVersion{
public Locale? AudioLocale{ get; set; } public Locale? AudioLocale{ get; set; }
public string? Guid{ get; set; } public string? Guid{ get; set; }
public bool? Original{ get; set; } public bool Original{ get; set; }
public string? Variant{ get; set; } public string? Variant{ get; set; }
[JsonProperty("season_guid")] [JsonProperty("season_guid")]
@ -200,6 +200,6 @@ public class CrBrowseEpisodeVersion{
public string? MediaGuid{ get; set; } public string? MediaGuid{ get; set; }
[JsonProperty("is_premium_only")] [JsonProperty("is_premium_only")]
public bool? IsPremiumOnly{ get; set; } public bool IsPremiumOnly{ get; set; }
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.Views; using CRD.Views;
@ -51,9 +52,18 @@ public class LanguageItem{
public string Language{ get; set; } public string Language{ get; set; }
} }
public readonly record struct EpisodeVariant(CrunchyEpisode Item, LanguageItem Lang);
public class EpisodeAndLanguage{ public class EpisodeAndLanguage{
public List<CrunchyEpisode> Items{ get; set; } public List<EpisodeVariant> Variants{ get; set; } = new();
public List<LanguageItem> Langs{ get; set; }
public bool AddUnique(CrunchyEpisode item, LanguageItem lang){
if (Variants.Any(v => v.Lang.CrLocale == lang.CrLocale))
return false;
Variants.Add(new EpisodeVariant(item, lang));
return true;
}
} }
public class 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){

View file

@ -31,6 +31,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _showUpcomingEpisodes; private bool _showUpcomingEpisodes;
[ObservableProperty]
private bool _updateHistoryFromCalendar;
[ObservableProperty] [ObservableProperty]
private bool _hideDubs; private bool _hideDubs;
@ -74,6 +77,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar; CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs; HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes; ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
UpdateHistoryFromCalendar = CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar;
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null; ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0]; CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0];
@ -289,4 +293,14 @@ public partial class CalendarPageViewModel : ViewModelBase{
CfgManager.WriteCrSettings(); CfgManager.WriteCrSettings();
} }
} }
partial void OnUpdateHistoryFromCalendarChanged(bool value){
if (loading){
return;
}
CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar = value;
CfgManager.WriteCrSettings();
}
} }

View file

@ -44,17 +44,17 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private ComboBoxItem? _selectedView; private ComboBoxItem? _selectedView;
public ObservableCollection<ComboBoxItem> ViewsList{ get; } =[]; public ObservableCollection<ComboBoxItem> ViewsList{ get; } = [];
[ObservableProperty] [ObservableProperty]
private SortingListElement? _selectedSorting; private SortingListElement? _selectedSorting;
public ObservableCollection<SortingListElement> SortingList{ get; } =[]; public ObservableCollection<SortingListElement> SortingList{ get; } = [];
[ObservableProperty] [ObservableProperty]
private FilterListElement? _selectedFilter; private FilterListElement? _selectedFilter;
public ObservableCollection<FilterListElement> FilterList{ get; } =[]; public ObservableCollection<FilterListElement> FilterList{ get; } = [];
[ObservableProperty] [ObservableProperty]
private double _posterWidth; private double _posterWidth;
@ -115,7 +115,16 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private static string _progressText; private static string _progressText;
[ObservableProperty]
private string _searchInput;
[ObservableProperty]
private bool _isSearchOpen;
[ObservableProperty]
public bool _isSearchActiveClosed;
#region Table Mode #region Table Mode
[ObservableProperty] [ObservableProperty]
@ -125,8 +134,8 @@ public partial class HistoryPageViewModel : ViewModelBase{
public Symbol _selectedDownloadIcon = Symbol.ClosedCaption; public Symbol _selectedDownloadIcon = Symbol.ClosedCaption;
#endregion #endregion
public Vector LastScrollOffset { get; set; } = Vector.Zero; public Vector LastScrollOffset{ get; set; } = Vector.Zero;
public HistoryPageViewModel(){ public HistoryPageViewModel(){
ProgramManager = ProgramManager.Instance; ProgramManager = ProgramManager.Instance;
@ -324,11 +333,32 @@ public partial class HistoryPageViewModel : ViewModelBase{
filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Series); filteredItems.RemoveAll(item => item.SeriesType == SeriesType.Series);
} }
if (!string.IsNullOrWhiteSpace(SearchInput)){
var tokens = SearchInput
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
filteredItems.RemoveAll(item => {
var title = item.SeriesTitle ?? string.Empty;
return tokens.Any(t => title.IndexOf(t, StringComparison.OrdinalIgnoreCase) < 0);
});
}
FilteredItems.Clear(); FilteredItems.Clear();
FilteredItems.AddRange(filteredItems); FilteredItems.AddRange(filteredItems);
} }
partial void OnSearchInputChanged(string value){
ApplyFilter();
}
partial void OnIsSearchOpenChanged(bool value){
IsSearchActiveClosed = !string.IsNullOrEmpty(SearchInput) && !IsSearchOpen;
}
partial void OnScaleValueChanged(double value){ partial void OnScaleValueChanged(double value){
double t = (ScaleValue - 0.5) / (1 - 0.5); double t = (ScaleValue - 0.5) / (1 - 0.5);
@ -374,6 +404,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
} }
} }
[RelayCommand]
public void ClearSearchCommand(){
SearchInput = "";
}
[RelayCommand] [RelayCommand]
public void NavToSeries(){ public void NavToSeries(){
@ -539,7 +573,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
await episode.DownloadEpisode(); await episode.DownloadEpisode();
} }
} }
[RelayCommand] [RelayCommand]
public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){ public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){
var downloadMode = SelectedDownloadMode; var downloadMode = SelectedDownloadMode;
@ -606,7 +640,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
public void ToggleInactive(){ public void ToggleInactive(){
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
} }
partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){ partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
SelectedDownloadIcon = SelectedDownloadMode switch{ SelectedDownloadIcon = SelectedDownloadMode switch{
EpisodeDownloadMode.OnlyVideo => Symbol.Video, EpisodeDownloadMode.OnlyVideo => Symbol.Video,

View file

@ -99,6 +99,10 @@
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}" <CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
Content="Show Upcoming episodes" Margin="5 5 0 0"> Content="Show Upcoming episodes" Margin="5 5 0 0">
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding UpdateHistoryFromCalendar}"
Content="Update History from Calendar" Margin="5 5 0 0">
</CheckBox>
</StackPanel> </StackPanel>
</controls:SettingsExpander.Footer> </controls:SettingsExpander.Footer>
@ -185,7 +189,8 @@
<Grid HorizontalAlignment="Center"> <Grid HorizontalAlignment="Center">
<Grid> <Grid>
<Image HorizontalAlignment="Center" IsVisible="{Binding !AnilistEpisode}" Source="../Assets/coming_soon_ep.jpg" /> <Image HorizontalAlignment="Center" IsVisible="{Binding !AnilistEpisode}" Source="../Assets/coming_soon_ep.jpg" />
<Image HorizontalAlignment="Center" MaxHeight="150" Source="{Binding ImageBitmap}" /> <Image HorizontalAlignment="Center" IsVisible="{Binding !AnilistEpisode}" Source="{Binding ImageBitmap}" />
<Image HorizontalAlignment="Center" IsVisible="{Binding AnilistEpisode}" MaxHeight="150" Source="{Binding ImageBitmap}" />
</Grid> </Grid>

View file

@ -13,7 +13,7 @@
Unloaded="OnUnloaded" Unloaded="OnUnloaded"
Loaded="Control_OnLoaded"> Loaded="Control_OnLoaded">
<UserControl.Resources> <UserControl.Resources>
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" /> <ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" /> <ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
<ui:UiListToStringConverter x:Key="UiListToStringConverter" /> <ui:UiListToStringConverter x:Key="UiListToStringConverter" />
@ -70,6 +70,48 @@
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
<StackPanel>
<ToggleButton x:Name="DropdownButtonSearch" Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsChecked="{Binding IsSearchOpen, Mode=TwoWay}"
IsEnabled="{Binding !ProgramManager.FetchingData}">
<Grid>
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Zoom" FontSize="32" />
<TextBlock Text="Search" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel>
<Ellipse Width="10" Height="10"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,0,0,0"
Fill="Orange"
IsHitTestVisible="False"
IsVisible="{Binding IsSearchActiveClosed}" />
</Grid>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsSearchOpen, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonSearch}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel Orientation="Horizontal" Margin="10">
<TextBox x:Name="SearchBar" Width="160"
Watermark="Search"
Text="{Binding SearchInput, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="✕" Margin="6,0,0,0"
Command="{Binding ClearSearchCommand}" />
</StackPanel>
</Border>
</Popup>
</StackPanel>
<Rectangle Width="1" Height="50" Fill="Gray" Margin="10,0" /> <Rectangle Width="1" Height="50" Fill="Gray" Margin="10,0" />
<StackPanel Margin="10,0"> <StackPanel Margin="10,0">
@ -115,6 +157,7 @@
<!-- <ToggleButton IsChecked="{Binding EditMode}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton> --> <!-- <ToggleButton IsChecked="{Binding EditMode}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton> -->
</StackPanel> </StackPanel>
<StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal"> <StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal">
<Slider VerticalAlignment="Center" Minimum="0.5" Maximum="1" Width="100" <Slider VerticalAlignment="Center" Minimum="0.5" Maximum="1" Width="100"