Skip to content

Commit f940850

Browse files
Merge pull request #243 from robbeverhelst/robbeverhelst/jellyfin-images
feat(jellyfin): add artwork commands for fixing missing or poor covers
2 parents 0b17da8 + 947bdd9 commit f940850

15 files changed

Lines changed: 465 additions & 26 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<br />
1313

14-
<img src="./docs/vhs/hero.gif" alt="tsarr doctor — eight services, one terminal" />
14+
<img src="./docs/vhs/hero.gif" alt="tsarr doctor — your whole media stack in one terminal" />
1515

1616
</div>
1717

‎docs/cli-commands.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ tsarr jellyfin <resource> <action> [args]
375375
| `item` | `latest` | `--user <v> [--limit <n>]` | List recently added items |
376376
| `item` | `nextup` | `--user <v> [--limit <n>]` | List next-up episodes |
377377
| `item` | `resume` | `--user <v> [--limit <n>]` | List in-progress (resumable) items |
378+
| `image` | `list` | `--id <v>` | Artwork an item has, with dimensions — use to spot missing or poor covers |
379+
| `image` | `remote` | `--id <v> [--type <v>] [--provider <v>] [--all-languages] [--limit <n>]` | Artwork candidates from metadata providers |
380+
| `image` | `providers` | `--id <v>` | Artwork providers available for an item |
381+
| `image` | `set` | `--id <v> --type <v> --url <v>` | Attach artwork from a URL, replacing the existing image |
382+
| `image` | `delete` | `--id <v> --type <v> [--index <n>]` | Remove artwork (confirms) |
378383
| `watched` | `status` | `--id <v> --user <v>` | Show watched state for an item |
379384
| `watched` | `mark` | `--id <v> --user <v>` | Mark an item as played |
380385
| `watched` | `unmark` | `--id <v> --user <v>` | Mark an item as unplayed |
@@ -411,6 +416,13 @@ tsarr jellyfin <resource> <action> [args]
411416
Jellyfin returns PascalCase JSON, so table columns and `--select` use PascalCase
412417
field names (`Id`, `Name`, `Type`) rather than the camelCase used by Servarr services.
413418

419+
**Artwork:** `image set --url` accepts any reachable image URL, not just the
420+
candidates from `image remote`. Note that `image remote` reports `Width`/`Height`
421+
on Jellyfin 10.11 but **not on 12.0**, even though the API schema declares them —
422+
rank candidates by `CommunityRating` when dimensions are absent. The image
423+
*serving* endpoints (`GET /Items/{id}/Images/...`, which return raw bytes) are
424+
deliberately not wrapped.
425+
414426
**Not available:** there is no `playlist get` or `playlist move`. Jellyfin's
415427
`GetPlaylist` and `MoveItem` endpoints require a user-context token and expose no
416428
`userId` parameter, so they return HTTP 400 under the API-key authentication tsarr

‎docs/cli-getting-started.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ group: CLI
55

66
# CLI Getting Started
77

8-
TsArr includes a command-line interface for interacting with Servarr services directly from your terminal.
8+
TsArr includes a command-line interface for interacting with your media stack directly from your terminal — Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr, qBittorrent, Seerr and Jellyfin.
99

1010
## Installation
1111

‎docs/cli.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,13 @@ tsarr jellyfin item latest --user <userId> # Recently added
745745
tsarr jellyfin item nextup --user <userId> # Next-up episodes
746746
tsarr jellyfin item resume --user <userId> # In-progress items
747747

748+
# Artwork — fix a missing or poor cover
749+
tsarr jellyfin image list --id <itemId> # what artwork exists, with dimensions
750+
tsarr jellyfin image remote --id <itemId> --type Primary --limit 10
751+
tsarr jellyfin image set --id <itemId> --type Primary --url "<url>"
752+
tsarr jellyfin image delete --id <itemId> --type Primary
753+
tsarr jellyfin image providers --id <itemId>
754+
748755
# Watched state
749756
tsarr jellyfin watched status --id <itemId> --user <userId>
750757
tsarr jellyfin watched mark --id <itemId> --user <userId>

‎docs/usage.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Usage Guide
22

3-
This guide provides comprehensive documentation for using the TsArr TypeScript SDK to interact with Servarr APIs.
3+
This guide provides comprehensive documentation for using the TsArr TypeScript SDK to interact with Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr, qBittorrent, Seerr and Jellyfin.
44

55
## Installation
66

