Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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".

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions site/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(/<Config Name="([^"]+)" Target="([^"]+)"/g)].map((m) => [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);
Expand Down
44 changes: 33 additions & 11 deletions site/public/setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -66,11 +64,11 @@ export function install(server, method) {
: 'open the container\'s <b>Edit</b> screen, set <b>Dry Run</b> to <code>false</code> and choose <b>Apply</b>';
const logs = m === 'unraid' ? 'The container\'s <b>Logs</b> show progress.' : '<code>docker logs -f posteryard</code> 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)} &amp;&amp; sudo chown 1000:1000 ${V(DATA)}\ndocker compose up -d` : '',
fill: m === 'unraid'
? `Then <b>Docker › Add Container › Template › Posteryard</b> and fill in the TMDB API key, the ${x.label} address and key, and a webhook secret. <b>Data</b> is the share where Posteryard keeps its files. Need the keys? ${keys}`
? `In Unraid, open <b>Apps</b>, search for <b>Posteryard</b> and choose <b>Install</b>. Fill in the TMDB API key, the ${x.label} address and key, and a webhook secret. <b>Data</b> 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: `<li><div class="irow top"><svg class="si" aria-hidden="true"><use href="#sy-solid-p1-pre"/></svg><span class="imain"><span class="iname">First, a preview. ${x.label} is not changed.</span><span class="itext">Posteryard saves each poster to ${where}. Open a few to check them. ${logs}</span></span></div></li>`
+ `<li><div class="irow top"><svg class="si" aria-hidden="true"><use href="#sy-solid-p1-live"/></svg><span class="imain"><span class="iname">Then go live.</span><span class="itext">When you like them, ${goLive}. Posteryard ${x.live}</span></span></div></li>`,
Expand Down Expand Up @@ -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 <code>environment:</code> in <code>compose.yml</code>',
fname: 'compose.yml',
line: (s, v) => `- ${s.key}=${v}`,
apply: 'run <code>docker compose up -d</code>',
},
run: {
head: 'Add these to the <code>docker run</code> command',
fname: 'Terminal',
line: (s, v) => `-e ${s.key}=${v}`,
apply: 'remove the container with <code>docker rm -f posteryard</code> and run the command again',
},
unraid: {
head: 'Change these fields on the container\'s <b>Edit</b> screen. Most are under <b>Show more settings</b>.',
fname: 'Edit screen',
line: (s, v) => `${esc(s.unraid)}: ${v}`,
apply: 'choose <b>Apply</b>',
},
};

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') : '<span class="c"># Everything is on its default.\n# Change a setting and its line appears here.</span>',
count: out.length,
changed: changed.length,
foot: placeholder ? 'Replace the yellow value, then run <code>docker compose up -d</code>.'
: out.length ? 'Then run <code>docker compose up -d</code>.'
foot: placeholder ? `Replace the yellow value, then ${o.apply}.`
: out.length ? `Then ${o.apply}.`
: `Server, keys and <code>DRY_RUN</code> are in the install block above. ${link(`${REPO}#settings`, 'Every setting, explained')}.`,
};
}
10 changes: 7 additions & 3 deletions site/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ <h2>Settings</h2>
<div role="tabpanel" id="s9-panel" aria-labelledby="s9-tab-0" tabindex="-1"><ul class="ilist" id="s9">{{s9_rows}}</ul></div>
<div class="s9-side">
<p class="list-head" id="s9-head">Add these lines under <code>environment:</code> in <code>compose.yml</code></p>
<div class="code" id="s9-out"><div class="code-bar"><span>compose.yml</span><span class="bar-btns"><button class="ghost" type="button" id="s9-reset"{{s9_reset_hidden}}>Reset</button><button type="button" class="copy" aria-label="Copy the setting lines"{{s9_copy_hidden}}>Copy</button></span></div><pre tabindex="0" role="region" aria-labelledby="s9-head"><code class="out">{{s9_lines}}</code></pre></div>
<div class="code" id="s9-out"><div class="code-bar"><span class="fname">compose.yml</span><span class="bar-btns"><button class="ghost" type="button" id="s9-reset"{{s9_reset_hidden}}>Reset</button><button type="button" class="copy" aria-label="Copy the setting lines"{{s9_copy_hidden}}>Copy</button></span></div><pre tabindex="0" role="region" aria-labelledby="s9-head"><code class="out">{{s9_lines}}</code></pre></div>
<p class="list-foot" id="s9-foot">{{s9_foot}}</p>
</div>
</div>
Expand Down Expand Up @@ -694,12 +694,14 @@ <h2>Settings</h2>
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;
Expand Down Expand Up @@ -731,10 +733,12 @@ <h2>Settings</h2>
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);
Expand Down
8 changes: 4 additions & 4 deletions templates/posteryard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
<Registry>https://github.com/SandObserver/Posteryard/pkgs/container/posteryard</Registry>
<Network>bridge</Network>
<Privileged>false</Privileged>
<ExtraParams>--user=99:100 --init --read-only --tmpfs=/tmp --cap-drop=ALL --security-opt=no-new-privileges:true --memory=1536m</ExtraParams>
<ExtraParams>--user=99:100 --restart=unless-stopped --init --read-only --tmpfs=/tmp --cap-drop=ALL --security-opt=no-new-privileges:true --memory=1536m</ExtraParams>
<Support>https://github.com/SandObserver/Posteryard/issues</Support>
<Project>https://github.com/SandObserver/Posteryard</Project>
<ReadMe>https://github.com/SandObserver/Posteryard/blob/main/README.md</ReadMe>
<License>https://github.com/SandObserver/Posteryard/blob/main/LICENSE</License>
<Overview>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.</Overview>
<Overview>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.</Overview>
<Requires>A free TMDB API key and a Plex, Jellyfin or Emby server.</Requires>
<Changes>https://github.com/SandObserver/Posteryard/blob/main/CHANGELOG.md</Changes>
<Category>MediaApp:Video Tools:Utilities</Category>
<TemplateURL>https://raw.githubusercontent.com/SandObserver/Posteryard/main/templates/posteryard.xml</TemplateURL>
<Icon>https://raw.githubusercontent.com/SandObserver/Posteryard/main/docs/logo.svg</Icon>
<Icon>https://raw.githubusercontent.com/SandObserver/Posteryard/main/site/public/icon-512.png</Icon>
<Screenshot>https://raw.githubusercontent.com/SandObserver/Posteryard/main/docs/before-after.jpg</Screenshot>
<ExtraSearchTerms>plex jellyfin emby posters artwork tmdb kometa textless</ExtraSearchTerms>
<DonateText>If Posteryard is useful to you, you can buy me a coffee.</DonateText>
<DonateLink>https://buymeacoffee.com/sandobserver</DonateLink>
<Config Name="Webhook Port" Target="8000" Default="8000" Mode="tcp" Description="Port for the Plex, Jellyfin or Emby webhook and the /healthz check." Type="Port" Display="always" Required="true" Mask="false">8000</Config>
<Config Name="Webhook Port" Target="8000" Default="8098" Mode="tcp" Description="Port for the Plex, Jellyfin or Emby webhook and the /healthz check." Type="Port" Display="always" Required="true" Mask="false">8098</Config>
<Config Name="Data" Target="/data" Default="/mnt/user/appdata/posteryard" Mode="rw" Description="Database, previews and custom art. Keep this, or every image is rendered and uploaded again." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/posteryard</Config>
<Config Name="TMDB API Key" Target="TMDB_API_KEY" Default="" Mode="" Description="A free key from themoviedb.org, under Settings, API." Type="Variable" Display="always" Required="true" Mask="true"></Config>
<Config Name="Plex URL" Target="PLEX_URL" Default="" Mode="" Description="Address of your Plex server, such as http://192.168.1.10:32400. Leave empty for Jellyfin or Emby." Type="Variable" Display="always" Required="false" Mask="false"></Config>
Expand Down
7 changes: 7 additions & 0 deletions tests/test_unraid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")