feat(couverture): 100pc API/MCP/tests/docs - volet 2
CI / build-and-test (push) Successful in 14m6s

- tests: server/tests/api_coverage2.test.mjs (23 cas offline-safe) :
  oauth (status/url/connections/preview), channels (validations),
  subscriptions batch + groupes CRUD, transcripts history, likes/history
  DELETE, playlists public/view/reorder, telemetry summary, downloads
  401/403/400, ai 503, twitch-token, config.js, rumble 400
- mcp v1.1.0 : 4 outils authentifies (list_playlists/likes/
  subscriptions/watch_history, NEWTUBE_TOKEN) + tests stub
- openapi.json v1.1 : 15 chemins documentes (sante, recherche,
  details, trending, transcript, auth, prefs, likes, subs, downloads,
  oauth, playlists) ; guide maj (11 outils)
This commit is contained in:
2026-09-27 18:51:50 -04:00
parent 1812b86dd4
commit 148a00e60f
7 changed files with 405 additions and 8 deletions
+74 -1
View File
@@ -27,7 +27,7 @@
const API_BASE = (process.env.NEWTUBE_API_URL || 'http://localhost:4000/api').replace(/\/+$/, '');
const TOKEN = (process.env.NEWTUBE_TOKEN || '').trim();
const TIMEOUT_MS = Number(process.env.MCP_TOOL_TIMEOUT_MS || 60000);
const SERVER_VERSION = '1.0.0';
const SERVER_VERSION = '1.1.0';
const PROVIDERS = ['yt', 'dm', 'tw', 'pt', 'od', 'ru'];
const TRANSCRIPT_PROVIDERS = ['youtube', 'yt', 'dailymotion', 'dm', 'peertube', 'pt', 'twitch', 'tw', 'odysee', 'od', 'rumble', 'ru'];
@@ -187,8 +187,48 @@ const TOOLS = [
description: "Santé API : mode YouTube (YT_SEARCH_MODE), binaire yt-dlp, cache, quota jour, clés. Équivalent GET /healthz.",
inputSchema: { type: 'object', properties: {} },
},
{
name: 'list_playlists',
description: "Playlists de l'utilisateur connecté (NEWTUBE_TOKEN requis). Équivalent GET /api/playlists.",
inputSchema: {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 200, default: 50 },
offset: { type: 'integer', minimum: 0, default: 0 },
q: { type: 'string', description: 'Recherche dans les titres' },
},
},
},
{
name: 'list_likes',
description: "Vidéos likées de l'utilisateur connecté (NEWTUBE_TOKEN requis). Équivalent GET /api/user/likes.",
inputSchema: {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 500, default: 50 },
q: { type: 'string', description: 'Recherche serveur' },
},
},
},
{
name: 'list_subscriptions',
description: "Abonnements aux chaînes de l'utilisateur connecté (NEWTUBE_TOKEN requis). Équivalent GET /api/subscriptions.",
inputSchema: { type: 'object', properties: {} },
},
{
name: 'list_watch_history',
description: "Historique de visionnage de l'utilisateur connecté (NEWTUBE_TOKEN requis). Équivalent GET /api/user/history/watch.",
inputSchema: {
type: 'object',
properties: { limit: { type: 'integer', minimum: 1, maximum: 200, default: 50 } },
},
},
];
function requireAuth() {
if (!TOKEN) throw new Error('NEWTUBE_TOKEN manquant — connectez-vous (POST /api/auth/login) puis exportez NEWTUBE_TOKEN=<accessToken>');
}
async function callTool(name, args = {}) {
switch (name) {
case 'search_videos': {
@@ -265,6 +305,39 @@ async function callTool(name, args = {}) {
const data = await apiFetch('/healthz');
return { summary: `API ${data.status} — YT mode=${data.youtube?.mode}, yt-dlp=${data.youtube?.ytdlp?.version || 'n/a'}`, data };
}
case 'list_playlists': {
requireAuth();
const qs = new URLSearchParams({
limit: String(args.limit ?? 50), offset: String(args.offset ?? 0),
...(args.q ? { q: String(args.q) } : {}),
});
const data = await apiFetch(`/playlists?${qs}`, { auth: true });
const n = Array.isArray(data) ? data.length : 0;
return { summary: `${n} playlist(s)`, data };
}
case 'list_likes': {
requireAuth();
const qs = new URLSearchParams({
limit: String(args.limit ?? 50),
...(args.q ? { q: String(args.q) } : {}),
});
const data = await apiFetch(`/user/likes?${qs}`, { auth: true });
const n = Array.isArray(data) ? data.length : 0;
return { summary: `${n} vidéo(s) likée(s)`, data };
}
case 'list_subscriptions': {
requireAuth();
const data = await apiFetch('/subscriptions', { auth: true });
const n = Array.isArray(data?.items) ? data.items.length : 0;
return { summary: `${n} abonnement(s)`, data };
}
case 'list_watch_history': {
requireAuth();
const qs = new URLSearchParams({ limit: String(args.limit ?? 50) });
const data = await apiFetch(`/user/history/watch?${qs}`, { auth: true });
const n = Array.isArray(data) ? data.length : 0;
return { summary: `${n} entrée(s) d'historique`, data };
}
default:
throw new Error(`unknown_tool: ${name}`);
}