feat: add video like status endpoint and improve Odysee image proxy with fallback URLs
This commit is contained in:
parent
68f6c67240
commit
3dbfb04b15
BIN
db/newtube.db
BIN
db/newtube.db
Binary file not shown.
131
server/index.mjs
131
server/index.mjs
@ -1073,52 +1073,122 @@ r.delete('/user/likes', authMiddleware, (req, res) => {
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
// Like status for a specific video
|
||||
r.get('/user/likes/status', authMiddleware, (req, res) => {
|
||||
const provider = req.query.provider ? String(req.query.provider) : '';
|
||||
const videoId = req.query.videoId ? String(req.query.videoId) : '';
|
||||
if (!provider || !videoId) return res.status(400).json({ error: 'provider and videoId are required' });
|
||||
const liked = isVideoLiked({ userId: req.user.id, provider, videoId });
|
||||
return res.json({ liked });
|
||||
});
|
||||
|
||||
// Odysee image proxy to avoid CORS/ORB issues
|
||||
r.get('/img/odysee', async (req, res) => {
|
||||
try {
|
||||
const imageUrl = req.query.u ? String(req.query.u) : '';
|
||||
if (!imageUrl) return res.status(400).json({ error: 'Missing image URL' });
|
||||
|
||||
// Only allow specific Odysee domains for security
|
||||
const allowedHosts = [
|
||||
const u = String(req.query.u || '').trim();
|
||||
if (!u) return res.status(400).json({ error: 'missing_url' });
|
||||
let target = u;
|
||||
// Ensure absolute https URL
|
||||
if (target.startsWith('//')) target = 'https:' + target;
|
||||
if (!/^https?:\/\//i.test(target)) target = 'https://' + target.replace(/^\/*/, '');
|
||||
// Parse and validate host
|
||||
let parsed;
|
||||
try { parsed = new URL(target); } catch { return res.status(400).json({ error: 'invalid_url' }); }
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
const allowed = new Set([
|
||||
'thumbnails.odycdn.com',
|
||||
'thumbs.odycdn.com',
|
||||
'od.lk',
|
||||
'cdn.lbryplayer.xyz',
|
||||
'spee.ch',
|
||||
'thumb.odycdn.com',
|
||||
'static.odycdn.com',
|
||||
'images.odycdn.com',
|
||||
'cdn.lbryplayer.xyz',
|
||||
'thumbnails.lbry.com',
|
||||
'static.odycdn.com',
|
||||
'thumbnails.odycdn.com'
|
||||
];
|
||||
'thumbnails.lbry.tech'
|
||||
]);
|
||||
const headers = {
|
||||
'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
'Referer': 'https://odysee.com/',
|
||||
'User-Agent': 'Mozilla/5.0'
|
||||
};
|
||||
|
||||
const url = new URL(imageUrl);
|
||||
if (!allowedHosts.includes(url.hostname)) {
|
||||
return res.status(403).json({ error: 'Forbidden domain' });
|
||||
// Build candidates: extract inner URL after '/plain/' when present, try host alternates, toggle extension, strip query
|
||||
function extractPlainUrl(href) {
|
||||
try {
|
||||
const idx = href.indexOf('/plain/');
|
||||
if (idx !== -1) {
|
||||
const tail = href.substring(idx + 7); // after '/plain/'
|
||||
// Tail can be absolute URL possibly percent-encoded
|
||||
try { return new URL(tail).toString(); } catch {}
|
||||
try { return new URL(decodeURIComponent(tail)).toString(); } catch {}
|
||||
}
|
||||
} catch {}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Fetch the image
|
||||
const response = await fetch(imageUrl, {
|
||||
headers: { 'User-Agent': 'NewTube/1.0' },
|
||||
redirect: 'follow'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return res.status(response.status).send(`Error fetching image: ${response.statusText}`);
|
||||
function toggleExt(u0) {
|
||||
try {
|
||||
const u1 = new URL(u0);
|
||||
if (/\.webp(\?|$)/i.test(u1.pathname)) u1.pathname = u1.pathname.replace(/\.webp(\?|$)/i, '.jpg$1');
|
||||
else if (/\.jpg(\?|$)/i.test(u1.pathname)) u1.pathname = u1.pathname.replace(/\.jpg(\?|$)/i, '.webp$1');
|
||||
return u1.toString();
|
||||
} catch { return u0; }
|
||||
}
|
||||
|
||||
// Forward the image with appropriate caching
|
||||
res.setHeader('Cache-Control', 'public, max-age=600');
|
||||
res.setHeader('Content-Type', response.headers.get('content-type') || 'image/jpeg');
|
||||
function stripQuery(u0) {
|
||||
try { const u1 = new URL(u0); u1.search = ''; return u1.toString(); } catch { return u0; }
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
res.send(Buffer.from(arrayBuffer));
|
||||
} catch (error) {
|
||||
console.error('Error proxying Odysee image:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
const hosts = ['thumbs.odycdn.com','thumbnails.lbry.com'];
|
||||
// const hosts = ['thumbnails.odycdn.com','thumbnails.lbry.com','thumbs.odycdn.com','thumb.odycdn.com','static.odycdn.com','images.odycdn.com','thumbnails.lbry.tech','cdn.lbryplayer.xyz'];
|
||||
function swapHost(u0, h) { try { const u1 = new URL(u0); u1.hostname = h; return u1.toString(); } catch { return u0; } }
|
||||
|
||||
const baseHref = parsed.toString();
|
||||
const inner = extractPlainUrl(baseHref);
|
||||
const seed = inner && (() => { try { const p = new URL(inner); return allowed.has(p.hostname.toLowerCase()) ? inner : ''; } catch { return ''; } })() || baseHref;
|
||||
const candidates = new Set();
|
||||
candidates.add(seed);
|
||||
candidates.add(stripQuery(seed));
|
||||
candidates.add(toggleExt(seed));
|
||||
// host alternatives
|
||||
for (const h of hosts) { candidates.add(swapHost(seed, h)); }
|
||||
// If it contained optimize/plain, also try removing that segment entirely
|
||||
try {
|
||||
if (/\/optimize\//.test(seed) && /\/plain\//.test(seed)) {
|
||||
const idx = seed.indexOf('/plain/');
|
||||
const after = seed.substring(idx + 7);
|
||||
try { const direct = new URL(after).toString(); candidates.add(direct); candidates.add(stripQuery(direct)); candidates.add(toggleExt(direct)); } catch {}
|
||||
try { const dec = new URL(decodeURIComponent(after)).toString(); candidates.add(dec); candidates.add(stripQuery(dec)); candidates.add(toggleExt(dec)); } catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Try sequentially and stream the first success
|
||||
let lastStatus = 0;
|
||||
for (const href of candidates) {
|
||||
try {
|
||||
const u2 = new URL(href);
|
||||
if (!allowed.has(u2.hostname.toLowerCase())) continue;
|
||||
const upstream = await axios.get(u2.toString(), { responseType: 'stream', maxRedirects: 3, timeout: 15000, headers, validateStatus: s => s >= 200 && s < 400 });
|
||||
const ctype = upstream.headers['content-type'] || 'image/jpeg';
|
||||
const clen = upstream.headers['content-length'];
|
||||
res.setHeader('Content-Type', ctype);
|
||||
if (clen) res.setHeader('Content-Length', String(clen));
|
||||
res.setHeader('Cache-Control', 'public, max-age=600');
|
||||
upstream.data.pipe(res);
|
||||
return; // success
|
||||
} catch (e) {
|
||||
lastStatus = e?.response?.status || lastStatus || 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// All attempts failed
|
||||
return res.status(lastStatus || 502).json({ error: 'odysee_img_proxy_error', details: 'no_variant_succeeded' });
|
||||
} catch (e) {
|
||||
const status = e?.response?.status || 502;
|
||||
return res.status(status).json({ error: 'odysee_img_proxy_error', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// Mount API router (prod) and alias for dev proxy
|
||||
app.use('/api', r);
|
||||
// Alias to support Angular dev proxy paths in both dev and production builds
|
||||
app.use('/proxy/api', r);
|
||||
@ -1167,6 +1237,7 @@ async function forwardJson(req, res, base) {
|
||||
}
|
||||
|
||||
app.all('/api/dm/*', (req, res) => forwardJson(req, res, 'https://api.dailymotion.com'));
|
||||
app.all('/api/odysee/*', (req, res) => forwardJson(req, res, 'https://api.na-backend.odysee.com'));
|
||||
app.all('/api/twitch-api/*', (req, res) => forwardJson(req, res, 'https://api.twitch.tv'));
|
||||
app.all('/api/twitch-auth/*', (req, res) => forwardJson(req, res, 'https://id.twitch.tv'));
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user