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
This commit is contained in:
parent
503f88820d
commit
df0693f01d
4 changed files with 112 additions and 1 deletions
|
|
@ -143,6 +143,47 @@ async function cmdStats() {
|
||||||
console.log(`in-progress:${inProgress}\tfinished:${finished}\ttotal-listened:${formatDuration(totalListened)}`);
|
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 ---
|
// --- Main ---
|
||||||
|
|
||||||
const [cmd, ...args] = process.argv.slice(2);
|
const [cmd, ...args] = process.argv.slice(2);
|
||||||
|
|
@ -154,6 +195,8 @@ const run = async () => {
|
||||||
case 'finished': await cmdFinished(args); break;
|
case 'finished': await cmdFinished(args); break;
|
||||||
case 'stats': await cmdStats(); break;
|
case 'stats': await cmdStats(); break;
|
||||||
case 'libraries': await cmdLibraries(); break;
|
case 'libraries': await cmdLibraries(); break;
|
||||||
|
case 'library': await cmdLibrary(args); break;
|
||||||
|
case 'genres': await cmdGenres(); break;
|
||||||
default:
|
default:
|
||||||
console.log(`Usage: audiobooks <command>
|
console.log(`Usage: audiobooks <command>
|
||||||
|
|
||||||
|
|
@ -162,7 +205,9 @@ Commands:
|
||||||
recent [limit] Recently active books (default: 5)
|
recent [limit] Recently active books (default: 5)
|
||||||
finished [limit] Finished books (default: 10)
|
finished [limit] Finished books (default: 10)
|
||||||
stats Listening stats overview
|
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`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
35
bin/jellyfin
35
bin/jellyfin
|
|
@ -219,6 +219,39 @@ const commands = {
|
||||||
console.log(`Episodes watched:\t${episodes.TotalRecordCount}`);
|
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() {
|
async libraries() {
|
||||||
const uid = await getUserId();
|
const uid = await getUserId();
|
||||||
const r = await api(`/Users/${uid}/Views`);
|
const r = await api(`/Users/${uid}/Views`);
|
||||||
|
|
@ -242,6 +275,8 @@ if (!cmd || !commands[cmd]) {
|
||||||
console.log(' movies All movies');
|
console.log(' movies All movies');
|
||||||
console.log(' search <query> Search library');
|
console.log(' search <query> Search library');
|
||||||
console.log(' stats Watch statistics');
|
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');
|
console.log(' libraries List libraries');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
skills/book-recommender/SKILL.md
Normal file
16
skills/book-recommender/SKILL.md
Normal file
|
|
@ -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)
|
||||||
15
skills/media-recommender/SKILL.md
Normal file
15
skills/media-recommender/SKILL.md
Normal file
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue