Make Smart Search library-aware (Emby dedup + TV gap-fill)
New controller/library.js queries Emby by TMDB provider id (resilient: an unreachable Emby degrades to "unknown" so search still works). Movies — quality-aware dedup: - findReleases checks movieInLibrary(tmdbId) and flags each option alreadyOwned when its resolution is <= what you already own, so only genuine upgrades are offered; the release picker greys owned qualities and shows a library banner. TV — library-driven "fill the gaps": - GET /__api/search/seasons compares Emby's episode inventory against TMDB's per-season episode counts to mark each season complete/partial/missing. - POST /__api/search/fill picks the best complete-season pack (LLM) for every missing/incomplete season and queues them. Front-end routes TV titles to a season plan + "Fill all gaps" instead of the movie release list. Adds tmdb.tmdbSeasons; shared tmdb module import in search.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
const conf = require('>/conf');
|
||||
|
||||
// Resolution rank so we can compare "what you own" against a candidate release.
|
||||
function rankFromWidth(w){
|
||||
w = Number(w) || 0;
|
||||
if(w >= 3000) return 4; // 4k
|
||||
if(w >= 1700) return 3; // 1080p
|
||||
if(w >= 1000) return 2; // 720p
|
||||
if(w > 0) return 1; // sd
|
||||
return 0;
|
||||
}
|
||||
|
||||
function rankFromLabel(label){
|
||||
label = String(label || '').toLowerCase();
|
||||
if(/2160|4k|uhd/.test(label)) return 4;
|
||||
if(label.includes('1080')) return 3;
|
||||
if(label.includes('720')) return 2;
|
||||
if(label.includes('480')) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function labelFromRank(rank){ return ['', '480p', '720p', '1080p', '4k'][rank] || ''; }
|
||||
|
||||
async function embyGet(pathAndQuery){
|
||||
let sep = pathAndQuery.includes('?') ? '&' : '?';
|
||||
let res = await fetch(`${conf.emby.url}${pathAndQuery}${sep}api_key=${conf.emby.apiKey}`);
|
||||
if(!res.ok) throw new Error(`Emby ${res.status}`);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
// Is this movie already in the library, and at what best quality? Never throws — an
|
||||
// unreachable Emby just means "unknown", so search still works.
|
||||
async function movieInLibrary(tmdbId){
|
||||
try{
|
||||
let data = await embyGet(`/Items?Recursive=true&IncludeItemTypes=Movie&AnyProviderIdEquals=tmdb.${tmdbId}&Fields=Width`);
|
||||
let items = data.Items || [];
|
||||
if(!items.length) return { owned: false, rank: 0, quality: '' };
|
||||
let rank = Math.max(...items.map(i => rankFromWidth(i.Width)));
|
||||
return { owned: true, rank, quality: labelFromRank(rank), count: items.length };
|
||||
}catch(error){
|
||||
return { owned: false, rank: 0, quality: '', unknown: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Which episodes of a series are already present, grouped by season( season -> count).
|
||||
async function seriesInventory(tmdbId){
|
||||
try{
|
||||
let s = await embyGet(`/Items?Recursive=true&IncludeItemTypes=Series&AnyProviderIdEquals=tmdb.${tmdbId}&Fields=ProviderIds`);
|
||||
let series = (s.Items || [])[0];
|
||||
if(!series) return { owned: false, have: {} };
|
||||
|
||||
let ep = await embyGet(`/Shows/${series.Id}/Episodes`);
|
||||
let sets = {};
|
||||
for(let e of ep.Items || []){
|
||||
let sn = e.ParentIndexNumber, en = e.IndexNumber;
|
||||
if(sn == null || en == null || sn === 0) continue; // skip specials
|
||||
(sets[sn] = sets[sn] || new Set()).add(en);
|
||||
}
|
||||
let have = {};
|
||||
for(let sn of Object.keys(sets)) have[sn] = sets[sn].size;
|
||||
return { owned: true, seriesId: series.Id, have };
|
||||
}catch(error){
|
||||
return { owned: false, have: {}, unknown: true };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { movieInLibrary, seriesInventory, rankFromWidth, rankFromLabel, labelFromRank };
|
||||
Reference in New Issue
Block a user