Files
tpbproxy/controller/library.js
wmantly 51e6817393 Add exponential backoff retry to all downstream calls
New controller/retry.js wraps async calls with infinite retries, capped

at 30s with jitter. Applied to: TPB search, TMDB, Emby, Ollama chat,

Transmission RPC, and the status/status interval calls inherit it via

a Proxy around the Transmission client.

Also handles ollama package returning pre-parsed JSON objects.

Signed-off-by: William Mantly <wmantly@gmail.com>
2026-07-20 14:44:41 -04:00

71 lines
2.6 KiB
JavaScript

'use strict';
const conf = require('>/conf');
const { fetchJSONWithRetry } = require('./retry');
// 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 fetchJSONWithRetry(`${conf.emby.url}${pathAndQuery}${sep}api_key=${conf.emby.apiKey}`, {}, { label: `emby ${pathAndQuery.split('?')[0]}` });
return res;
}
// 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.
// Returns { owned, seriesId, have: { season: Set(episode) }, counts: { season: n } }.
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: {}, counts: {} };
let ep = await embyGet(`/Shows/${series.Id}/Episodes`);
let have = {};
for(let e of ep.Items || []){
let sn = e.ParentIndexNumber, en = e.IndexNumber;
if(sn == null || en == null || sn === 0) continue; // skip specials
(have[sn] = have[sn] || new Set()).add(en);
}
let counts = {};
for(let sn of Object.keys(have)) counts[sn] = have[sn].size;
return { owned: true, seriesId: series.Id, have, counts };
}catch(error){
return { owned: false, have: {}, counts: {}, unknown: true };
}
}
module.exports = { movieInLibrary, seriesInventory, rankFromWidth, rankFromLabel, labelFromRank };