diff --git a/Shokofin/Configuration/PluginConfiguration.cs b/Shokofin/Configuration/PluginConfiguration.cs index c25a416c..7358a7d0 100644 --- a/Shokofin/Configuration/PluginConfiguration.cs +++ b/Shokofin/Configuration/PluginConfiguration.cs @@ -96,9 +96,14 @@ public virtual string PrettyUrl public bool MarkSpecialsWhenGrouped { get; set; } /// - /// The description source. This will be replaced in the future. + /// The collection of providers for descriptions. Replaces the former `DescriptionSource`. /// - public TextSourceType DescriptionSource { get; set; } + public TextSourceType[] DescriptionSourceList { get; set; } + + /// + /// The prioritisation order of source providers for description sources. + /// + public TextSourceType[] DescriptionSourceOrder { get; set; } /// /// Clean up links within the AniDB description for entries. @@ -286,7 +291,8 @@ public PluginConfiguration() TitleMainType = DisplayLanguageType.Default; TitleAlternateType = DisplayLanguageType.Origin; TitleAllowAny = false; - DescriptionSource = TextSourceType.Default; + DescriptionSourceList = new[] { TextSourceType.AniDb, TextSourceType.TvDb, TextSourceType.TMDB }; + DescriptionSourceOrder = new[] { TextSourceType.AniDb, TextSourceType.TvDb, TextSourceType.TMDB }; VirtualFileSystem = true; VirtualFileSystemThreads = 10; UseGroupsForShows = false; diff --git a/Shokofin/Configuration/configController.js b/Shokofin/Configuration/configController.js index 94ce8bdf..df841d99 100644 --- a/Shokofin/Configuration/configController.js +++ b/Shokofin/Configuration/configController.js @@ -46,6 +46,56 @@ const Messages = { // Convert it back into an array. return Array.from(filteredSet).sort((a, b) => a - b); + } + + function adjustSortableListElement(element) { + const btnSortable = element.querySelector(".btnSortable"); + const inner = btnSortable.querySelector(".material-icons"); + + if (element.previousElementSibling) { + btnSortable.title = "Up"; + btnSortable.classList.add("btnSortableMoveUp"); + inner.classList.add("keyboard_arrow_up"); + + btnSortable.classList.remove("btnSortableMoveDown"); + inner.classList.remove("keyboard_arrow_down"); + } else { + btnSortable.title = "Down"; + btnSortable.classList.add("btnSortableMoveDown"); + inner.classList.add("keyboard_arrow_down"); + + btnSortable.classList.remove("btnSortableMoveUp"); + inner.classList.remove("keyboard_arrow_up"); + } +} + +/** @param {PointerEvent} event */ +function onSortableContainerClick(event) { + const parentWithClass = (element, className) => { + return (element.parentElement.classList.contains(className)) ? element.parentElement : null; + } + const btnSortable = parentWithClass(event.target, "btnSortable"); + if (btnSortable) { + const listItem = parentWithClass(btnSortable, "sortableOption"); + const list = parentWithClass(listItem, "paperList"); + if (btnSortable.classList.contains("btnSortableMoveDown")) { + const next = listItem.nextElementSibling; + if (next) { + listItem.parentElement.removeChild(listItem); + next.parentElement.insertBefore(listItem, next.nextSibling); + } + } else { + const prev = listItem.previousElementSibling; + if (prev) { + listItem.parentElement.removeChild(listItem); + prev.parentElement.insertBefore(listItem, prev); + } + } + + for (const option of list.querySelectorAll(".sortableOption")) { + adjustSortableListElement(option) + }; + } } async function loadUserConfig(form, userId, config) { @@ -227,7 +277,7 @@ async function defaultSubmit(form) { config.TitleAllowAny = form.querySelector("#TitleAllowAny").checked; config.TitleAddForMultipleEpisodes = form.querySelector("#TitleAddForMultipleEpisodes").checked; config.MarkSpecialsWhenGrouped = form.querySelector("#MarkSpecialsWhenGrouped").checked; - config.DescriptionSource = form.querySelector("#DescriptionSource").value; + setDescriptionSourcesIntoConfig(form, config); config.SynopsisCleanLinks = form.querySelector("#CleanupAniDBDescriptions").checked; config.SynopsisCleanMultiEmptyLines = form.querySelector("#CleanupAniDBDescriptions").checked; config.SynopsisCleanMiscLines = form.querySelector("#MinimalAniDBDescriptions").checked; @@ -417,7 +467,7 @@ async function syncSettings(form) { config.TitleAllowAny = form.querySelector("#TitleAllowAny").checked; config.TitleAddForMultipleEpisodes = form.querySelector("#TitleAddForMultipleEpisodes").checked; config.MarkSpecialsWhenGrouped = form.querySelector("#MarkSpecialsWhenGrouped").checked; - config.DescriptionSource = form.querySelector("#DescriptionSource").value; + setDescriptionSourcesIntoConfig(form, config); config.SynopsisCleanLinks = form.querySelector("#CleanupAniDBDescriptions").checked; config.SynopsisCleanMultiEmptyLines = form.querySelector("#CleanupAniDBDescriptions").checked; config.SynopsisCleanMiscLines = form.querySelector("#MinimalAniDBDescriptions").checked; @@ -689,6 +739,8 @@ export default function (page) { } }); + form.querySelector("#descriptionSourceList").addEventListener("click", onSortableContainerClick); + page.addEventListener("viewshow", async function () { Dashboard.showLoadingMsg(); try { @@ -707,7 +759,7 @@ export default function (page) { form.querySelector("#TitleAllowAny").checked = config.TitleAllowAny; form.querySelector("#TitleAddForMultipleEpisodes").checked = config.TitleAddForMultipleEpisodes != null ? config.TitleAddForMultipleEpisodes : true; form.querySelector("#MarkSpecialsWhenGrouped").checked = config.MarkSpecialsWhenGrouped; - form.querySelector("#DescriptionSource").value = config.DescriptionSource; + await setDescriptionSourcesFromConfig(form, config); form.querySelector("#CleanupAniDBDescriptions").checked = config.SynopsisCleanMultiEmptyLines || config.SynopsisCleanLinks; form.querySelector("#MinimalAniDBDescriptions").checked = config.SynopsisRemoveSummary || config.SynopsisCleanMiscLines; @@ -819,3 +871,39 @@ export default function (page) { return false; }); } + +function setDescriptionSourcesIntoConfig(form, config) { + const descriptionElements = form.querySelectorAll(`#descriptionSourceList .chkDescriptionSource`); + config.DescriptionSourceList = Array.prototype.filter.call(descriptionElements, + (el) => el.checked) + .map((el) => el.dataset.descriptionsource); + + config.DescriptionSourceOrder = Array.prototype.map.call(descriptionElements, + (el) => el.dataset.descriptionsource + ); +} + +async function setDescriptionSourcesFromConfig(form, config) { + const list = form.querySelector("#descriptionSourceList .checkboxList"); + const listItems = list.querySelectorAll('.listItem'); + + for (const item of listItems) { + const source = item.dataset.descriptionsource; + if (config.DescriptionSourceList.includes(source)) { + item.querySelector(".chkDescriptionSource").checked = true; + } + if (config.DescriptionSourceOrder.includes(source)) { + list.removeChild(item); // This is safe to be removed as we can re-add it in the next loop + } + } + + for (const source of config.DescriptionSourceOrder) { + const targetElement = Array.prototype.find.call(listItems, (el) => el.dataset.descriptionsource === source); + if (targetElement) { + list.append(targetElement); + } + } + for (const option of list.querySelectorAll(".sortableOption")) { + adjustSortableListElement(option) + }; +} diff --git a/Shokofin/Configuration/configPage.html b/Shokofin/Configuration/configPage.html index 73040651..30507c0e 100644 --- a/Shokofin/Configuration/configPage.html +++ b/Shokofin/Configuration/configPage.html @@ -72,16 +72,26 @@

