Plex: edit a title's details (fields locked on save) and choose its poster or background, owner only, through a narrow write rule in the proxy
build-container / build (push) Successful in 19m31s
build-container / build (push) Successful in 19m31s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b171f35be6
commit
5c5a7b5155
5 files changed
+113
-5
No files matched your search
@@ -109,6 +109,9 @@ async function videoItem(id) {
|
||||
<p class="vsum">${esc(m.summary || '')}</p>
|
||||
${m.Genre && m.Genre.length ? `<p class="sub"><b>Genres</b> ${names(m.Genre)}</p>` : ''}${m.Director && m.Director.length ? `<p class="sub"><b>Director</b> ${names(m.Director, 'tag', 3)}</p>` : ''}${m.Role && m.Role.length ? `<p class="sub"><b>Cast</b> ${names(m.Role, 'tag', 8)}</p>` : ''}
|
||||
<div id="vkids"></div></div>`;
|
||||
jfetch('/api/plex/status').then(st => { // Plex only lets its owner edit details, so the button is offered only then
|
||||
if (st.canEdit && $('#vact') && !$('#vedit')) { $('#vact').insertAdjacentHTML('beforeend', '<button class="btn ghost" id="vedit">Edit</button>'); $('#vedit').onclick = () => videoEdit(m); }
|
||||
}).catch(() => {});
|
||||
if ($('#vplay')) $('#vplay').onclick = () => { location.hash = `#/video/watch/${id}`; };
|
||||
if ($('#vstart')) $('#vstart').onclick = () => { store.set('video_from_start', id); location.hash = `#/video/watch/${id}`; };
|
||||
if ($('#vnext')) $('#vnext').onclick = () => { location.hash = `#/video/watch/${next.ratingKey}`; };
|
||||
@@ -394,3 +397,55 @@ addEventListener('keydown', e => {
|
||||
else if (k === 'm') el.muted = !el.muted; else if (k === 'n' || k === 'N') videoGo(1); else if (k === 'p' || k === 'P') videoGo(-1);
|
||||
else if (/^[0-9]$/.test(k) && vp.m.duration) { videoSeek(vp.m.duration / 1000 * (+k / 10)); }
|
||||
}, true);
|
||||
|
||||
|
||||
// ---- Edit a title's details the way Plex's Edit dialog does: only what changed is saved, and each saved field is locked so a Plex refresh keeps it.
|
||||
const VD_TYPE = { movie: 1, show: 2, season: 3, episode: 4 };
|
||||
async function videoEdit(m) {
|
||||
const kind = m.type, rich = kind === 'movie' || kind === 'show', genres = (m.Genre || []).map(g => g.tag);
|
||||
const F = [['title', 'Title'], ['titleSort', 'Sort title'], ['originalTitle', 'Original title'], ['originallyAvailableAt', kind === 'show' ? 'First aired' : 'Release date', 'date'], ['contentRating', 'Content rating'], ...(rich ? [['studio', 'Studio'], ['tagline', 'Tagline']] : []), ['summary', 'Summary', 'area']]
|
||||
.filter(([k]) => kind !== 'season' || ['title', 'titleSort', 'summary'].includes(k));
|
||||
const old = k => String(m[k] || ''), dateOf = v => String(v || '').slice(0, 10);
|
||||
const el = openSheet('Edit ' + (m.title || ''), '', `<div class="sgrp" role="tablist" style="margin-bottom:8px"><a href="#" data-t="d" aria-selected="true" class="on">Details</a><a href="#" data-t="p" aria-selected="false">Poster</a><a href="#" data-t="a" aria-selected="false">Background</a></div>
|
||||
<form id="vf"><div id="vtab-d">${F.map(([k, l, t]) => `<label for="ve-${k}">${l}</label>${t === 'area' ? `<textarea id="ve-${k}" rows="5">${esc(old(k))}</textarea>` : `<input type="${t || 'text'}" id="ve-${k}" value="${esc(t === 'date' ? dateOf(m[k]) : old(k))}" autocomplete="off">`}`).join('')}
|
||||
${rich ? `<label for="ve-genre">Genres <small>(separate with commas)</small></label><input type="text" id="ve-genre" value="${esc(genres.join(', '))}" autocomplete="off">` : ''}
|
||||
<p class="sub" style="margin:10px 0 0">Changed fields are locked in Plex, so scans and refreshes will not put the old values back.</p></div>
|
||||
<div id="vtab-p" hidden></div><div id="vtab-a" hidden></div>
|
||||
<div class="bar"><button class="btn ghost" type="button" id="sx" style="flex:1;justify-content:center">Cancel</button><button class="btn" id="vs" style="flex:1;justify-content:center">Save</button></div><div class="err" id="ve"></div></form>`);
|
||||
el.querySelector('.sheet').classList.add('wide');
|
||||
el.querySelector('#sx').onclick = closeSheet;
|
||||
const loaded = {};
|
||||
const images = async (tab, what) => { // posters / backgrounds: pick one, or add from an image link
|
||||
const box = el.querySelector('#vtab-' + tab); box.innerHTML = '<p class="empty">Loading…</p>';
|
||||
try {
|
||||
const mc = await px.get(`/library/metadata/${m.ratingKey}/${what}`), list = mc.Metadata || [];
|
||||
box.innerHTML = `<div class="vimgs ${what}">${list.map((x, i) => `<button type="button" class="vpick ${x.selected ? 'on' : ''}" data-i="${i}" aria-label="Use this image"><img alt="" loading="lazy" src="${/^https?:/.test(x.thumb || '') ? esc(x.thumb) : vdArt(x.thumb, 200, 300)}"></button>`).join('') || '<p class="empty">No images yet.</p>'}</div>
|
||||
<label>Add from an image link</label><div class="bar" style="margin-top:4px"><input type="url" class="vurl" placeholder="https://…" autocomplete="off" style="flex:1"><button type="button" class="btn ghost vadd">Add</button></div><div class="err vie"></div>`;
|
||||
const put = async (url, add) => {
|
||||
const r = await fetch(`/plex/library/metadata/${m.ratingKey}/${add ? what : what === 'posters' ? 'poster' : 'art'}?url=${encodeURIComponent(url)}`, { method: add ? 'POST' : 'PUT' });
|
||||
if (!r.ok) throw new Error(r.status === 403 || r.status === 404 ? 'Only the Plex owner can change images' : 'Plex would not take that (' + r.status + ')');
|
||||
};
|
||||
box.querySelectorAll('.vpick').forEach(b => b.onclick = async () => { try { await put(list[+b.dataset.i].ratingKey || list[+b.dataset.i].key); box.querySelectorAll('.vpick').forEach(x => x.classList.toggle('on', x === b)); toast('Image changed'); changed = true; } catch (e) { box.querySelector('.vie').textContent = e.message; } });
|
||||
box.querySelector('.vadd').onclick = async () => { const u = box.querySelector('.vurl').value.trim(); if (!/^https:\/\//.test(u)) { box.querySelector('.vie').textContent = 'Use a link that starts with https://'; return; } try { await put(u, true); toast('Image added'); changed = true; loaded[tab] = false; images(tab, what); } catch (e) { box.querySelector('.vie').textContent = e.message; } };
|
||||
} catch (e) { box.innerHTML = `<p class="err">${esc(e.message)}</p>`; }
|
||||
};
|
||||
let changed = false;
|
||||
el.querySelectorAll('.sgrp a[data-t]').forEach(a => a.onclick = ev => {
|
||||
ev.preventDefault(); const t = a.dataset.t;
|
||||
el.querySelectorAll('.sgrp a[data-t]').forEach(x => { x.classList.toggle('on', x === a); x.setAttribute('aria-selected', x === a); });
|
||||
['d', 'p', 'a'].forEach(k => { el.querySelector('#vtab-' + k).hidden = k !== t; }); el.querySelector('#vs').hidden = t !== 'd';
|
||||
if (t !== 'd' && !loaded[t]) { loaded[t] = true; images(t, t === 'p' ? 'posters' : 'arts'); }
|
||||
});
|
||||
el.querySelector('#vf').onsubmit = async ev => {
|
||||
ev.preventDefault(); const btn = el.querySelector('#vs'); btn.disabled = true; el.querySelector('#ve').textContent = '';
|
||||
const q = new URLSearchParams({ type: VD_TYPE[kind] || 1, id: m.ratingKey, includeExternalMedia: 1 }); let n = 0;
|
||||
for (const [k, , t] of F) { const v = el.querySelector('#ve-' + k).value.trim(); if (v !== (t === 'date' ? dateOf(m[k]) : old(k))) { q.set(k + '.value', v); q.set(k + '.locked', 1); n++; } }
|
||||
if (rich) { const g = [...new Set(el.querySelector('#ve-genre').value.split(',').map(x => x.trim()).filter(Boolean))]; if (g.join('|') !== genres.join('|')) { g.forEach((x, i) => q.set(`genre[${i}].tag.tag`, x)); q.set('genre.locked', 1); n++; } }
|
||||
if (!n) { closeSheet(); if (changed) route(); return; }
|
||||
try {
|
||||
const r = await fetch(`/plex/library/sections/${m.librarySectionID}/all?${q}`, { method: 'PUT' });
|
||||
if (!r.ok) throw new Error(r.status === 403 || r.status === 404 ? 'Only the Plex owner can change details' : 'Plex would not save that (' + r.status + ')');
|
||||
closeSheet(); toast('Saved in Plex'); route();
|
||||
} catch (e) { btn.disabled = false; el.querySelector('#ve').textContent = e.message; }
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user