chore: update Angular cache and TypeScript build info

This commit is contained in:
2025-09-20 22:33:11 -04:00
parent 9c03f5f5fe
commit 3f04191623
25 changed files with 750 additions and 54 deletions
+18 -6
View File
@@ -9,7 +9,12 @@ const overrideDbFile = process.env.NEWTUBE_DB_FILE && String(process.env.NEWTUBE
: null;
const dbDir = overrideDbFile ? path.dirname(overrideDbFile) : path.join(root, 'db');
const dbFile = overrideDbFile || path.join(dbDir, 'newtube.db');
const schemaFile = path.join(root, 'db', 'schema.sql');
// Try multiple schema locations to survive when /app/db is a mounted volume
const schemaCandidates = [
path.join(root, 'db', 'schema.sql'), // normal repo path (may be hidden by a volume)
path.join(root, 'db-schema', 'schema.sql'), // immutable path bundled in image
];
const schemaFile = schemaCandidates.find(p => fs.existsSync(p));
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
@@ -19,12 +24,19 @@ if (!fs.existsSync(dbDir)) {
const db = new Database(dbFile);
db.pragma('foreign_keys = ON');
// Run schema if present
if (fs.existsSync(schemaFile)) {
const ddl = fs.readFileSync(schemaFile, 'utf8');
if (ddl && ddl.trim().length) {
db.exec(ddl);
// Run schema if present (first boot)
if (schemaFile && fs.existsSync(schemaFile)) {
try {
const ddl = fs.readFileSync(schemaFile, 'utf8');
if (ddl && ddl.trim().length) {
db.exec(ddl);
console.log(`[db] Applied schema from ${schemaFile}`);
}
} catch (e) {
console.warn(`[db] Failed to apply schema from ${schemaFile}:`, e?.message || e);
}
} else {
console.warn('[db] No schema.sql found in expected locations:', schemaCandidates.join(', '));
}
// Lightweight schema upgrades for existing databases (SQLite is permissive)
+29 -18
View File
@@ -967,7 +967,7 @@ r.post('/auth/refresh', async (req, res) => {
// rotate token
const nextToken = cryptoRandomId();
const nextHash = await hashToken(nextToken);
const days = session.is_remember ? REMEMBER_TTL_DAYS : REFRESH_TTL_DAYS;
const days = session.isRemember ? REMEMBER_TTL_DAYS : REFRESH_TTL_DAYS;
const expiresAt = new Date(Date.now() + days * 86400_000).toISOString();
updateSessionToken(session.id, nextHash, expiresAt);
setRefreshCookies(res, { sessionId: session.id, token: nextToken, days });
@@ -1132,9 +1132,7 @@ r.post('/user/likes', authMiddleware, async (req, res) => {
const meta = (typeof raw === 'string') ? JSON.parse(raw || '{}') : (raw || {});
if (needTitle) title = meta?.title || title || '';
if (needThumb) thumbnail = meta?.thumbnail || (Array.isArray(meta?.thumbnails) && meta.thumbnails.length ? meta.thumbnails[0].url : thumbnail || '');
} catch (e) {
console.warn('[POST /user/likes] details fetch failed, continuing without enrichment:', e?.message || e);
}
} catch {}
}
} catch {}
@@ -1267,6 +1265,8 @@ r.get('/img/odysee', async (req, res) => {
// Mount API router (prod) and alias for dev proxy
app.use('/api', r);
// Health endpoint for container checks
app.get('/api/health', (_req, res) => res.json({ status: 'ok' }));
// Alias to support Angular dev proxy paths in both dev and production builds
app.use('/proxy/api', r);
// Mount dedicated Rumble router (browse, search, video)
@@ -1274,7 +1274,7 @@ app.use('/api/rumble', rumbleRouter);
// -------------------- Client config from environment --------------------
function jsVal(v) { return JSON.stringify(v == null ? '' : v); }
app.get(['/assets/config.local.js', '/config.js'], (_req, res) => {
app.get(['/assets/config.local.js', '/assets/config.js', '/config.js'], (_req, res) => {
// WARNING: Values served here are exposed to the browser. Do not put secrets here unless you accept this.
const lines = [];
const env = process.env || {};
@@ -1319,23 +1319,34 @@ app.all('/api/twitch-api/*', (req, res) => forwardJson(req, res, 'https://api.tw
app.all('/api/twitch-auth/*', (req, res) => forwardJson(req, res, 'https://id.twitch.tv'));
// -------------------- Static Frontend (Angular build) --------------------
try {
const staticDir = path.join(process.cwd(), 'dist');
if (fs.existsSync(staticDir)) {
app.use(express.static(staticDir, { maxAge: '1h', index: 'index.html' }));
// SPA fallback: route non-API GETs to index.html
app.get('*', (req, res, next) => {
const url = req.originalUrl || req.url || '';
if (url.startsWith('/api/') || url.startsWith('/health')) return next();
const indexPath = path.join(staticDir, 'index.html');
if (fs.existsSync(indexPath)) return res.sendFile(indexPath);
return next();
});
const distRoot = path.join(process.cwd(), 'dist');
const distBrowser = path.join(distRoot, 'browser');
const staticDir = fs.existsSync(distBrowser) ? distBrowser : distRoot;
// Mount static files unconditionally; if path missing, it will just not serve anything
app.use(express.static(staticDir, { maxAge: '1h', index: 'index.html' }));
// SPA fallback: any non-API GET should serve index.html
app.get('*', (req, res, next) => {
try {
const url = req.originalUrl || req.url || '';
if (url.startsWith('/api/')) return next();
const indexPath = path.join(staticDir, 'index.html');
if (fs.existsSync(indexPath)) return res.sendFile(indexPath);
return next();
} catch {
return next();
}
} catch {}
});
app.listen(PORT, () => {
const cwd = process.cwd();
const hasDistRoot = fs.existsSync(distRoot);
const hasDistBrowser = fs.existsSync(distBrowser);
const hasIndex = fs.existsSync(path.join(staticDir, 'index.html'));
console.log(`[newtube-api] listening on http://localhost:${PORT}`);
console.log(`[newtube-api] cwd=${cwd}`);
console.log(`[newtube-api] distRoot=${distRoot} exists=${hasDistRoot}`);
console.log(`[newtube-api] distBrowser=${distBrowser} exists=${hasDistBrowser}`);
console.log(`[newtube-api] staticDir=${staticDir} indexExists=${hasIndex}`);
});
// --- Playlists ---
+90 -10
View File
@@ -223,11 +223,56 @@ async function scrapeRumbleVideo(videoIdOrUrl) {
/* ------------------ Scraper de liste (search / browse) ------------------ */
function parseDurationToSeconds(text) {
if (!text) return 0;
// supporte mm:ss ou hh:mm:ss
const m = text.trim().match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
if (!m) return 0;
const h = parseInt(m[3] || '0', 10), mn = parseInt(m[1] || '0', 10), s = parseInt(m[2] || '0', 10);
return h * 3600 + mn * 60 + s;
// Nettoyer le texte en supprimant les espaces et caractères non numériques inutiles
const cleanText = text.trim().replace(/\s+/g, '');
// Format hh:mm:ss
let m = cleanText.match(/^(\d+):(\d{2}):(\d{2})$/);
if (m) {
const h = parseInt(m[1], 10) || 0;
const mn = parseInt(m[2], 10) || 0;
const s = parseInt(m[3], 10) || 0;
return h * 3600 + mn * 60 + s;
}
// Format mm:ss
m = cleanText.match(/^(\d+):(\d{2})$/);
if (m) {
const mn = parseInt(m[1], 10) || 0;
const s = parseInt(m[2], 10) || 0;
return mn * 60 + s;
}
// Format avec unités (ex: 1h 30m 45s)
m = cleanText.match(/(\d+h)?(\d+m)?(\d+s)?/);
if (m) {
const hours = m[1] ? parseInt(m[1], 10) : 0;
const minutes = m[2] ? parseInt(m[2], 10) : 0;
const seconds = m[3] ? parseInt(m[3], 10) : 0;
return (hours * 3600) + (minutes * 60) + seconds;
}
// Si on arrive ici, on essaie d'extraire tous les nombres et on suppose un format mmss
const numbers = cleanText.match(/\d+/g);
if (numbers && numbers.length > 0) {
// Si un seul nombre, on suppose que c'est en secondes
if (numbers.length === 1) {
return parseInt(numbers[0], 10) || 0;
}
// Si deux nombres, on suppose mm:ss
if (numbers.length === 2) {
return (parseInt(numbers[0], 10) * 60) + (parseInt(numbers[1], 10) || 0);
}
// Si trois nombres, on suppose hh:mm:ss
if (numbers.length >= 3) {
return (parseInt(numbers[0], 10) * 3600) +
(parseInt(numbers[1], 10) * 60) +
(parseInt(numbers[2], 10) || 0);
}
}
return 0; // Par défaut si aucun format n'est reconnu
}
async function scrapeRumbleList({ q, page = 1, limit = 24, sort = 'viral' }) {
@@ -263,12 +308,47 @@ async function scrapeRumbleList({ q, page = 1, limit = 24, sort = 'viral' }) {
|| '';
if (thumb && thumb.startsWith('//')) thumb = 'https:' + thumb;
const durationText =
card.find('.video-item--duration, .video-duration, .duration, .video-item__duration').first().text().trim();
// Essayer plusieurs sélecteurs pour la durée, y compris les attributs data-
let durationText = '';
// Ajout de plus de sélecteurs spécifiques à Rumble
const durationElement = card.find(
'.video-item--duration, .video-duration, .duration, .video-item__duration, ' +
'[data-duration], .videoDuration, .video-time, .time, ' +
'.video-card__duration, .media__duration, .thumb-time, ' +
'.video-listing-entry__duration, .video-item__duration'
).first();
if (durationElement.length) {
// Essayer d'abord les attributs data-
durationText = durationElement.attr('data-duration') ||
durationElement.attr('data-time') ||
// Essayer aussi les attributs style ou autres qui pourraient contenir la durée
durationElement.attr('aria-label') ||
durationElement.attr('title') ||
// Essayer le contenu textuel
durationElement.text().trim();
}
// Si on n'a pas trouvé de durée, essayer de la trouver dans le contenu de la card
if (!durationText) {
// Chercher un élément qui ressemble à une durée (mm:ss ou hh:mm:ss)
const timeMatch = card.html().match(/>\s*([0-9]+:[0-9]{2}(?::[0-9]{2})?)\s*</);
if (timeMatch && timeMatch[1]) {
durationText = timeMatch[1];
}
}
// Nettoyer le texte de durée avant de le parser
const cleanDurationText = durationText.replace(/[^0-9:]/g, '').trim();
// Extraire les vues
const viewsText =
card.find('.video-item--views, .rumbles-views, .views, .video-item__views').first().text().trim();
const duration = parseDurationToSeconds(durationText);
card.find('.video-item--views, .rumbles-views, .views, .video-item__views, [data-views]').first()
.attr('data-views') ||
card.find('.video-item--views, .rumbles-views, .views, .video-item__views, .video-views').first().text().trim();
// Parser la durée
const duration = parseDurationToSeconds(cleanDurationText);
const views = parseInt((viewsText || '').replace(/[^\d]/g, ''), 10) || 0;
// Important: on renvoie TOUJOURS une URL canonique cohérente