feat(sync): sélecteur de chaîne YouTube (multi-chaînes/marque) via PageId

This commit is contained in:
2026-09-27 08:05:01 -04:00
parent d3836989ea
commit f038f19b71
7 changed files with 216 additions and 4 deletions
+35
View File
@@ -82,6 +82,7 @@ import {
getOAuthConnection,
updateOAuthTokens,
deleteOAuthConnection,
setOAuthChannel,
upsertChannelRow,
insertDownloadJob,
getDownloadJob,
@@ -107,6 +108,7 @@ import {
fetchGoogleLiked, fetchTwitchProfile, fetchTwitchFollows, hasGoogleWriteScope,
fetchGoogleWatchLater, pushGoogleWatchLater, fetchInnerTubeHistory,
fetchInnerTubeWatchLaterViaLib, pushInnerTubeWatchLater, fetchGoogleWatchLaterId,
fetchAccountChannels,
} from './oauth.mjs';
import { getSearchMode, getYtDlpBin, hasCookiesFile, metricsSnapshot } from './providers/youtube-common.mjs';
import { ytScrapeCacheStats } from './providers/youtube.mjs';
@@ -559,6 +561,39 @@ r.delete('/oauth/:provider', authMiddlewareCookieAware, oauthLimiter, (req, res)
}
});
// -------------------- Google : choix de la chaîne (multi-chaînes / marque) --------------------
// La chaîne par défaut peut être une coquille vide (pas d'historique ni WL)
// alors que l'activité est sur une chaîne secondaire : l'utilisateur la
// choisit ici, et les appels InnerTube la ciblent via X-Goog-PageId.
r.get('/oauth/google/yt-channels', authMiddlewareCookieAware, oauthLimiter, async (req, res) => {
try {
const conn = await freshOAuthToken(req.user.id, 'google');
const { channels } = await fetchAccountChannels(conn);
try {
console.log(`[oauth] yt-channels user=${req.user?.id} count=${channels.length}`);
} catch {}
return res.json({ channels, selected: { channelId: conn.ytChannelId || null, pageId: conn.ytPageId || null } });
} catch (error) {
const out = { error: error?.message || 'yt_channels_failed' };
if (error?.detail) out.detail = error.detail;
if (error?.hint) out.hint = error.hint;
return res.status(error?.status || 502).json(out);
}
});
r.put('/oauth/google/yt-channel', authMiddlewareCookieAware, oauthLimiter, (req, res) => {
try {
const channelId = String(req.body?.channelId || '').trim().slice(0, 64) || null;
const pageId = String(req.body?.pageId || '').trim().slice(0, 64) || null;
const conn = setOAuthChannel(req.user.id, 'google', { channelId, pageId });
if (!conn) return res.status(404).json({ error: 'oauth_not_connected' });
return res.json({ selected: { channelId: conn.ytChannelId || null, pageId: conn.ytPageId || null } });
} catch {
return res.status(500).json({ error: 'yt_channel_save_failed' });
}
});
// Aperçu distant (sans rien importer) : abonnements + favoris.
r.get('/oauth/:provider/preview', authMiddlewareCookieAware, oauthLimiter, async (req, res) => {
try {