Metadata Settings

Add a number to the title of each specials episode
-
- - -
How to select the description to use for each item.
+
+

Description source:

+
+
+ +

AniDB

+ +
+
+ +

TVDB

+ +
+
+ +

TMDB

+ +
+
+
The metadata providers to use as the source of episode/series/season descriptions.
-
Trim any lines starting with '* ', '-- ', '~ ', 'Note', 'Source', and/or 'Summery'
+
Trim any lines starting with '* ', '-- ', '~ ', 'Note', 'Source', and/or 'Summary'
public enum TextSourceType { /// - /// Use the default source for the current series grouping. + /// Use data from AniDB. /// - Default = 1, - - /// - /// Only use AniDb, or null if no data is available. - /// - OnlyAniDb = 2, + AniDb = 0, /// - /// Prefer the AniDb data, but use the other provider if there is no - /// AniDb data available. + /// Use data from TvDB. /// - PreferAniDb = 3, + TvDb = 1, /// - /// Prefer the other provider (e.g. TvDB/TMDB) + /// Use data from TMDB /// - PreferOther = 4, - - /// - /// Only use the other provider, or null if no data is available. - /// - OnlyOther = 5, + TMDB = 2 } /// @@ -149,40 +138,49 @@ public enum DisplayTitleType { FullTitle = 3, } - public static string? GetDescription(ShowInfo show) + public static string GetDescription(ShowInfo show) => GetDescription(show.DefaultSeason); - public static string? GetDescription(SeasonInfo season) - => GetDescription(season.AniDB.Description, season.TvDB?.Description); + public static string GetDescription(SeasonInfo season) + => GetDescription(new Dictionary() { + {TextSourceType.AniDb, season.AniDB.Description ?? string.Empty}, + {TextSourceType.TvDb, season.TvDB?.Description ?? string.Empty}, + }); - public static string? GetDescription(EpisodeInfo episode) - => GetDescription(episode.AniDB.Description, episode.TvDB?.Description); + public static string GetDescription(EpisodeInfo episode) + => GetDescription(new Dictionary() { + {TextSourceType.AniDb, episode.AniDB.Description ?? string.Empty}, + {TextSourceType.TvDb, episode.TvDB?.Description ?? string.Empty}, + }); - public static string? GetDescription(IEnumerable episodeList) - => JoinText(episodeList.Select(episode => GetDescription(episode))); + public static string GetDescription(IEnumerable episodeList) + => JoinText(episodeList.Select(episode => GetDescription(episode))) ?? string.Empty; - private static string GetDescription(string aniDbDescription, string? otherDescription) + private static string GetDescription(Dictionary descriptions) { - string overview; - switch (Plugin.Instance.Configuration.DescriptionSource) { - default: - case TextSourceType.PreferAniDb: - overview = SanitizeTextSummary(aniDbDescription); - if (string.IsNullOrEmpty(overview)) - goto case TextSourceType.OnlyOther; - break; - case TextSourceType.PreferOther: - overview = otherDescription ?? string.Empty; - if (string.IsNullOrEmpty(overview)) - goto case TextSourceType.OnlyAniDb; - break; - case TextSourceType.OnlyAniDb: - overview = SanitizeTextSummary(aniDbDescription); - break; - case TextSourceType.OnlyOther: - overview = otherDescription ?? string.Empty; - break; + var overview = string.Empty; + + var providerOrder = Plugin.Instance.Configuration.DescriptionSourceOrder; + var providers = Plugin.Instance.Configuration.DescriptionSourceList; + + if (providers.Length == 0) { + return overview; // This is what they want if everything is unticked... } + + foreach (var provider in providerOrder.Where(provider => providers.Contains(provider))) + { + if (!string.IsNullOrEmpty(overview)) { + return overview; + } + + overview = provider switch + { + TextSourceType.AniDb => descriptions.TryGetValue(TextSourceType.AniDb, out var desc) ? SanitizeTextSummary(desc) : string.Empty, + TextSourceType.TvDb => descriptions.TryGetValue(TextSourceType.TvDb, out var desc) ? desc : string.Empty, + _ => string.Empty + }; + } + return overview; } @@ -217,7 +215,7 @@ public static (string?, string?) GetEpisodeTitles(IEnumerable seriesTitle => GetTitles(seriesTitles, episodeTitles, null, episodeTitle, DisplayTitleType.SubTitle, metadataLanguage); public static (string?, string?) GetSeriesTitles(IEnumerable<Title> seriesTitles, string seriesTitle, string metadataLanguage) - => GetTitles(seriesTitles, null, seriesTitle, null, DisplayTitleType.MainTitle, metadataLanguage); + => GetTitles(seriesTitles, null, seriesTitle, null, DisplayTitleType.MainTitle, metadataLanguage); public static (string?, string?) GetMovieTitles(IEnumerable<Title> seriesTitles, IEnumerable<Title> episodeTitles, string seriesTitle, string episodeTitle, string metadataLanguage) => GetTitles(seriesTitles, episodeTitles, seriesTitle, episodeTitle, DisplayTitleType.FullTitle, metadataLanguage);