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);
|
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
|
// Odysee image proxy to avoid CORS/ORB issues
|
||||||
r.get('/img/odysee', async (req, res) => {
|
r.get('/img/odysee', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const imageUrl = req.query.u ? String(req.query.u) : '';
|
const u = String(req.query.u || '').trim();
|
||||||
if (!imageUrl) return res.status(400).json({ error: 'Missing image URL' });
|
if (!u) return res.status(400).json({ error: 'missing_url' });
|
||||||
|
let target = u;
|
||||||
// Only allow specific Odysee domains for security
|
// Ensure absolute https URL
|
||||||
const allowedHosts = [
|
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',
|
'thumbnails.odycdn.com',
|
||||||
'thumbs.odycdn.com',
|
'thumbs.odycdn.com',
|
||||||
'od.lk',
|
'thumb.odycdn.com',
|
||||||
'cdn.lbryplayer.xyz',
|
'static.odycdn.com',
|
||||||
'spee.ch',
|
'images.odycdn.com',
|
||||||
'cdn.lbryplayer.xyz',
|
'cdn.lbryplayer.xyz',
|
||||||
'thumbnails.lbry.com',
|
'thumbnails.lbry.com',
|
||||||
'static.odycdn.com',
|
'thumbnails.lbry.tech'
|
||||||
'thumbnails.odycdn.com'
|
]);
|
||||||
];
|
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);
|
// Build candidates: extract inner URL after '/plain/' when present, try host alternates, toggle extension, strip query
|
||||||
if (!allowedHosts.includes(url.hostname)) {
|
function extractPlainUrl(href) {
|
||||||
return res.status(403).json({ error: 'Forbidden domain' });
|
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
|
function toggleExt(u0) {
|
||||||
const response = await fetch(imageUrl, {
|
try {
|
||||||
headers: { 'User-Agent': 'NewTube/1.0' },
|
const u1 = new URL(u0);
|
||||||
redirect: 'follow'
|
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();
|
||||||
if (!response.ok) {
|
} catch { return u0; }
|
||||||
return res.status(response.status).send(`Error fetching image: ${response.statusText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward the image with appropriate caching
|
function stripQuery(u0) {
|
||||||
|
try { const u1 = new URL(u0); u1.search = ''; return u1.toString(); } catch { return u0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
res.setHeader('Cache-Control', 'public, max-age=600');
|
||||||
res.setHeader('Content-Type', response.headers.get('content-type') || 'image/jpeg');
|
upstream.data.pipe(res);
|
||||||
|
return; // success
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
} catch (e) {
|
||||||
res.send(Buffer.from(arrayBuffer));
|
lastStatus = e?.response?.status || lastStatus || 0;
|
||||||
} catch (error) {
|
continue;
|
||||||
console.error('Error proxying Odysee image:', error);
|
}
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
}
|
||||||
|
// 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);
|
app.use('/api', r);
|
||||||
// Alias to support Angular dev proxy paths in both dev and production builds
|
// Alias to support Angular dev proxy paths in both dev and production builds
|
||||||
app.use('/proxy/api', r);
|
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/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-api/*', (req, res) => forwardJson(req, res, 'https://api.twitch.tv'));
|
||||||
app.all('/api/twitch-auth/*', (req, res) => forwardJson(req, res, 'https://id.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