Release 0.3.4: Jellyfin 12 stable support and audit fixes
Harden proxy permissions and cache behavior, streamline metadata fetching, remove obsolete rules, and validate the stable SDK with expanded regression coverage. Document backup format changes.
This commit is contained in:
@@ -109,8 +109,19 @@
|
||||
var ISO3166 = M.staticData.iso3166;
|
||||
var LANG_REGION_COMBOS = M.staticData.langRegionCombos;
|
||||
|
||||
var listeners = [];
|
||||
function listen(target, type, handler, capture) {
|
||||
target.addEventListener(type, handler, capture);
|
||||
listeners.push(() => target.removeEventListener(type, handler, capture));
|
||||
}
|
||||
page._mlDispose = function () {
|
||||
listeners.forEach(remove => remove());
|
||||
listeners = [];
|
||||
cleanupActionDrag();
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
return page.querySelector("#" + id);
|
||||
}
|
||||
|
||||
function make(tag, className, text) {
|
||||
@@ -144,18 +155,18 @@
|
||||
}
|
||||
|
||||
function categoryId(category) {
|
||||
return category.Id || category.id || "";
|
||||
return category.Id;
|
||||
}
|
||||
|
||||
function categoryLabel(category, index) {
|
||||
var label = String(category.Label || category.label || "").trim();
|
||||
var label = String(category.Label || "").trim();
|
||||
return label || categoryPlaceholder(index);
|
||||
}
|
||||
|
||||
function categoryPlaceholder(index) {
|
||||
var used = {};
|
||||
categories().forEach(function (category) {
|
||||
var label = String(category.Label || category.label || "").trim().toLowerCase();
|
||||
var label = String(category.Label || "").trim().toLowerCase();
|
||||
if (label) used[label] = true;
|
||||
});
|
||||
var n = Math.max(1, index + 1);
|
||||
@@ -163,11 +174,10 @@
|
||||
return "Category " + n;
|
||||
}
|
||||
|
||||
function newCategory(label, criteria) {
|
||||
function newCategory(label) {
|
||||
return {
|
||||
Id: randomId(),
|
||||
Label: label || "",
|
||||
CriteriaText: criteria || "",
|
||||
Requirements: [emptyRequirement()],
|
||||
MatchAllConditions: true,
|
||||
Scopes: ["M", "S", "C"],
|
||||
@@ -176,7 +186,7 @@
|
||||
}
|
||||
|
||||
function emptyRequirement() {
|
||||
return { Field: "", Fields: [], UseFieldOr: false, Relation: "", UseOr: false, Values: [] };
|
||||
return { Fields: [], UseFieldOr: false, Relation: "", UseOr: false, Values: [] };
|
||||
}
|
||||
|
||||
function normalizeActionValue(fieldId, value) {
|
||||
@@ -189,13 +199,6 @@
|
||||
return canonical ? "Language:" + canonical : "";
|
||||
}
|
||||
|
||||
function legacyActionToList(fieldId, legacy) {
|
||||
if (!legacy || legacy.toLowerCase() === "fallback") {
|
||||
return ["Jellyfin"];
|
||||
}
|
||||
return [legacy];
|
||||
}
|
||||
|
||||
function normalizeActionList(fieldId, values) {
|
||||
var field = fields.find(function (f) { return f.id === fieldId; });
|
||||
var result = [];
|
||||
@@ -211,64 +214,31 @@
|
||||
return result.length || !(field && field.lockedJ) ? result : ["Jellyfin"];
|
||||
}
|
||||
|
||||
function normalizeActionMap(source, legacy) {
|
||||
if (legacy && looksLikeDefaultActionMap(source) && hasMeaningfulLegacyActions(legacy)) {
|
||||
source = null;
|
||||
}
|
||||
function normalizeActionMap(source) {
|
||||
var result = {};
|
||||
fields.forEach(function (field) {
|
||||
var values = source && (source[field.id] || source[field.label]);
|
||||
if (!values && legacy) values = legacyActionToList(field.id, legacy[field.id] || legacy[field.label]);
|
||||
result[field.id] = normalizeActionList(field.id, Array.isArray(values) ? values : values ? [values] : ["Jellyfin"]);
|
||||
result[field.id] = normalizeActionList(field.id, source[field.id]);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function looksLikeDefaultActionMap(source) {
|
||||
if (!source) return true;
|
||||
return fields.every(function (field) {
|
||||
var values = source[field.id] || source[field.label];
|
||||
return Array.isArray(values) && values.length === 1 && String(values[0]).toLowerCase() === "jellyfin";
|
||||
});
|
||||
}
|
||||
|
||||
function hasMeaningfulLegacyActions(legacy) {
|
||||
return Object.keys(legacy || {}).some(function (key) {
|
||||
var action = String(legacy[key] || "").toLowerCase();
|
||||
return action && action !== "fallback" && action !== "jellyfin";
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeRules() {
|
||||
state.rules = state.rules || {};
|
||||
state.rules.Categories = (state.rules.Categories || []).filter(function (category) {
|
||||
var id = categoryId(category).toLowerCase();
|
||||
var label = String(category.Label || category.label || "").trim().toLowerCase();
|
||||
var criteria = String(category.CriteriaText || category.criteriaText || "").trim();
|
||||
return !(id === "fallback" && label === "fallback" && criteria.length === 0);
|
||||
}).map(function (category, index) {
|
||||
category.Id = categoryId(category) || randomId();
|
||||
category.Label = String(category.Label || category.label || "").trim();
|
||||
category.CriteriaText = category.CriteriaText || category.criteriaText || "";
|
||||
category.Requirements = normalizeRequirements(category.Requirements || category.requirements);
|
||||
category.MatchAllConditions = category.MatchAllConditions !== false && category.matchAllConditions !== false;
|
||||
category.Scopes = category.Scopes || category.scopes || ["M", "S", "C"];
|
||||
category.FieldActionLists = normalizeActionMap(category.FieldActionLists || category.fieldActionLists, category.FieldActions || category.fieldActions);
|
||||
return category;
|
||||
state.rules.Categories.forEach(function (category) {
|
||||
category.Requirements = normalizeRequirements(category.Requirements);
|
||||
category.FieldActionLists = normalizeActionMap(category.FieldActionLists);
|
||||
});
|
||||
state.rules.FallbackFieldActions = normalizeActionMap(state.rules.FallbackFieldActions || state.rules.fallbackFieldActions, null);
|
||||
state.rules.FallbackFieldActions = normalizeActionMap(state.rules.FallbackFieldActions);
|
||||
}
|
||||
|
||||
function normalizeRequirements(requirements) {
|
||||
var normalized = (requirements || []).map(function (requirement) {
|
||||
var fields = normalizeRequirementFields(requirement.Fields || requirement.fields, requirement.Field || requirement.field);
|
||||
var fields = normalizeRequirementFields(requirement.Fields);
|
||||
return {
|
||||
Field: fields[0] || "",
|
||||
Fields: fields,
|
||||
UseFieldOr: requirement.UseFieldOr === true || requirement.useFieldOr === true,
|
||||
Relation: String(requirement.Relation || requirement.relation || "").trim(),
|
||||
UseOr: requirement.UseOr === true || requirement.useOr === true,
|
||||
Values: orderedRequirementValues(fields[0] || "", requirement.Values || requirement.values || [])
|
||||
UseFieldOr: requirement.UseFieldOr === true,
|
||||
Relation: String(requirement.Relation || "").trim(),
|
||||
UseOr: requirement.UseOr === true,
|
||||
Values: orderedRequirementValues(fields[0] || "", requirement.Values || [])
|
||||
};
|
||||
});
|
||||
return normalized.length ? normalized : [emptyRequirement()];
|
||||
@@ -294,14 +264,12 @@
|
||||
return criteriaFields.find(function (item) { return item.id === id; });
|
||||
}
|
||||
|
||||
function normalizeRequirementFields(values, legacy) {
|
||||
function normalizeRequirementFields(values) {
|
||||
var raw = [];
|
||||
(values || []).forEach(function (value) {
|
||||
value = String(value || "").trim();
|
||||
if (value) raw.push(value);
|
||||
});
|
||||
legacy = String(legacy || "").trim();
|
||||
if (legacy) raw.push(legacy);
|
||||
var wanted = {};
|
||||
raw.forEach(function (value) {
|
||||
var id = value.toLowerCase();
|
||||
@@ -319,7 +287,7 @@
|
||||
}
|
||||
|
||||
function requirementFields(requirement) {
|
||||
return normalizeRequirementFields(requirement && (requirement.Fields || requirement.fields), requirement && (requirement.Field || requirement.field));
|
||||
return normalizeRequirementFields(requirement.Fields);
|
||||
}
|
||||
|
||||
function criteriaLabel(id) {
|
||||
@@ -329,7 +297,7 @@
|
||||
|
||||
function renderSortLocales() {
|
||||
var select = $("ml-sort-locale");
|
||||
select.replaceChildren(option("Auto", "Auto", false));
|
||||
M.replaceChildren(select, option("Auto", "Auto", false));
|
||||
state.allowed.forEach(function (lang) {
|
||||
select.appendChild(option(lang, lang, false));
|
||||
});
|
||||
@@ -363,7 +331,6 @@
|
||||
function syncCategoryFromRow(row, category) {
|
||||
category.Label = row.querySelector("[data-category-label]").value;
|
||||
category.MatchAllConditions = row.querySelector("[data-category-match-all]").checked;
|
||||
category.CriteriaText = categoryCriteriaText(category);
|
||||
category.Scopes = Array.prototype.slice.call(row.querySelectorAll("[data-category-scope]:checked"))
|
||||
.map(function (input) { return input.value; });
|
||||
}
|
||||
@@ -376,7 +343,7 @@
|
||||
}
|
||||
|
||||
function addSelectOptions(select, items, selectedValue) {
|
||||
select.replaceChildren();
|
||||
M.replaceChildren(select);
|
||||
items.forEach(function (item) {
|
||||
select.appendChild(option(item.label, item.id, item.id === selectedValue));
|
||||
});
|
||||
@@ -462,23 +429,6 @@
|
||||
return values.join(joiner);
|
||||
}
|
||||
|
||||
function requirementText(requirement) {
|
||||
if (!completeRequirement(requirement)) return "";
|
||||
var values = orderedRequirementValues(requirementFields(requirement)[0] || "", requirement.Values || []);
|
||||
var right = values.length === 1 ? values[0] : "(" + values.join(requirement.UseOr ? " or " : " and ") + ")";
|
||||
var fields = requirementFields(requirement);
|
||||
var left = fields.length === 1 ? fields[0] : "(" + fields.join(requirement.UseFieldOr ? " or " : " and ") + ")";
|
||||
return left + " " + requirement.Relation + " " + right;
|
||||
}
|
||||
|
||||
function categoryCriteriaText(category) {
|
||||
var joiner = category.MatchAllConditions === false ? " or " : " and ";
|
||||
return (category.Requirements || [])
|
||||
.filter(completeRequirement)
|
||||
.map(requirementText)
|
||||
.join(joiner);
|
||||
}
|
||||
|
||||
function makeRequirementRow(category, requirement, index) {
|
||||
var row = make("div", "ml-requirement-row");
|
||||
var selectedFields = requirementFields(requirement);
|
||||
@@ -524,7 +474,7 @@
|
||||
var current = requirement.Relation || "";
|
||||
var fields = requirementFields(requirement);
|
||||
var multiple = fields.length > 1;
|
||||
select.replaceChildren(option("Choose relation", "", !current));
|
||||
M.replaceChildren(select, option("Choose relation", "", !current));
|
||||
if (fields.length) {
|
||||
relations.forEach(function (item) {
|
||||
select.appendChild(option(multiple ? item.pluralLabel : item.label, item.id, item.id === current));
|
||||
@@ -569,7 +519,6 @@
|
||||
fields = normalizeRequirementFields(fields, "");
|
||||
var nextKind = fields.length ? fieldKind(fields[0]) : "";
|
||||
requirement.Fields = fields;
|
||||
requirement.Field = fields[0] || "";
|
||||
button.textContent = fieldSummary(requirement);
|
||||
refreshFieldPanelOptions(panel, requirement);
|
||||
if (onChange) onChange(previousKind, nextKind);
|
||||
@@ -674,7 +623,6 @@
|
||||
function cloneRequirements(requirements) {
|
||||
return normalizeRequirements(requirements).map(function (requirement) {
|
||||
return {
|
||||
Field: requirement.Field,
|
||||
Fields: (requirement.Fields || []).slice(),
|
||||
UseFieldOr: requirement.UseFieldOr === true,
|
||||
Relation: requirement.Relation,
|
||||
@@ -688,7 +636,6 @@
|
||||
return {
|
||||
Id: randomId(),
|
||||
Label: "",
|
||||
CriteriaText: categoryCriteriaText(category),
|
||||
Requirements: cloneRequirements(category.Requirements),
|
||||
MatchAllConditions: category.MatchAllConditions !== false,
|
||||
Scopes: (category.Scopes || ["M", "S", "C"]).slice(),
|
||||
@@ -698,7 +645,7 @@
|
||||
|
||||
function renderCategories() {
|
||||
var root = $("ml-categories");
|
||||
root.replaceChildren();
|
||||
M.replaceChildren(root);
|
||||
categories().forEach(function (category, index) {
|
||||
var row = make("div", "ml-classification-rule");
|
||||
row.dataset.categoryId = categoryId(category);
|
||||
@@ -809,7 +756,7 @@
|
||||
|
||||
function renderActionSource() {
|
||||
var root = $("ml-action-source");
|
||||
root.replaceChildren();
|
||||
M.replaceChildren(root);
|
||||
actionSourceValues().forEach(function (value) {
|
||||
root.appendChild(makeActionToken(value, false, true));
|
||||
});
|
||||
@@ -831,7 +778,7 @@
|
||||
function renderActionMatrix() {
|
||||
renderActionSource();
|
||||
var root = $("ml-action-matrix");
|
||||
root.replaceChildren();
|
||||
M.replaceChildren(root);
|
||||
actionRows().forEach(function (row) {
|
||||
var line = make("div", "ml-action-category-row");
|
||||
if (row.fallback) line.classList.add("ml-action-category-row-fallback");
|
||||
@@ -1068,7 +1015,7 @@
|
||||
|
||||
function clearCategoryActions(categoryId) {
|
||||
boxesForCategory(categoryId).forEach(function (box) {
|
||||
box.replaceChildren();
|
||||
M.replaceChildren(box);
|
||||
if (actionFieldLocksJ(box.dataset.fieldId)) box.appendChild(makeActionToken("Jellyfin", true, false));
|
||||
});
|
||||
}
|
||||
@@ -1085,7 +1032,7 @@
|
||||
return token.dataset.value !== "Jellyfin";
|
||||
});
|
||||
var token = makeActionToken(value, actionFieldLocksJ(box.dataset.fieldId) && value === "Jellyfin", false);
|
||||
box.replaceChildren();
|
||||
M.replaceChildren(box);
|
||||
if (prepend) box.appendChild(token);
|
||||
existing.forEach(function (item) { box.appendChild(item); });
|
||||
if (!prepend) box.appendChild(token);
|
||||
@@ -1128,7 +1075,6 @@
|
||||
categories().forEach(function (category) {
|
||||
var complete = (category.Requirements || []).filter(completeRequirement);
|
||||
category.Requirements = complete.length ? complete : [emptyRequirement()];
|
||||
category.CriteriaText = complete.map(requirementText).join(" and ");
|
||||
});
|
||||
return {
|
||||
Enabled: $("ml-enabled").checked,
|
||||
@@ -1140,12 +1086,10 @@
|
||||
return {
|
||||
Id: category.Id,
|
||||
Label: categoryLabel(category, index),
|
||||
CriteriaText: complete.map(requirementText).join(category.MatchAllConditions === false ? " or " : " and "),
|
||||
Requirements: complete,
|
||||
MatchAllConditions: category.MatchAllConditions !== false,
|
||||
Scopes: category.Scopes,
|
||||
FieldActionLists: category.FieldActionLists,
|
||||
FieldActions: {}
|
||||
FieldActionLists: category.FieldActionLists
|
||||
};
|
||||
})
|
||||
};
|
||||
@@ -1183,7 +1127,7 @@
|
||||
$("ml-user-import-modal").hidden = !show;
|
||||
if (!show) {
|
||||
$("ml-user-import-choice").hidden = true;
|
||||
$("ml-user-import-source").replaceChildren();
|
||||
M.replaceChildren($("ml-user-import-source"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1251,7 +1195,7 @@
|
||||
}
|
||||
|
||||
var select = $("ml-user-import-source");
|
||||
select.replaceChildren();
|
||||
M.replaceChildren(select);
|
||||
ids.forEach(function (id) { select.appendChild(option(id, id, false)); });
|
||||
choice.hidden = false;
|
||||
M.setStatus(status, "No matching user ID was found. Choose which exported user settings to import.", true);
|
||||
@@ -1295,26 +1239,26 @@
|
||||
$("ml-user-import-run").addEventListener("click", function () {
|
||||
runUserImport().catch(function (err) { M.setStatus(status, String(err), true); });
|
||||
});
|
||||
document.addEventListener("click", function () { closeValuePanels(null); });
|
||||
listen(document, "click", function () { closeValuePanels(null); });
|
||||
$("ml-add-category").addEventListener("click", function () {
|
||||
syncCategoriesFromDom();
|
||||
updateActionStateFromBoxes();
|
||||
categories().push(newCategory("", ""));
|
||||
categories().push(newCategory(""));
|
||||
renderAll();
|
||||
});
|
||||
document.addEventListener("dragstart", function (event) {
|
||||
listen(document, "dragstart", function (event) {
|
||||
if (event.target.closest && event.target.closest(".ml-action-token")) event.preventDefault();
|
||||
}, true);
|
||||
page.addEventListener("pointerdown", beginActionDrag, true);
|
||||
document.addEventListener("pointermove", moveActionDrag, true);
|
||||
document.addEventListener("pointerup", endActionDrag, true);
|
||||
document.addEventListener("pointercancel", function (event) {
|
||||
listen(document, "pointermove", moveActionDrag, true);
|
||||
listen(document, "pointerup", endActionDrag, true);
|
||||
listen(document, "pointercancel", function (event) {
|
||||
if (!actionDrag || event.pointerId !== actionDrag.pointerId) return;
|
||||
restoreActionDrag();
|
||||
cleanupActionDrag();
|
||||
updateActionStateFromBoxes();
|
||||
}, true);
|
||||
document.addEventListener("viewshow", function (event) {
|
||||
listen(document, "viewshow", function (event) {
|
||||
if (event.target && event.target.id === "ml-user") load();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user