mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-04-21 08:51:59 +00:00
- 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:
parent
6abbc129b6
commit
973c45ce5c
15 changed files with 776 additions and 282 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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){
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
89
CRD/Downloader/Crunchyroll/Utils/DownloadQueueItemFactory.cs
Normal file
89
CRD/Downloader/Crunchyroll/Utils/DownloadQueueItemFactory.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
122
CRD/Downloader/Crunchyroll/Utils/EpisodeMapper.cs
Normal file
122
CRD/Downloader/Crunchyroll/Utils/EpisodeMapper.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)){
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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){
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue