fix(sync): WL diagnostic, import-all paginé + totaux, sync auto historique InnerTube
- Watch Later: youtube_no_channel / watchlater_not_found avec hint actionnable + log cles relatedPlaylists (sans PII) - Preview Google: totaux compte (subscriptionTotal/likeTotal), UI 'X / N, l'import prend tout'; import-all paginé (subs<=1000, likes<=500) au lieu de 50/25 - Sync auto historique: GET /oauth/google/yt-history (browse FEhistory authentifie, continuations, <=500), bouton Sync auto dans carte Takeout, import via lots takeout existants
This commit is contained in:
+59
-8
@@ -105,7 +105,7 @@ import {
|
||||
oauthStatus, buildAuthUrl, createOAuthState, consumeOAuthState, exchangeCode,
|
||||
refreshAccessToken, redirectUriFor, fetchGoogleProfile, fetchGoogleSubscriptions,
|
||||
fetchGoogleLiked, fetchTwitchProfile, fetchTwitchFollows, hasGoogleWriteScope,
|
||||
fetchGoogleWatchLater, pushGoogleWatchLater,
|
||||
fetchGoogleWatchLater, pushGoogleWatchLater, fetchInnerTubeHistory,
|
||||
} from './oauth.mjs';
|
||||
import { getSearchMode, getYtDlpBin, hasCookiesFile, metricsSnapshot } from './providers/youtube-common.mjs';
|
||||
import { ytScrapeCacheStats } from './providers/youtube.mjs';
|
||||
@@ -564,11 +564,20 @@ r.get('/oauth/:provider/preview', authMiddlewareCookieAware, oauthLimiter, async
|
||||
const provider = requireOAuthProvider(req.params.provider);
|
||||
const conn = await freshOAuthToken(req.user.id, provider);
|
||||
if (provider === 'google') {
|
||||
const [subscriptions, likes] = await Promise.all([
|
||||
fetchGoogleSubscriptions(conn.accessToken, 50).catch(() => []),
|
||||
fetchGoogleLiked(conn.accessToken, 25).catch(() => []),
|
||||
const [subsRes, likesRes] = await Promise.all([
|
||||
fetchGoogleSubscriptions(conn.accessToken, 50).catch(() => ({ items: [], total: 0 })),
|
||||
fetchGoogleLiked(conn.accessToken, 25).catch(() => ({ items: [], total: 0 })),
|
||||
]);
|
||||
return res.json({ provider, subscriptions, likes, subscriptionCount: subscriptions.length, likeCount: likes.length });
|
||||
return res.json({
|
||||
provider,
|
||||
subscriptions: subsRes.items,
|
||||
likes: likesRes.items,
|
||||
subscriptionCount: subsRes.items.length,
|
||||
likeCount: likesRes.items.length,
|
||||
// Totaux du compte (l'aperçu n'affiche que les 50/25 premiers, l'import prend tout).
|
||||
subscriptionTotal: subsRes.total,
|
||||
likeTotal: likesRes.total,
|
||||
});
|
||||
}
|
||||
const profile = await fetchTwitchProfile(conn.accessToken).catch(() => ({ id: conn.externalUserId || '' }));
|
||||
const follows = await fetchTwitchFollows(conn.accessToken, profile?.id || conn.externalUserId || '', 100);
|
||||
@@ -594,7 +603,8 @@ r.post('/oauth/:provider/import', authMiddlewareCookieAware, oauthLimiter, async
|
||||
|
||||
if (provider === 'google') {
|
||||
if (wantSubs) {
|
||||
const subs = await fetchGoogleSubscriptions(conn.accessToken, 50);
|
||||
// Import complet paginé (l'aperçu n'en montre que 50).
|
||||
const { items: subs } = await fetchGoogleSubscriptions(conn.accessToken, 1000);
|
||||
for (const s of subs) {
|
||||
try {
|
||||
const row = upsertChannelRow({
|
||||
@@ -608,7 +618,8 @@ r.post('/oauth/:provider/import', authMiddlewareCookieAware, oauthLimiter, async
|
||||
}
|
||||
}
|
||||
if (wantLikes) {
|
||||
const likes = await fetchGoogleLiked(conn.accessToken, 25);
|
||||
// Import complet paginé (l'aperçu n'en montre que 25).
|
||||
const { items: likes } = await fetchGoogleLiked(conn.accessToken, 500);
|
||||
for (const v of likes) {
|
||||
try {
|
||||
likeVideo({ userId: req.user.id, provider: 'youtube', videoId: v.videoId, title: v.title, thumbnail: v.thumbnail });
|
||||
@@ -655,7 +666,47 @@ r.get('/oauth/google/watchlater', authMiddlewareCookieAware, oauthLimiter, async
|
||||
writeGranted: hasGoogleWriteScope(conn.scopes),
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(error?.status || 502).json({ error: error?.message || 'watchlater_fetch_failed' });
|
||||
const out = { error: error?.message || 'watchlater_fetch_failed' };
|
||||
if (error?.hint) out.hint = error.hint;
|
||||
return res.status(error?.status || 502).json(out);
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------- Google : historique auto via InnerTube authentifié --------------------
|
||||
// L'API Data v3 n'expose pas l'historique : on lit youtubei/v1/browse
|
||||
// (FEhistory) avec le Bearer OAuth de l'utilisateur (mécanisme SmartTube).
|
||||
// Le client réutilise ensuite POST /user/history/takeout pour l'importer.
|
||||
|
||||
function innertubeApiKey() {
|
||||
const single = String(process.env.YOUTUBE_API_KEY || '').trim();
|
||||
if (single && !single.includes(',')) return single;
|
||||
const csv = String(process.env.YOUTUBE_API_KEYS || '').trim();
|
||||
try {
|
||||
if (csv.startsWith('[')) {
|
||||
const arr = JSON.parse(csv);
|
||||
if (Array.isArray(arr) && arr[0]) return String(arr[0]);
|
||||
}
|
||||
} catch {}
|
||||
if (csv) {
|
||||
const first = csv.split(',').map((s) => s.trim()).filter(Boolean)[0];
|
||||
if (first) return first;
|
||||
}
|
||||
if (single) return single.split(',').map((s) => s.trim()).filter(Boolean)[0] || '';
|
||||
return '';
|
||||
}
|
||||
|
||||
r.get('/oauth/google/yt-history', authMiddlewareCookieAware, oauthLimiter, async (req, res) => {
|
||||
try {
|
||||
const conn = await freshOAuthToken(req.user.id, 'google');
|
||||
const max = Math.min(500, Math.max(1, Number(req.query.max || 200)));
|
||||
const { items, hasMore } = await fetchInnerTubeHistory(conn.accessToken, innertubeApiKey(), max);
|
||||
return res.json({ items, count: items.length, hasMore });
|
||||
} catch (error) {
|
||||
const out = { error: error?.message || 'ythistory_fetch_failed' };
|
||||
if (error?.status === 401) {
|
||||
out.hint = 'Session Google expirée : déconnectez puis reconnectez Google.';
|
||||
}
|
||||
return res.status(error?.status || 502).json(out);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user