@@ -365,6 +365,10 @@ const devRadarr = new RadarrClient({
365365
});
366366
```
367367

368+
Every client owns its configuration, so instances of the same service never
369+
interfere with each other — `prodRadarr` keeps its base URL and API key when
370+
`devRadarr` is constructed. The same holds for `updateConfig()` on one instance.
371+
368372
### Custom Headers and Options
369373

370374
```typescript

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"types": "./dist/clients/jellyfin-types.d.ts"
9494
}
9595
},
96-
"description": "Type-safe TypeScript SDK for Servarr APIs (Radarr, Sonarr, etc.)",
96+
"description": "Type-safe TypeScript SDK and CLI for your media stack (Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr, qBittorrent, Seerr, Jellyfin)",
9797
"engines": {
9898
"node": ">=24.18.0"
9999
},

‎scripts/jellyfin-smoke.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,92 @@ for (const target of TARGETS) {
244244
]);
245245
}
246246

247+
// ---- artwork ------------------------------------------------------------
248+
if (movieId) {
249+
check(
250+
'image list',
251+
['jellyfin', 'image', 'list', '--id', movieId, '--json'],
252+
expectField('ImageType')
253+
);
254+
const remoteOut = check('image remote', [
255+
'jellyfin',
256+
'image',
257+
'remote',
258+
'--id',
259+
movieId,
260+
'--type',
261+
'Primary',
262+
'--limit',
263+
'5',
264+
'--json',
265+
]);
266+
const candidates = json(remoteOut ?? '[]') ?? [];
267+
check('image providers', ['jellyfin', 'image', 'providers', '--id', movieId, '--json']);
268+
269+
if (candidates.length) {
270+
// Rank by resolution where the server reports it (10.11) and by rating
271+
// otherwise (12.0 omits Width/Height).
272+
const ranked = [...candidates].sort((a: any, b: any) =>
273+
typeof a.Width === 'number'
274+
? (b.Width ?? 0) - (a.Width ?? 0)
275+
: (b.CommunityRating ?? 0) - (a.CommunityRating ?? 0)
276+
);
277+
check('image set from url', [
278+
'jellyfin',
279+
'image',
280+
'set',
281+
'--id',
282+
movieId,
283+
'--type',
284+
'Primary',
285+
'--url',
286+
ranked[0].Url,
287+
]);
288+
check(
289+
'image list shows the new cover',
290+
['jellyfin', 'image', 'list', '--id', movieId, '--json'],
291+
stdout => {
292+
const images = json(stdout) ?? [];
293+
return images.some((i: any) => i.ImageType === 'Primary')
294+
? null
295+
: 'Primary missing after set';
296+
}
297+
);
298+
check('image delete', [
299+
'jellyfin',
300+
'image',
301+
'delete',
302+
'--id',
303+
movieId,
304+
'--type',
305+
'Primary',
306+
'--yes',
307+
]);
308+
check(
309+
'image list shows it gone',
310+
['jellyfin', 'image', 'list', '--id', movieId, '--json'],
311+
stdout => {
312+
const images = json(stdout) ?? [];
313+
return images.some((i: any) => i.ImageType === 'Primary')
314+
? 'Primary still present'
315+
: null;
316+
}
317+
);
318+
// Put it back so reruns start from a known state.
319+
cli([
320+
'jellyfin',
321+
'image',
322+
'set',
323+
'--id',
324+
movieId,
325+
'--type',
326+
'Primary',
327+
'--url',
328+
ranked[0].Url,
329+
]);
330+
}
331+
}
332+
247333
// ---- search -------------------------------------------------------------
248334
check(
249335
'search query',

‎skills/tsarr/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: tsarr
3-
description: Manage home media services through TsArr from OpenClaw. Use for Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr, qBittorrent, Seerr, and Jellyfin tasks such as checking health, inspecting queues and history, browsing libraries, searching, adding, editing, deleting items, viewing profiles, tags, and root folders, managing torrents, managing media requests, triggering library scans, reading watched state, checking active playback sessions, and checking TsArr configuration.
3+
description: Manage home media services through TsArr from OpenClaw. Use for Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr, qBittorrent, Seerr, and Jellyfin tasks such as checking health, inspecting queues and history, browsing libraries, searching, adding, editing, deleting items, viewing profiles, tags, and root folders, managing torrents, managing media requests, triggering library scans, reading watched state, checking active playback sessions, fixing missing or poor cover images, and checking TsArr configuration.
44
metadata:
55
openclaw:
66
requires:

‎skills/tsarr/references/common-workflows.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,58 @@ Jellyfin output is PascalCase, so extract fields accordingly:
197197
tsarr jellyfin item list --type Movie --json --select Id,Name
198198
```
199199

200+
## Fix a missing or poor cover image
201+
202+
When someone says a title has no cover, or a bad one, resolve it in four steps.
203+
Never guess an item ID — look it up.
204+
205+
```bash
206+
# 1. Find the item
207+
tsarr jellyfin item list --search "Pokemon" --type Movie --json --select Id,Name,ProductionYear
208+
209+
# 2. Inspect what artwork it has. No Primary row means no cover; a small
210+
# Width/Height means a poor one.
211+
tsarr jellyfin image list --id <itemId> --json
212+
213+
# 3. List candidates from the metadata providers
214+
tsarr jellyfin image remote --id <itemId> --type Primary --limit 10 --json
215+
216+
# 4. Apply the chosen one
217+
tsarr jellyfin image set --id <itemId> --type Primary --url "<url>"
218+
```
219+
220+
Choosing a candidate:
221+
222+
- Prefer higher `Width`/`Height`, then higher `CommunityRating`.
223+
- **`Width`/`Height` are absent on Jellyfin 12.0** (present on 10.11), even
224+
though the API schema declares them. When they are missing, rank by
225+
`CommunityRating` and `VoteCount` instead.
226+
- `Language` matters for posters with title text; pass `--all-languages` to
227+
widen the search.
228+
229+
`--url` accepts **any reachable image URL**, not only provider candidates, so a
230+
cover found elsewhere can be applied directly:
231+
232+
```bash
233+
tsarr jellyfin image set --id <itemId> --type Primary --url "https://example.com/poster.jpg"
234+
```
235+
236+
Other artwork types work the same way — `Backdrop`, `Logo`, `Thumb`, `Banner`.
237+
To drop a bad image without replacing it:
238+
239+
```bash
240+
tsarr jellyfin image delete --id <itemId> --type Primary --yes
241+
```
242+
243+
To sweep a whole library for missing covers:
244+
245+
```bash
246+
for id in $(tsarr jellyfin item list --type Movie --quiet); do
247+
tsarr jellyfin image list --id "$id" --json \
248+
| grep -q '"Primary"' || echo "no cover: $id"
249+
done
250+
```
251+
200252
## Configuration checks
201253

202254
When a command fails unexpectedly, inspect TsArr’s merged configuration:

‎skills/tsarr/references/service-cheatsheet.md‎

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -162,36 +162,73 @@ Seerr uses API key authentication. Configure via `tsarr config init` or environm
162162

163163
## Jellyfin
164164

165-
Use for media server tasks: trigger library scans, read watched state, and check who is streaming.
165+
Use for media server tasks: trigger library scans, read watched state, see who is streaming, control playback, manage playlists and collections, and fix artwork.
166+
167+
Full command surface:
166168

167169
```bash
168-
tsarr jellyfin system status --json
169170
tsarr jellyfin library refresh
170-
tsarr jellyfin library folders --json
171-
tsarr jellyfin item list --type Movie --json
172-
tsarr jellyfin item list --search "The Matrix" --limit 10 --json
173-
tsarr jellyfin item counts --json
174-
tsarr jellyfin item get --id <itemId> --user <userId> --json
175-
tsarr jellyfin watched status --id <itemId> --user <userId> --json
176-
tsarr jellyfin watched mark --id <itemId> --user <userId>
177-
tsarr jellyfin session list --json
178-
tsarr jellyfin session pause --id <sessionId>
179-
tsarr jellyfin session message --id <sessionId> --text "Maintenance in 5 minutes"
180-
tsarr jellyfin playlist create --name "Friday" --user <userId> --json
181-
tsarr jellyfin playlist items --id <playlistId> --user <userId> --json
182-
tsarr jellyfin collection create --name "Sci-Fi" --items <a,b> --json
183-
tsarr jellyfin user list --json
184-
tsarr jellyfin task list --json
185-
tsarr jellyfin search query --query "The Matrix" --json
171+
tsarr jellyfin library folders
172+
tsarr jellyfin library add --name <name> [--collection-type <collection-type>] [--paths <paths>] [--refresh]
173+
tsarr jellyfin library remove --name <name>
174+
tsarr jellyfin item list [--search <search>] [--type <type>] [--parent <parent>] [--user <user>] [--played] [--limit <limit>]
175+
tsarr jellyfin item get --id <id> --user <user>
176+
tsarr jellyfin item refresh --id <id> [--mode <mode>] [--replace-metadata] [--replace-images]
177+
tsarr jellyfin item delete --id <id>
178+
tsarr jellyfin item counts [--user <user>]
179+
tsarr jellyfin item latest --user <user> [--limit <limit>]
180+
tsarr jellyfin item nextup --user <user> [--limit <limit>]
181+
tsarr jellyfin item resume --user <user> [--limit <limit>]
182+
tsarr jellyfin image list --id <id>
183+
tsarr jellyfin image remote --id <id> [--type <type>] [--provider <provider>] [--all-languages] [--limit <limit>]
184+
tsarr jellyfin image providers --id <id>
185+
tsarr jellyfin image set --id <id> --type <type> --url <url>
186+
tsarr jellyfin image delete --id <id> --type <type> [--index <index>]
187+
tsarr jellyfin watched status --id <id> --user <user>
188+
tsarr jellyfin watched mark --id <id> --user <user>
189+
tsarr jellyfin watched unmark --id <id> --user <user>
190+
tsarr jellyfin watched favorite --id <id> --user <user>
191+
tsarr jellyfin watched unfavorite --id <id> --user <user>
192+
tsarr jellyfin session list [--active-within <active-within>]
193+
tsarr jellyfin session play --id <id> --items <a,b> [--mode <mode>] [--position <position>]
194+
tsarr jellyfin session pause --id <id>
195+
tsarr jellyfin session unpause --id <id>
196+
tsarr jellyfin session stop --id <id>
197+
tsarr jellyfin session seek --id <id> --position <position>
198+
tsarr jellyfin session message --id <id> --text <text> [--header <header>] [--timeout <timeout>]
199+
tsarr jellyfin session command --id <id> --command <command>
200+
tsarr jellyfin session system --id <id> --command <command>
201+
tsarr jellyfin session display --id <id> --item <item> --name <name> --type <type>
202+
tsarr jellyfin session add-user --id <id> --user <user>
203+
tsarr jellyfin session remove-user --id <id> --user <user>
204+
tsarr jellyfin playlist create --name <name> --user <user> [--items <a,b>] [--type <type>]
205+
tsarr jellyfin playlist items --id <id> --user <user> [--limit <limit>]
206+
tsarr jellyfin playlist add --id <id> --items <a,b> --user <user>
207+
tsarr jellyfin playlist remove --id <id> --entries <a,b>
208+
tsarr jellyfin collection create --name <name> [--items <a,b>] [--parent <parent>]
209+
tsarr jellyfin collection add --id <id> --items <a,b>
210+
tsarr jellyfin collection remove --id <id> --items <a,b>
211+
tsarr jellyfin user list
212+
tsarr jellyfin user get --id <id>
213+
tsarr jellyfin task list
214+
tsarr jellyfin task start --id <id>
215+
tsarr jellyfin task stop --id <id>
216+
tsarr jellyfin search query --query <query> [--type <type>] [--limit <limit>]
217+
tsarr jellyfin system status
218+
tsarr jellyfin system activity [--limit <limit>]
186219
```
187220

188-
Jellyfin uses API key authentication. Configure via `tsarr config init` or environment variables `TSARR_JELLYFIN_URL` and `TSARR_JELLYFIN_API_KEY`. Get a key from Dashboard -> Advanced -> API Keys.
221+
Add `--json` to any command when extracting values.
189222

190-
Playlists are user-owned: `playlist create`, `items` and `add` all require `--user`. There is no `playlist get` or `playlist move` — those Jellyfin endpoints need a user-context token and reject API keys; use `tsarr jellyfin item get --id <playlistId> --user <userId>` instead.
223+
Jellyfin uses API key authentication. Configure via `tsarr config init` or environment variables `TSARR_JELLYFIN_URL` and `TSARR_JELLYFIN_API_KEY`. Get a key from Dashboard -> Advanced -> API Keys.
191224

192225
**Important:** Jellyfin returns PascalCase JSON (`Id`, `Name`, `Type`, `Items`), unlike the camelCase used by the Servarr services. Use PascalCase when parsing output or passing `--select`.
193226

194-
`--user <userId>` is **required** on `item get`, `item latest`, `item nextup`, `item resume` and all `watched` commands. Jellyfin's spec marks it optional but the server returns 400 without it. Get IDs from `tsarr jellyfin user list --json`.
227+
`--user <userId>` is **required** on `item get`, `item latest`, `item nextup`, `item resume` and all `watched` and `playlist` commands. Jellyfin's spec marks it optional but the server returns 400 without it. Get IDs from `tsarr jellyfin user list --json`.
228+
229+
There is no `playlist get` or `playlist move` — those Jellyfin endpoints need a user-context token and reject API keys; use `tsarr jellyfin item get --id <playlistId> --user <userId>` instead.
230+
231+
See "Fix a missing or poor cover image" in common-workflows.md for the artwork workflow.
195232

196233
## Multi-instance services
197234

0 commit comments

Comments
 (0)