diff --git a/CHANGELOG.md b/CHANGELOG.md index e6fb24b..4dba73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Give a Live TV title without art its channel's logo large over the logo's blurred colours, in place of the title on dark grey. - Give a Live TV block named after its channel the same card, in place of art from the channel's show with the most airtime. - List what Posteryard needs and its Live TV posters in the Unraid template. +- Install Posteryard on Unraid from the Apps tab, listed in Community Applications. +- Default the Unraid webhook port to 8098, as many Unraid apps already use 8000. +- Restart the Unraid container after a crash. +- Show the settings panel's lines for the install method picked on the landing page. ### Fixed +- Show the Posteryard icon in the Unraid Docker tab, which cannot show SVG icons. - Upload Live TV posters again at the next sweep when Plex deletes them while cleaning bundles. - Give a Plex guide title a poster when Plex returns another entry for it, such as "News" for "News at Ten". diff --git a/README.md b/README.md index d33c476..1d07513 100644 --- a/README.md +++ b/README.md @@ -112,17 +112,17 @@ Two more steps are worth it: ## Unraid -Save the template to your flash drive from the Unraid terminal: +Open **Apps**, search for **Posteryard** and choose **Install**. Fill in the TMDB API key, the Plex URL and token, and a webhook secret. Previews land in `/mnt/user/appdata/posteryard/previews`. The webhook port is `8098`. Use it in place of `8000` in every webhook address, such as `http://UNRAID-IP:8098/webhook/YOUR-WEBHOOK-SECRET` for Plex. The template runs as Unraid's `nobody` user (`--user=99:100`) with the same lockdown as the compose file, and restarts the container after a crash. + +To go live, open the container's **Edit** screen, set **Dry Run** to `false` and choose **Apply**. The container's **Logs** show progress. + +If **Apps** does not list Posteryard yet, save the template from the Unraid terminal, then pick **Posteryard** under **Docker**, **Add Container**, **Template**: ```sh curl -fsSL -o /boot/config/plugins/dockerMan/templates-user/my-Posteryard.xml \ https://raw.githubusercontent.com/SandObserver/Posteryard/main/templates/posteryard.xml ``` -Then pick **Posteryard** under **Docker**, **Add Container**, **Template**. Fill in the TMDB API key, the Plex URL and token, and a webhook secret. Previews land in `/mnt/user/appdata/posteryard/previews`. The template runs as Unraid's `nobody` user (`--user=99:100`) with the same lockdown as the compose file. - -To go live, open the container's **Edit** screen, set **Dry Run** to `false` and choose **Apply**. The container's **Logs** show progress. - ## Settings Every setting has a default. Add a line under `environment:` only to change one. The [landing page](https://posteryard.sandobserver.com/#settings) builds these lines for you. diff --git a/site/build.mjs b/site/build.mjs index f40c9ff..f676885 100644 --- a/site/build.mjs +++ b/site/build.mjs @@ -55,6 +55,12 @@ const version = changelog.match(/^## \[(\d+\.\d+\.\d+)\]/m)?.[1] ?? fail('CHANGE checkInstall(); const settingsData = JSON.parse(readFileSync(join(here, 'settings.json'), 'utf8')); +const unraidNames = Object.fromEntries( + [...readFileSync(join(root, 'templates', 'posteryard.xml'), 'utf8').matchAll(/ [m[2], m[1]]), +); +for (const setting of installer.flatten(settingsData)) { + setting.unraid = unraidNames[setting.key] ?? fail(`templates/posteryard.xml has no field for ${setting.key}`); +} const firstView = installer.install('plex', 'compose'); const firstVals = installer.defaults(settingsData); const firstLines = installer.lines(settingsData, firstVals); diff --git a/site/public/setup.mjs b/site/public/setup.mjs index c51b039..d11409b 100644 --- a/site/public/setup.mjs +++ b/site/public/setup.mjs @@ -54,8 +54,6 @@ docker run -d --name posteryard --restart unless-stopped \\ -v ${V(DATA)}:/data -p 8000:8000 \\ ghcr.io/sandobserver/posteryard:latest`; } -const unraid = () => `curl -fsSL -o /boot/config/plugins/dockerMan/templates-user/my-Posteryard.xml \\ - https://raw.githubusercontent.com/SandObserver/Posteryard/main/templates/posteryard.xml`; export function install(server, method) { const x = servers[server], m = method; @@ -66,11 +64,11 @@ export function install(server, method) { : 'open the container\'s Edit screen, set Dry Run to false and choose Apply'; const logs = m === 'unraid' ? 'The container\'s Logs show progress.' : 'docker logs -f posteryard shows progress.'; return { - fname: { compose: 'compose.yml', run: 'Terminal', unraid: 'Unraid terminal' }[m], - code: m === 'compose' ? compose(x) : m === 'run' ? run(x) : unraid(), + fname: { compose: 'compose.yml', run: 'Terminal', unraid: '' }[m], + code: m === 'compose' ? compose(x) : m === 'run' ? run(x) : '', start: m === 'compose' ? `mkdir -p ${V(DATA)} && sudo chown 1000:1000 ${V(DATA)}\ndocker compose up -d` : '', fill: m === 'unraid' - ? `Then Docker › Add Container › Template › Posteryard and fill in the TMDB API key, the ${x.label} address and key, and a webhook secret. Data is the share where Posteryard keeps its files. Need the keys? ${keys}` + ? `In Unraid, open Apps, search for Posteryard and choose Install. Fill in the TMDB API key, the ${x.label} address and key, and a webhook secret. Data is the share where Posteryard keeps its files. Need the keys? ${keys}` : `Replace the yellow values. ${V(DATA)} is the folder on your server where Posteryard keeps its files; any path works. Need the keys? ${keys}`, next: `
  • First, a preview. ${x.label} is not changed.Posteryard saves each poster to ${where}. Open a few to check them. ${logs}
  • ` + `
  • Then go live.When you like them, ${goLive}. Posteryard ${x.live}
  • `, @@ -123,19 +121,43 @@ export function rows(data, active, vals, server) { }).join(''); } -export function lines(data, vals) { - const all = flatten(data); +const outputs = { + compose: { + head: 'Add these lines under environment: in compose.yml', + fname: 'compose.yml', + line: (s, v) => `- ${s.key}=${v}`, + apply: 'run docker compose up -d', + }, + run: { + head: 'Add these to the docker run command', + fname: 'Terminal', + line: (s, v) => `-e ${s.key}=${v}`, + apply: 'remove the container with docker rm -f posteryard and run the command again', + }, + unraid: { + head: 'Change these fields on the container\'s Edit screen. Most are under Show more settings.', + fname: 'Edit screen', + line: (s, v) => `${esc(s.unraid)}: ${v}`, + apply: 'choose Apply', + }, +}; + +export function lines(data, vals, method = 'compose') { + const all = flatten(data), o = outputs[method]; const changed = all.filter((s) => isChanged(s, vals)); - const out = changed.map((s) => `- ${s.key}=${esc(vals[s.key].trim())}`); + const out = changed.map((s) => o.line(s, esc(vals[s.key].trim()))); const events = all.find((s) => s.key === 'NOTIFY_EVENTS'); const placeholder = isChanged(events, vals) && vals.NOTIFY_EVENTS !== '' && !vals.NOTIFY_URLS.trim(); - if (placeholder) out.push(`- NOTIFY_URLS=${V('your-alert-address')}`); + if (placeholder) out.push(o.line(all.find((s) => s.key === 'NOTIFY_URLS'), V('your-alert-address'))); return { + head: o.head, + fname: o.fname, + copy: method !== 'unraid', html: out.length ? out.join('\n') : '# Everything is on its default.\n# Change a setting and its line appears here.', count: out.length, changed: changed.length, - foot: placeholder ? 'Replace the yellow value, then run docker compose up -d.' - : out.length ? 'Then run docker compose up -d.' + foot: placeholder ? `Replace the yellow value, then ${o.apply}.` + : out.length ? `Then ${o.apply}.` : `Server, keys and DRY_RUN are in the install block above. ${link(`${REPO}#settings`, 'Every setting, explained')}.`, }; } diff --git a/site/template.html b/site/template.html index ceefa6a..4d5400e 100644 --- a/site/template.html +++ b/site/template.html @@ -439,7 +439,7 @@

    Settings

    Add these lines under environment: in compose.yml

    -
    compose.yml
    {{s9_lines}}
    +
    compose.yml
    {{s9_lines}}

    {{s9_foot}}

    @@ -694,12 +694,14 @@

    Settings

    const view = install(g.server, g.method); const code = $("#g-code .out"), oldText = code.textContent, sameFile = $("#g-code .fname").textContent === view.fname; morph($("#gs"), () => { + $("#g-code").hidden = !view.code; $("#g-code .fname").textContent = view.fname; code.innerHTML = animate && sameFile ? highlight(oldText, view.code) : view.code; $("#g-start").hidden = !view.start; $("#g-start .out").innerHTML = view.start; $("#g-fill").innerHTML = view.fill; $("#g-next").innerHTML = view.next; + drawOut(false); for (const b of $$("#gs .pop")) { const k = b.dataset.k, name = pickers[k].options[g[k]].label; $("span", b).textContent = name; @@ -731,10 +733,12 @@

    Settings

    let tab = 0; const byKey = (k) => settings.find((s) => s.key === k); function drawOut(animate) { - const out = lines(data, vals), pre = $("#s9-out pre"); + const out = lines(data, vals, g.method), pre = $("#s9-out pre"); morph(pre, () => { $(".out", pre).innerHTML = out.html; }, animate); + $("#s9-head").innerHTML = out.head; + $("#s9-out .fname").textContent = out.fname; $("#s9-reset").hidden = !out.changed; - $("#s9-out .copy").hidden = !out.count; + $("#s9-out .copy").hidden = !out.count || !out.copy; $("#s9-foot").innerHTML = out.foot; $$("#s9-tabs [role=tab]").forEach((b, i) => { const has = groups[i].settings.some((s) => isChanged(s, vals)), dot = $(".dot", b); diff --git a/templates/posteryard.xml b/templates/posteryard.xml index f53178c..e307d67 100644 --- a/templates/posteryard.xml +++ b/templates/posteryard.xml @@ -5,22 +5,22 @@ https://github.com/SandObserver/Posteryard/pkgs/container/posteryard bridge false - --user=99:100 --init --read-only --tmpfs=/tmp --cap-drop=ALL --security-opt=no-new-privileges:true --memory=1536m + --user=99:100 --restart=unless-stopped --init --read-only --tmpfs=/tmp --cap-drop=ALL --security-opt=no-new-privileges:true --memory=1536m https://github.com/SandObserver/Posteryard/issues https://github.com/SandObserver/Posteryard https://github.com/SandObserver/Posteryard/blob/main/README.md https://github.com/SandObserver/Posteryard/blob/main/LICENSE - Clean, consistent posters for Plex, Jellyfin and Emby, picked, rendered and kept current automatically.[br][br]Posteryard gives every movie, show, season and episode the same calm look: clean art without printed text, the title logo in one fixed spot, a soft fade, and small badges for 4K, Dolby Vision, Dolby Atmos and the streaming service. It finds the art on TMDB, checks it, renders it, uploads it and keeps it up to date. With Live TV on, it does the same for the shows and movies in the Plex or Jellyfin guide.[br][br]It starts with Dry Run on: images go to the previews folder in Data and the server is not touched. Set Dry Run to false when you like them.[br][br]For new titles right away, add the webhook http://UNRAID-IP:8000/webhook/YOUR-WEBHOOK-SECRET in Plex under Settings, Webhooks. For Jellyfin and Emby, see the README. Without it, new titles get posters within 15 minutes. + Clean, consistent posters for Plex, Jellyfin and Emby, picked, rendered and kept current automatically.[br][br]Posteryard gives every movie, show, season and episode the same calm look: clean art without printed text, the title logo in one fixed spot, a soft fade, and small badges for 4K, Dolby Vision, Dolby Atmos and the streaming service. It finds the art on TMDB, checks it, renders it, uploads it and keeps it up to date. With Live TV on, it does the same for the shows and movies in the Plex or Jellyfin guide.[br][br]It starts with Dry Run on: images go to the previews folder in Data and the server is not touched. Set Dry Run to false when you like them.[br][br]For new titles right away, add the webhook http://UNRAID-IP:8098/webhook/YOUR-WEBHOOK-SECRET in Plex under Settings, Webhooks. For Jellyfin and Emby, see the README. Without it, new titles get posters within 15 minutes. A free TMDB API key and a Plex, Jellyfin or Emby server. https://github.com/SandObserver/Posteryard/blob/main/CHANGELOG.md MediaApp:Video Tools:Utilities https://raw.githubusercontent.com/SandObserver/Posteryard/main/templates/posteryard.xml - https://raw.githubusercontent.com/SandObserver/Posteryard/main/docs/logo.svg + https://raw.githubusercontent.com/SandObserver/Posteryard/main/site/public/icon-512.png https://raw.githubusercontent.com/SandObserver/Posteryard/main/docs/before-after.jpg plex jellyfin emby posters artwork tmdb kometa textless If Posteryard is useful to you, you can buy me a coffee. https://buymeacoffee.com/sandobserver - 8000 + 8098 /mnt/user/appdata/posteryard diff --git a/tests/test_unraid.py b/tests/test_unraid.py index 065f536..fb48915 100644 --- a/tests/test_unraid.py +++ b/tests/test_unraid.py @@ -35,3 +35,10 @@ def test_the_template_matches_the_image_and_the_readme() -> None: assert _configs("Port").keys() == set(re.findall(r"EXPOSE (\d+)", dockerfile)) readme = (ROOT / "README.md").read_text() assert [name for name in _configs("Variable") if f"`{name}`" not in readme] == [] + + +def test_the_template_images_are_in_the_repository() -> None: + raw = "https://raw.githubusercontent.com/SandObserver/Posteryard/main/" + images = [TEMPLATE.findtext("Icon") or "", *(s.text or "" for s in TEMPLATE.iter("Screenshot"))] + assert [url for url in images if not url.startswith(raw) or not (ROOT / url.removeprefix(raw)).is_file()] == [] + assert (TEMPLATE.findtext("Icon") or "").endswith(".png")