Fix import of chars and lorebook by chub URL Closes #4011

c3376da3939e9c55555515a17f0e50642e978655

Cohee <18619528+Cohee1207@users.noreply.github.com>

1 files changed, +53 -20Ignore whitespace
src/endpoints/content-manager.js+53 -20
@@ -17,6 +17,7 @@ const contentIndexPath = path.join(contentDirectory, 'index.json');
1717const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
1818
1919const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);
20+const USER_AGENT = 'SillyTavern';
2021
2122/**
2223 * @typedef {Object} ContentItem
@@ -323,48 +324,80 @@ function getContentLog(contentLogPath) {
323324}
324325
325326async function downloadChubLorebook(id) {
326- const result = await fetch('https://api.chub.ai/api/lorebooks/download', {
327+ const [lorebooks, creatorName, projectName] = id.split('/');
327- method: 'POST',
328+ const result = await fetch(`https://api.chub.ai/api/${lorebooks}/${creatorName}/${projectName}`, {
328- headers: { 'Content-Type': 'application/json' },
329+ method: 'GET',
329- body: JSON.stringify({
330+ headers: { 'Accept': 'application/json', 'User-Agent': USER_AGENT },
330- 'fullPath': id,
331- 'format': 'SILLYTAVERN',
332- }),
333331 });
334332
335333 if (!result.ok) {
336334 const text = await result.text();
337335 console.error('Chub returned error', result.statusText, text);
336+ throw new Error('Failed to fetch lorebook metadata');
337+ }
338+
339+ /** @type {any} */
340+ const metadata = await result.json();
341+ const projectId = metadata.node?.id;
342+
343+ if (!projectId) {
344+ throw new Error('Project ID not found in lorebook metadata');
345+ }
346+
347+ const downloadUrl = `https://api.chub.ai/api/v4/projects/${projectId}/repository/files/raw%252Fsillytavern_raw.json/raw`;
348+ const downloadResult = await fetch(downloadUrl, {
349+ method: 'GET',
350+ headers: { 'Accept': 'application/json', 'User-Agent': USER_AGENT },
351+ });
352+
353+ if (!downloadResult.ok) {
354+ const text = await downloadResult.text();
355+ console.error('Chub returned error', downloadResult.statusText, text);
338356 throw new Error('Failed to download lorebook');
339357 }
340358
341359 const name = id.split('/').pop()projectName;
342360 const buffer = Buffer.from(await resultdownloadResult.arrayBuffer());
343361 const fileName = `${sanitize(name)}.json`;
344362 const fileType = resultdownloadResult.headers.get('content-type');
345363
346364 return { buffer, fileName, fileType };
347365}
348366
349367async function downloadChubCharacter(id) {
350- const result = await fetch('https://api.chub.ai/api/characters/download', {
368+ const [creatorName, projectName] = id.split('/');
351- method: 'POST',
369+ const result = await fetch(`https://api.chub.ai/api/characters/${creatorName}/${projectName}`, {
352- headers: { 'Content-Type': 'application/json' },
370+ method: 'GET',
353- body: JSON.stringify({
371+ headers: { 'Accept': 'application/json', 'User-Agent': USER_AGENT },
354- 'format': 'tavern',
355- 'fullPath': id,
356- }),
357372 });
358373
359374 if (!result.ok) {
360375 const text = await result.text();
361376 console.error('Chub returned error', result.statusText, text);
377+ throw new Error('Failed to fetch character metadata');
378+ }
379+
380+ /** @type {any} */
381+ const metadata = await result.json();
382+ const downloadUrl = metadata.node?.max_res_url;
383+
384+ if (!downloadUrl) {
385+ throw new Error('Download URL not found in character metadata');
386+ }
387+
388+ const downloadResult = await fetch(downloadUrl);
389+
390+ if (!downloadResult.ok) {
391+ const text = await downloadResult.text();
392+ console.error('Chub returned error', downloadResult.statusText, text);
362393 throw new Error('Failed to download character');
363394 }
364395
365396 const buffer = Buffer.from(await resultdownloadResult.arrayBuffer());
366- const fileName = result.headers.get('content-disposition')?.split('filename=')[1] || `${sanitize(id)}.png`;
397+ const fileName =
367- const fileType = result.headers.get('content-type');
398+ downloadResult.headers.get('content-disposition')?.split('filename=')[1]?.replace(/["']/g, '') ||
399+ `${sanitize(projectName)}.png`;
400+ const fileType = downloadResult.headers.get('content-type');
368401
369402 return { buffer, fileName, fileType };
370403}