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:
@@ -386,6 +386,7 @@
|
||||
|
||||
// Step 2: ask the server( TPB + LLM) for the curated release options.
|
||||
function tbpFindReleases(title){
|
||||
if(title.mediaType === 'tv') return tbpSeasonPlan(title);
|
||||
tbpSearchBody('<p>Finding the best copies for <b>'+ tbpEsc(title.title) +'</b>…<br/>this can take a few seconds.</p>');
|
||||
app.api.post('search/releases', { tmdbId: title.tmdbId, mediaType: title.mediaType }, function(error, data){
|
||||
if(error || !data || !data.options){ tbpSearchBody('<p>Search failed. Please try again.</p>'); return; }
|
||||
@@ -396,8 +397,17 @@
|
||||
// Step 3: render the release cards; a tap feeds the existing add dialog.
|
||||
function tbpRenderReleases(data){
|
||||
if(!data.options.length){ tbpSearchBody('<p>No good torrents found for '+ tbpEsc(data.title) +'.</p>'); return; }
|
||||
tbpSearchBody('<h3>'+ tbpEsc(data.title +' ('+ (data.year || '?') +')') +'</h3>');
|
||||
var header = '<h3>'+ tbpEsc(data.title +' ('+ (data.year || '?') +')') +'</h3>';
|
||||
if(data.library && data.library.owned) header += '<p style="color:#2a2">✓ Already in your library ('+ tbpEsc(data.library.quality) +') — only upgrades are offered.</p>';
|
||||
tbpSearchBody(header);
|
||||
data.options.forEach(function(o){
|
||||
if(o.alreadyOwned){
|
||||
$('<div style="padding:.5em;border-bottom:1px solid #ccc;color:#999;"></div>')
|
||||
.html('<b>'+ tbpEsc(o.label || o.role) +'</b> — you already have this quality or better'
|
||||
+ '<div style="font-size:.9em">'+ tbpEsc(o.name) +'</div>')
|
||||
.appendTo('#tbp_proxy_search_dialog_body');
|
||||
return;
|
||||
}
|
||||
var warn = o.warning ? '<div style="color:#b00;font-weight:bold;">⚠ '+ tbpEsc(o.warning) +'</div>' : '';
|
||||
var why = o.why ? '<div style="color:#555;font-style:italic;">'+ tbpEsc(o.why) +'</div>' : '';
|
||||
$('<div class="tbp_search_card" style="cursor:pointer;padding:.5em;border-bottom:1px solid #ccc;"></div>')
|
||||
@@ -409,6 +419,46 @@
|
||||
}
|
||||
|
||||
// Reuse the existing add flow: fill the torrentAdd scope and open the add dialog.
|
||||
function tbpSeasonPlan(title){
|
||||
tbpSearchBody('<p>Checking your library for <b>'+ tbpEsc(title.title) +'</b>…</p>');
|
||||
app.api.get('search/seasons?tmdbId='+ encodeURIComponent(title.tmdbId), function(error, data){
|
||||
if(error || !data || !data.seasons){ tbpSearchBody('<p>Could not load seasons.</p>'); return; }
|
||||
tbpRenderSeasons(title, data);
|
||||
});
|
||||
}
|
||||
|
||||
function tbpRenderSeasons(title, data){
|
||||
tbpSearchBody('<h3>'+ tbpEsc(data.title +' ('+ (data.year || '?') +')') +'</h3>');
|
||||
data.seasons.forEach(function(s){
|
||||
var badge, color;
|
||||
if(s.status === 'complete'){ badge = '✓ complete'; color = '#2a2'; }
|
||||
else if(s.status === 'partial'){ badge = '⚠ '+ s.haveCount +'/'+ s.episodeCount; color = '#b80'; }
|
||||
else { badge = '✗ missing'; color = '#b00'; }
|
||||
$('<div style="padding:.3em;border-bottom:1px solid #eee;"></div>')
|
||||
.html('<b>Season '+ s.season +'</b> <span style="color:'+ color +'">'+ badge +'</span>')
|
||||
.appendTo('#tbp_proxy_search_dialog_body');
|
||||
});
|
||||
if(data.missing && data.missing.length){
|
||||
$('<button class="ui-button ui-corner-all ui-widget" style="margin-top:.6em;">Fill all gaps ('+ data.missing.length +' season'+ (data.missing.length > 1 ? 's' : '') +')</button>')
|
||||
.on('click', function(){ tbpFillGaps(title); })
|
||||
.appendTo('#tbp_proxy_search_dialog_body');
|
||||
}else{
|
||||
$('#tbp_proxy_search_dialog_body').append('<p style="color:#2a2">✓ You already have every season.</p>');
|
||||
}
|
||||
}
|
||||
|
||||
function tbpFillGaps(title){
|
||||
tbpSearchBody('<p>Finding the best packs for the missing seasons of <b>'+ tbpEsc(title.title) +'</b>…<br/>this can take a bit.</p>');
|
||||
app.api.post('search/fill', { tmdbId: title.tmdbId }, function(error, data){
|
||||
if(error || !data){ tbpSearchBody('<p>Fill failed. Please try again.</p>'); return; }
|
||||
var html = '<h3>'+ tbpEsc(data.title) +'</h3>';
|
||||
(data.queued || []).forEach(function(q){ html += '<p style="color:#2a2">✓ Queued Season '+ q.season +': '+ tbpEsc(q.name) +'</p>'; });
|
||||
(data.skipped || []).forEach(function(s){ html += '<p style="color:#b00">✗ Season '+ s.season +': '+ tbpEsc(s.reason) +'</p>'; });
|
||||
if(!(data.queued || []).length && !(data.skipped || []).length) html += '<p>Nothing to fill.</p>';
|
||||
tbpSearchBody(html);
|
||||
});
|
||||
}
|
||||
|
||||
function tbpAddRelease(o){
|
||||
$.scope.torrentAdd.update({
|
||||
magnetLink: o.magnetLink,
|
||||
|
||||
Reference in New Issue
Block a user