From df0693f01d737d432be2181923e174adba80921c Mon Sep 17 00:00:00 2001 From: Hoid Date: Mon, 9 Feb 2026 22:00:57 +0000 Subject: [PATCH] Add library/genres commands and recommendation skills - bin/audiobooks: add library [limit] [--full] and genres commands - bin/jellyfin: add library [limit] [--full] and genres commands - skills/book-recommender: new skill for audiobook recommendations - skills/media-recommender: new skill for movie/show recommendations --- bin/audiobooks | 47 ++++++++++++++++++++++++++++++- bin/jellyfin | 35 +++++++++++++++++++++++ skills/book-recommender/SKILL.md | 16 +++++++++++ skills/media-recommender/SKILL.md | 15 ++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 skills/book-recommender/SKILL.md create mode 100644 skills/media-recommender/SKILL.md diff --git a/bin/audiobooks b/bin/audiobooks index 1783129..49bd22f 100755 --- a/bin/audiobooks +++ b/bin/audiobooks @@ -143,6 +143,47 @@ async function cmdStats() { console.log(`in-progress:${inProgress}\tfinished:${finished}\ttotal-listened:${formatDuration(totalListened)}`); } +const LIBRARY_ID = '197852a2-2602-4cb9-985c-70f9df5867a8'; + +async function cmdLibrary(args) { + const full = args.includes('--full'); + const limitArg = args.find(a => a !== '--full'); + const endpoint = `/api/libraries/${LIBRARY_ID}/items?limit=0&sort=media.metadata.title`; + const data = await api(endpoint); + const items = data.results || []; + const limit = limitArg ? parseInt(limitArg, 10) : items.length; + + console.log('TITLE\tAUTHOR\tSERIES\tNARRATOR\tGENRES\tDURATION\tYEAR\tDESCRIPTION'); + for (const item of items.slice(0, limit)) { + const meta = item.media?.metadata || {}; + const title = meta.title || 'Unknown'; + const author = meta.authorName || '-'; + const series = meta.seriesName || '-'; + const narrator = meta.narratorName || '-'; + const genres = (meta.genres || []).join(',') || '-'; + const duration = formatDuration(item.media?.duration || 0); + const year = meta.publishedYear || '-'; + const desc = meta.description || ''; + const descOut = full ? desc.replace(/[\t\n\r]/g, ' ') : (desc.length > 100 ? desc.slice(0, 100).replace(/[\t\n\r]/g, ' ') + '...' : desc.replace(/[\t\n\r]/g, ' ')) || '-'; + console.log(`${title}\t${author}\t${series}\t${narrator}\t${genres}\t${duration}\t${year}\t${descOut}`); + } +} + +async function cmdGenres() { + const data = await api(`/api/libraries/${LIBRARY_ID}/items?limit=0`); + const items = data.results || []; + const counts = {}; + for (const item of items) { + for (const g of (item.media?.metadata?.genres || [])) { + counts[g] = (counts[g] || 0) + 1; + } + } + const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]); + for (const [genre, count] of sorted) { + console.log(`${genre}\t${count}`); + } +} + // --- Main --- const [cmd, ...args] = process.argv.slice(2); @@ -154,6 +195,8 @@ const run = async () => { case 'finished': await cmdFinished(args); break; case 'stats': await cmdStats(); break; case 'libraries': await cmdLibraries(); break; + case 'library': await cmdLibrary(args); break; + case 'genres': await cmdGenres(); break; default: console.log(`Usage: audiobooks @@ -162,7 +205,9 @@ Commands: recent [limit] Recently active books (default: 5) finished [limit] Finished books (default: 10) stats Listening stats overview - libraries List libraries`); + libraries List libraries + library [limit] All books with metadata (--full for full descriptions) + genres List genres with counts`); } }; diff --git a/bin/jellyfin b/bin/jellyfin index 81982f9..2d48cb0 100755 --- a/bin/jellyfin +++ b/bin/jellyfin @@ -219,6 +219,39 @@ const commands = { console.log(`Episodes watched:\t${episodes.TotalRecordCount}`); }, + async library(limitArg) { + const uid = await getUserId(); + const full = process.argv.includes('--full'); + const [movies, shows] = await Promise.all([ + api(`/Users/${uid}/Items?IncludeItemTypes=Movie&Recursive=true&SortBy=SortName&Fields=Genres,OfficialRating,Overview,RunTimeTicks&Limit=${limitArg && limitArg !== '--full' ? limitArg : 10000}`), + api(`/Users/${uid}/Items?IncludeItemTypes=Series&Recursive=true&SortBy=SortName&Fields=Genres,OfficialRating,Overview,RunTimeTicks&Limit=${limitArg && limitArg !== '--full' ? limitArg : 10000}`) + ]); + console.log('TYPE\tTITLE\tYEAR\tGENRES\tRATING\tDURATION\tSTATUS\tDESCRIPTION'); + const printItem = (type, i) => { + const genres = (i.Genres || []).join(',') || '-'; + const rating = i.OfficialRating || '-'; + const overview = i.Overview || ''; + const desc = full ? overview.replace(/[\t\n\r]/g, ' ') : (overview.length > 100 ? overview.slice(0, 100).replace(/[\t\n\r]/g, ' ') + '...' : overview.replace(/[\t\n\r]/g, ' ')) || '-'; + const status = pct(i.UserData, i.RunTimeTicks) || '-'; + console.log([type, i.Name, i.ProductionYear || '', genres, rating, dur(i.RunTimeTicks), status, desc].join('\t')); + }; + (movies.Items || []).forEach(i => printItem('MOV', i)); + (shows.Items || []).forEach(i => printItem('SHOW', i)); + }, + + async genres() { + const uid = await getUserId(); + const [movies, shows] = await Promise.all([ + api(`/Users/${uid}/Items?IncludeItemTypes=Movie&Recursive=true&Fields=Genres&Limit=10000`), + api(`/Users/${uid}/Items?IncludeItemTypes=Series&Recursive=true&Fields=Genres&Limit=10000`) + ]); + const counts = {}; + [...(movies.Items || []), ...(shows.Items || [])].forEach(i => { + (i.Genres || []).forEach(g => { counts[g] = (counts[g] || 0) + 1; }); + }); + Object.entries(counts).sort((a, b) => b[1] - a[1]).forEach(([g, c]) => console.log(`${g}\t${c}`)); + }, + async libraries() { const uid = await getUserId(); const r = await api(`/Users/${uid}/Views`); @@ -242,6 +275,8 @@ if (!cmd || !commands[cmd]) { console.log(' movies All movies'); console.log(' search Search library'); console.log(' stats Watch statistics'); + console.log(' library [limit] All movies & shows with metadata (--full for full descriptions)'); + console.log(' genres List genres with counts'); console.log(' libraries List libraries'); process.exit(0); } diff --git a/skills/book-recommender/SKILL.md b/skills/book-recommender/SKILL.md new file mode 100644 index 0000000..deac509 --- /dev/null +++ b/skills/book-recommender/SKILL.md @@ -0,0 +1,16 @@ +--- +name: book-recommender +description: Recommend books and audiobooks based on the user's listening history and preferences. Use when the user asks for book recommendations, what to read/listen to next, or wants suggestions similar to books they enjoyed. Analyzes the user's Audiobookshelf library to understand taste. +--- + +## How to Recommend Books + +1. Run `bin/audiobooks library` to get the user's full library with genres and metadata +2. Run `bin/audiobooks current` and `bin/audiobooks finished` to see what's in progress / recently finished +3. Analyze reading patterns: favorite authors, genres, series preferences, German vs English +4. Use `web_search` to find books similar to their favorites if needed +5. Recommend 3-5 books with explanations of WHY they'd like each one based on their library +6. Include: title, author, brief pitch, which books in their library it's similar to +7. Check if a German audiobook version exists (user prefers German audiobooks) +8. Note: user has ADHD — keep recommendations punchy, not walls of text +9. Format for WhatsApp (no markdown tables, use bold + bullet lists) diff --git a/skills/media-recommender/SKILL.md b/skills/media-recommender/SKILL.md new file mode 100644 index 0000000..3d8f021 --- /dev/null +++ b/skills/media-recommender/SKILL.md @@ -0,0 +1,15 @@ +--- +name: media-recommender +description: Recommend movies and TV shows based on the user's watch history and preferences. Use when the user asks for movie/show recommendations, what to watch next, or wants suggestions. Analyzes the user's Jellyfin library and watch history to understand taste. +--- + +## How to Recommend Movies & Shows + +1. Run `bin/jellyfin library` to get the full library with genres and ratings +2. Run `bin/jellyfin watched` and `bin/jellyfin resume` to see recent activity +3. Analyze watch patterns: favorite genres, preferred types (movies vs shows), completion rates +4. Use `web_search` to find similar content if needed +5. Recommend 3-5 items with explanations of WHY based on their watch history +6. Check availability (is it on streaming services they might have? on Jellyfin already?) +7. Note: user has ADHD — keep recommendations punchy +8. Format for WhatsApp (no markdown tables)