From b109177d82d54658ab04a83fe608ac62fa4a7107 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Sun, 6 Oct 2024 21:16:09 +0200 Subject: [PATCH 01/15] fix: if there is a itunes:subtitle use that as item title --- generate/rss_parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index e180ebe..bba26ec 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -39,6 +39,7 @@ export type RssItem = { "podcast:season"?: number; "podcast:episode"?: number; "itunes:season"?: number; + "itunes:subtitle"?: string; "itunes:episode"?: number; "itunes:duration"?: string; "itunes:image"?: { @@ -144,7 +145,7 @@ async function getFolderWithUrlFromRssUrl( } export function getItemFileName(item: RssItem) { - const title = convertToValidFilename(item.title!); + const title = convertToValidFilename((item['itunes:subtitle'] || item.title)!); return ( new Date(item.pubDate).getTime() + ` - ${title}.${getExtension(item.enclosure["@url"])}` From f7cc943de5125c59a87d0de4d565b4afd69057d4 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 09:32:09 +0200 Subject: [PATCH 02/15] fix: all generateImage to have custom arguments --- generate/gen_image.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/generate/gen_image.ts b/generate/gen_image.ts index ed30d52..77662a3 100644 --- a/generate/gen_image.ts +++ b/generate/gen_image.ts @@ -6,24 +6,22 @@ import { getConvertCommand } from "../utils/external_commands.ts"; export async function generateImage( title: string, outputPath: string, - fontName: string, + fontName: string, + extraArgs = {}, ) { console.log(green(`Generate image to ${outputPath}`)); - + const args: Record = Object.assign({ + background:'black', + fill:'white', + gravity:'center', + size:'320x240', + font:fontName, + }, extraArgs) const convertCommand = await getConvertCommand(); const cmd = [ convertCommand[0], ...(convertCommand.splice(1)), - "-background", - "black", - "-fill", - "white", - "-gravity", - "center", - "-size", - "320x240", - "-font", - fontName, + ...Object.keys(args).map(k=>([`-${k}`, args[k] ])).flat(), `caption:${title}`, outputPath, ]; From 9ac38bc28cda5a14eea4e2333c9be1bc93fd07b9 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 09:33:32 +0200 Subject: [PATCH 03/15] chore: use async function (for future module addition) --- generate/rss_parser.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index bba26ec..9672585 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -135,9 +135,9 @@ async function getFolderWithUrlFromRssUrl( ); console.log(blue(`→ ${items.length} items`)); if (items.length <= opt.rssSplitLength) { - fs.files.push(getFolderOfStories(items, !!opt.skipRssImageDl)); + fs.files.push(await getFolderOfStories(items, opt)); } else { - fs.files.push(getFolderParts(items, !!opt.skipRssImageDl)); + fs.files.push(await getFolderParts(items, opt)); } } @@ -158,13 +158,13 @@ export function fixUrl(url: string): string { .replace(/^.*http:\/\//g, "http://"); } -function getFolderOfStories( +async function getFolderOfStories( items: RssItem[], - skipRssImageDl: boolean, -): FolderWithUrl { + opt: ModOptions, +): Promise { return { name: i18next.t("storyQuestion"), - files: items.flatMap((item) => { + files: (await Promise.all(items.map(async (item) => { const itemFiles = [{ name: getItemFileName(item), url: fixUrl(item.enclosure["@url"]), @@ -182,14 +182,14 @@ function getFolderOfStories( } return itemFiles; - }), + }))).flat(), }; } -function getFolderParts( +async function getFolderParts( items: RssItem[], - skipRssImageDl: boolean, -): FolderWithUrl { + opt: ModOptions, +): Promise { const partCount = Math.ceil(items.length / 10); const parts: RssItem[][] = []; for (let i = 0; i < partCount; i++) { @@ -201,10 +201,10 @@ function getFolderParts( return { name: i18next.t("partQuestion"), - files: parts.map((part, index) => ({ + files: await Promise.all(parts.map(async (part, index) => ({ name: `${i18next.t("partTitle")} ${index + 1}`, - files: [getFolderOfStories(part, skipRssImageDl)], - })), + files: [await getFolderOfStories(part, opt)], + }))), }; } From 0a9c1c0595611731cfa1f589319aa92671c96268 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 09:34:10 +0200 Subject: [PATCH 04/15] chore: allow images url to be local file --- generate/rss_parser.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 9672585..44c322b 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -225,9 +225,14 @@ async function writeFileWithUrl(fileWithUrl: FileWithUrl, parentPath: string) { if (await exists(filePath)) { console.log(green(` → skip`)); } else { - const resp = await fetch(fileWithUrl.url); - const file = await Deno.open(filePath, { create: true, write: true }); - await resp.body?.pipeTo(file.writable); + if (fileWithUrl.url.startsWith('http')) { + const resp = await fetch(fileWithUrl.url); + const file = await Deno.open(filePath, { create: true, write: true }); + await resp.body?.pipeTo(file.writable); + } else { + const file = await Deno.open(filePath, { create: true, write: true }); + await (await Deno.open(fileWithUrl.url)).readable.pipeTo(file.writable); + } } } From 92ac916cfc7b981839ac86c75cd94f2fe726aba3 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 09:34:38 +0200 Subject: [PATCH 05/15] fix: basic custom module support. For now only for image query --- generate/rss_parser.ts | 4 ++-- types.ts | 8 ++++++++ utils/parse_args.ts | 15 +++++++++++++++ utils/utils.ts | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 44c322b..2884bfa 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -170,8 +170,8 @@ async function getFolderOfStories( url: fixUrl(item.enclosure["@url"]), sha1: "", }]; - const imageUrl = item["itunes:image"]?.["@href"]; - if (!skipRssImageDl && imageUrl) { + const imageUrl = opt.customModule?.fetchRssItemImage ? await opt.customModule?.fetchRssItemImage(item, opt): item["itunes:image"]?.["@href"]; + if (!opt.skipRssImageDl && imageUrl) { itemFiles.push({ name: `${getNameWithoutExt(getItemFileName(item))}.item.${ getExtension(imageUrl) diff --git a/types.ts b/types.ts index c6f9d0c..ff9b661 100644 --- a/types.ts +++ b/types.ts @@ -1,3 +1,5 @@ +import { RssItem } from "./generate/rss_parser.ts"; + export const OPEN_AI_VOICES = [ "alloy", "echo", @@ -7,6 +9,10 @@ export const OPEN_AI_VOICES = [ "shimmer", ] as const; export const OPEN_AI_MODELS = ["tts-1", "tts-1-hd"] as const; + +export interface CustomModule { + fetchRssItemImage?:(item: RssItem, opt: ModOptions) => Promise +} export type ModOptions = { storyPath: string; lang: string; @@ -46,4 +52,6 @@ export type ModOptions = { configFile?: string; isCompiled?: boolean; gui?: boolean; + customScript?: string; + customModule?: CustomModule; }; diff --git a/utils/parse_args.ts b/utils/parse_args.ts index 47e15a8..d96e1d5 100755 --- a/utils/parse_args.ts +++ b/utils/parse_args.ts @@ -33,6 +33,14 @@ export async function parseArgs(args: string[]) { opts = { ...opts, ...optsFromFile, storyPath: opts.storyPath }; } + if (opts.customScript) { + try { + opts.customModule = await import(opts.customScript); + } catch (error) { + console.error(error); + } + } + if (opts.extract) { return await new PackExtractor(opts).extractPack(); } else if (opts.gui) { @@ -298,6 +306,13 @@ export async function parseArgs(args: string[]) { hidden: true, describe: "true if compiled with deno compile", }) + .option("custom-script", { + demandOption: false, + boolean: false, + default: undefined, + type: "string", + describe: "custom script to be used for custom image... handling", + }) .version(false) .demandCommand(1) .parse(); diff --git a/utils/utils.ts b/utils/utils.ts index b2eb6f6..44b8c55 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -228,7 +228,7 @@ export function groupBy( } export function cleanOption(opt: ModOptions): ModOptions { - const cleanOpt: { [k: string]: string | number | boolean } = {}; + const cleanOpt: { [k: string]: string | number | boolean | any } = {}; Object.entries(opt).filter(([key]) => key.length > 1 && !key.includes("-") && key != "$0" ) From b3312a116f53eabe6ccab28165231c66ec86cf4c Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 12:32:14 +0200 Subject: [PATCH 06/15] fix: store rss items metadata and use that to get the text to tts. this ensure we dont rely on file name for tts which will help tts with caracters which would have been removed from file name --- generate/gen_missing_items.ts | 18 +++++++++++++++--- generate/rss_parser.ts | 27 +++++++++++++++++---------- utils/utils.ts | 3 ++- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/generate/gen_missing_items.ts b/generate/gen_missing_items.ts index 5b0830a..8332225 100644 --- a/generate/gen_missing_items.ts +++ b/generate/gen_missing_items.ts @@ -14,6 +14,7 @@ import { generateImage } from "./gen_image.ts"; import { generateAudio } from "./gen_audio.ts"; import i18next from "https://deno.land/x/i18next@v23.15.1/index.js"; import { join } from "@std/path"; +import { exists } from "@std/fs"; import type { ModOptions } from "../types.ts"; @@ -35,7 +36,7 @@ export async function genMissingItems( if (!opt.skipImageItemGen || !opt.skipAudioItemGen) { await checkRunPermission(); if (!opt.skipImageItemGen && !getFolderImageItem(folder)) { - if (isRoot && opt.useThumbnailAsRootImage) { + if (isRoot && opt.useThumbnailAsRootImage && await exists(join(rootpath, "thumbnail.png"))) { await Deno.copyFile( join(rootpath, "thumbnail.png"), `${rootpath}/0-item.png`, @@ -75,16 +76,27 @@ export async function genMissingItems( opt, ); } else if (isStory(file)) { + let title = getTitle(getNameWithoutExt(file.name)); + const metadataPath =join(rootpath, getNameWithoutExt(file.name)+ "-metadata.json"); + console.log('metadataPath', metadataPath, await exists(metadataPath)) + if (await exists(metadataPath)) { + try { + const metadata =JSON.parse(await Deno.readTextFile(metadataPath)) + title = metadata?.title ?? title + } catch (error) { + console.error(`error reading json metadata: ${metadataPath}`, error); + } + } if (!opt.skipImageItemGen && !getFileImageItem(file, folder)) { await generateImage( - getTitle(getNameWithoutExt(file.name)), + title, `${rootpath}/${getNameWithoutExt(file.name)}-generated.item.png`, opt.imageItemGenFont, ); } if (!opt.skipAudioItemGen && !getFileAudioItem(file, folder)) { await generateAudio( - getTitle(getNameWithoutExt(file.name)), + title, `${rootpath}/${getNameWithoutExt(file.name)}-generated.item.wav`, lang, opt, diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 2884bfa..47da3d4 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -46,20 +46,22 @@ export type RssItem = { "@href": string; }; }; -export type FolderWithUrl = { +export type FolderWithUrlOrData = { name: string; - files: (FolderWithUrl | FileWithUrl)[]; + files: (FolderWithUrlOrData | FileWithUrlOrData)[]; metadata?: Metadata; thumbnailUrl?: string; }; -export type FileWithUrl = File & { - url: string; +export type FileWithUrlOrData = File & { + url?: string; +} & { + data?: any; }; async function getFolderWithUrlFromRssUrl( url: string, opt: ModOptions, -): Promise { +): Promise { console.log(green(`→ url = ${url}`)); const resp = await fetch(url); @@ -104,6 +106,7 @@ async function getFolderWithUrlFromRssUrl( const rssName = convertToValidFilename(rss.title); const imgUrl = rss.image?.url || rss.itunes?.image?.["@href"] || ""; const fss: FolderWithUrl[] = rssItems.map((items, index) => { + const fss: FolderWithUrlOrData[] = rssItems.map((items, index) => { const name = rssItems.length > 1 ? `${rssName} ${ seasonIds[index] === "0" @@ -161,7 +164,7 @@ export function fixUrl(url: string): string { async function getFolderOfStories( items: RssItem[], opt: ModOptions, -): Promise { +): Promise { return { name: i18next.t("storyQuestion"), files: (await Promise.all(items.map(async (item) => { @@ -169,6 +172,10 @@ async function getFolderOfStories( name: getItemFileName(item), url: fixUrl(item.enclosure["@url"]), sha1: "", + }, { + name: getNameWithoutExt(getItemFileName(item)) + '-metadata.json', + data:{...item, title: item['itunes:subtitle'] || item.title}, + sha1: "", }]; const imageUrl = opt.customModule?.fetchRssItemImage ? await opt.customModule?.fetchRssItemImage(item, opt): item["itunes:image"]?.["@href"]; if (!opt.skipRssImageDl && imageUrl) { @@ -189,7 +196,7 @@ async function getFolderOfStories( async function getFolderParts( items: RssItem[], opt: ModOptions, -): Promise { +): Promise { const partCount = Math.ceil(items.length / 10); const parts: RssItem[][] = []; for (let i = 0; i < partCount; i++) { @@ -208,13 +215,13 @@ async function getFolderParts( }; } -async function writeFolderWithUrl(folder: FolderWithUrl, parentPath: string) { +async function writeFolderWithUrl(folder: FolderWithUrlOrData, parentPath: string) { const path = join(parentPath, folder.name); await Deno.mkdir(path, { recursive: true }); for (const file of folder.files) { isFolder(file) - ? await writeFolderWithUrl(file as FolderWithUrl, path) - : await writeFileWithUrl(file as FileWithUrl, path); + ? await writeFolderWithUrl(file as FolderWithUrlOrData, path) + : await writeFileWithUrl(file as FileWithUrlOrData, path); } } diff --git a/utils/utils.ts b/utils/utils.ts index 44b8c55..192b74f 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -211,7 +211,8 @@ export function rmDiacritic(s: string) { } export function convertToValidFilename(name: string): string { - return name.replace(/[^A-Za-zÀ-ÖØ-öø-ÿ0-9_\-.,()'! ]/g, " ").trim(); + // first we remove all unwanted chars. Then we "trim" the spaces + return name.replace(/[^A-Za-zÀ-ÖØ-öø-ÿ0-9_\-.,()'! ]/g, " ").replace(/\s{1,}/, " ").trim(); } export function cleanStageName(name: string): string { From 8fc4d7f25e2070e2e3a0c9c4c72a936771db3b70 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 12:32:35 +0200 Subject: [PATCH 07/15] fix: better handling of rss image --- generate/rss_parser.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 47da3d4..2bcf3a7 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -28,6 +28,9 @@ export type Rss = { "@href"?: string; }; }; + "itunes:image"?: { + "@href": string; + }; item: RssItem[]; }; export type RssItem = { @@ -104,8 +107,7 @@ async function getFolderWithUrlFromRssUrl( rssItems = sorted.map((i) => i[1]); } const rssName = convertToValidFilename(rss.title); - const imgUrl = rss.image?.url || rss.itunes?.image?.["@href"] || ""; - const fss: FolderWithUrl[] = rssItems.map((items, index) => { + const imgUrl = rss.image?.url || rss.itunes?.image?.["@href"] || rss["itunes:image"]?.["@href"] || ""; const fss: FolderWithUrlOrData[] = rssItems.map((items, index) => { const name = rssItems.length > 1 ? `${rssName} ${ @@ -120,7 +122,7 @@ async function getFolderWithUrlFromRssUrl( thumbnailUrl: opt.rssUseImageAsThumbnail ? items.find((item) => item["itunes:image"]?.["@href"]) ?.["itunes:image"]?.["@href"] - : undefined, + : imgUrl, metadata: { ...metadata, title: name }, }; }); From df7aa66937eb779f8387a6a848bd78897c9a0e9e Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 12:33:05 +0200 Subject: [PATCH 08/15] chore: was missing from commit about metadata --- generate/rss_parser.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 2bcf3a7..2ff6461 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -227,21 +227,30 @@ async function writeFolderWithUrl(folder: FolderWithUrlOrData, parentPath: strin } } -async function writeFileWithUrl(fileWithUrl: FileWithUrl, parentPath: string) { - const filePath = join(parentPath, fileWithUrl.name); - console.log(blue(`Download ${fileWithUrl.url}\n → ${filePath}`)); +async function writeFileWithUrl(fileWithUrlOrData: FileWithUrlOrData, parentPath: string) { + const filePath = join(parentPath, fileWithUrlOrData.name); + console.log(blue(`Download ${fileWithUrlOrData.url}\n → ${filePath}`)); if (await exists(filePath)) { console.log(green(` → skip`)); - } else { - if (fileWithUrl.url.startsWith('http')) { - const resp = await fetch(fileWithUrl.url); + } else if (fileWithUrlOrData.url) { + if (fileWithUrlOrData.url.startsWith('http')) { + const resp = await fetch(fileWithUrlOrData.url); const file = await Deno.open(filePath, { create: true, write: true }); await resp.body?.pipeTo(file.writable); } else { const file = await Deno.open(filePath, { create: true, write: true }); - await (await Deno.open(fileWithUrl.url)).readable.pipeTo(file.writable); + await (await Deno.open(fileWithUrlOrData.url)).readable.pipeTo(file.writable); } + } else if (fileWithUrlOrData.data) { + let toWrite = fileWithUrlOrData.data; + if (typeof toWrite === 'object') { + toWrite = JSON.stringify(toWrite); + } + if (typeof toWrite === 'string') { + toWrite = new TextEncoder().encode(toWrite) + } + await Deno.writeFile(filePath, toWrite); } } From b4f0134efc1f5aaa6cfce4443a38bd58384e32e3 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 12:33:24 +0200 Subject: [PATCH 09/15] fix: fix for when using `skipImageConvert` --- generate/rss_parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 2ff6461..a887e6a 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -130,7 +130,7 @@ async function getFolderWithUrlFromRssUrl( const fs = fss[index]; if (imgUrl) { fs.files.push({ - name: `0-item-to-resize.${getExtension(imgUrl)}`, + name: opt.skipImageConvert ? `0-item.${getExtension(imgUrl)}` : `0-item-to-resize.${getExtension(imgUrl)}`, url: imgUrl, sha1: "", }); From 3a3dbbb97ae473eb4b5440aa4d8fb86a810959fa Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 13:45:39 +0200 Subject: [PATCH 10/15] fix: regression fix for generated images names (would trigger tts again and again) --- utils/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/utils.ts b/utils/utils.ts index 192b74f..0cac8b0 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -76,10 +76,10 @@ export function getFolderImageItem(folder: Folder) { } export function getFileAudioItem(file: File, parent: Folder) { - const nameWithoutExt = getNameWithoutExt(file.name); + const nameWithoutExt = rmDiacritic(getNameWithoutExt(file.name)); const audioItem = parent.files.find( (f) => - getNameWithoutExt(rmDiacritic(f.name)).replace(/.item$/, "") === + getNameWithoutExt(rmDiacritic(f.name)).replace(/(-generated)?.item$/, "") === rmDiacritic(nameWithoutExt) && fileAudioItemRegEx.test(f.name), ) as File; From 091927305dc1a5b72b3f0efb1b3eb0e30191d794 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 13:46:54 +0200 Subject: [PATCH 11/15] feat: story.json export now uses metadata title instead of filename. Ensures all characters are there. Also added audio duration (for players needing story duration) --- gen_pack.ts | 2 +- generate/gen_missing_items.ts | 1 - serialize/converter.ts | 45 ++++++++++++++++++++++++----------- serialize/serialize-types.ts | 3 +++ serialize/serializer.ts | 4 +++- 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/gen_pack.ts b/gen_pack.ts index 42f80ed..64eae1e 100644 --- a/gen_pack.ts +++ b/gen_pack.ts @@ -116,7 +116,7 @@ export async function generatePack(opt: ModOptions) { folder = await fsToFolder(storyPath, true); const metadata: Metadata = await getMetadata(storyPath, opt); - const pack = folderToPack(folder, metadata); + const pack = await folderToPack(folder, metadata); const nightModeAudioItemName = getNightModeAudioItem(folder); const serializedPack = await serializePack(pack, opt, { autoNextStoryTransition: opt.autoNextStoryTransition, diff --git a/generate/gen_missing_items.ts b/generate/gen_missing_items.ts index 8332225..4dca588 100644 --- a/generate/gen_missing_items.ts +++ b/generate/gen_missing_items.ts @@ -78,7 +78,6 @@ export async function genMissingItems( } else if (isStory(file)) { let title = getTitle(getNameWithoutExt(file.name)); const metadataPath =join(rootpath, getNameWithoutExt(file.name)+ "-metadata.json"); - console.log('metadataPath', metadataPath, await exists(metadataPath)) if (await exists(metadataPath)) { try { const metadata =JSON.parse(await Deno.readTextFile(metadataPath)) diff --git a/serialize/converter.ts b/serialize/converter.ts index a6415b7..10267d5 100644 --- a/serialize/converter.ts +++ b/serialize/converter.ts @@ -17,12 +17,18 @@ import { getFileImageItem, getFolderAudioItem, getFolderImageItem, + getNameWithoutExt, isFolder, isStory, isZipFile, } from "../utils/utils.ts"; -export function folderToPack(folder: Folder, metadata?: Metadata): Pack { +import { join } from "@std/path"; +import { exists } from "@std/fs"; + +import {duration} from 'jsr:@dbushell/audio-duration'; + +export async function folderToPack(folder: Folder, metadata?: Metadata): Promise { const firstSubFolder = folder.files.find((f) => isFolder(f)) as Folder; const audio = getFolderAudioItem(folder); const image = getFolderImageItem(folder); @@ -46,8 +52,8 @@ export function folderToPack(folder: Folder, metadata?: Metadata): Pack { name: "Action node", options: [ firstSubFolder - ? folderToMenu(firstSubFolder, "") - : fileToStory(firstStoryFile(folder)!), + ? await folderToMenu(firstSubFolder, "") + : await fileToStory(firstStoryFile(folder)!), ], }, }, @@ -77,7 +83,7 @@ export function folderToPack(folder: Folder, metadata?: Metadata): Pack { return res; } -export function folderToMenu(folder: Folder, path: string): Menu { +export async function folderToMenu(folder: Folder, path: string): Promise { const image = getFolderImageItem(folder); const audio = getFolderAudioItem(folder); const folderPath = folder.path + "/"; @@ -95,16 +101,16 @@ export function folderToMenu(folder: Folder, path: string): Menu { okTransition: { class: "ActionNode", name: folder.name, - options: folder.files - .map((f) => + options: (await Promise.all(folder.files + .map(async (f) => isFolder(f) - ? folderToMenu(f as Folder, path + "/" + f.name) + ? await folderToMenu(f as Folder, path + "/" + f.name) : isStory(f as File) - ? fileToStoryItem(f as File, folder) + ? await fileToStoryItem(f as File, folder) : isZipFile(f as File) - ? fileToZipMenu(`${path}/${folder.name}/${f.name}`) + ? await fileToZipMenu(`${path}/${folder.name}/${f.name}`) : null - ) + ))) .filter((f) => f) as (Menu | ZipMenu | StoryItem)[], }, }; @@ -126,12 +132,22 @@ function getMTime(path: string | undefined) { } } -export function fileToStoryItem(file: File, parent: Folder): StoryItem { +export async function fileToStoryItem(file: File, parent: Folder): Promise { const audio = getFileAudioItem(file, parent); const image = getFileImageItem(file, parent); + let name = cleanStageName(file.name); + const metadataPath = join(parent.path!, getNameWithoutExt(file.name)+ "-metadata.json"); + if (await exists(metadataPath)) { + try { + const metadata =JSON.parse(await Deno.readTextFile(metadataPath)) + name = metadata?.title ?? name + } catch (error) { + console.error(`error reading json metadata: ${metadataPath}`, error); + } + } const res: StoryItem = { class: "StageNode-StoryItem", - name: cleanStageName(file.name), + name, path: file.path, audio: audio?.assetName ?? null, image: image?.assetName ?? null, @@ -141,14 +157,15 @@ export function fileToStoryItem(file: File, parent: Folder): StoryItem { imageTimestamp: parent.path && image ? getMTime(image?.path) : undefined, pathTimestamp: parent.path && file.path ? getMTime(file.path) : undefined, okTransition: { - name: cleanStageName(file.name), + name, class: "ActionNode", options: [ { class: "StageNode-Story", audio: getFileAudioStory(file)?.assetName ?? null, + duration: (await duration(file.path!)), image: null, - name: cleanStageName(file.name), + name, okTransition: null, }, ], diff --git a/serialize/serialize-types.ts b/serialize/serialize-types.ts index fe1ee67..d1a7aad 100644 --- a/serialize/serialize-types.ts +++ b/serialize/serialize-types.ts @@ -29,6 +29,7 @@ export type Menu = { path?: string; audioPath?: string; imagePath?: string; + duration?: number; audioTimestamp?: number; imageTimestamp?: number; pathTimestamp?: number; @@ -54,6 +55,7 @@ export type StoryAction = { export type Story = { class: "StageNode-Story"; audio: string; + duration?: number; image: null; name: string; path?: string; @@ -64,6 +66,7 @@ export type StageNode = { uuid: string; squareOne: boolean; type: string; + duration?: number; name: string; homeTransition: { actionNode: string; optionIndex: number } | null; audio: string | null; diff --git a/serialize/serializer.ts b/serialize/serializer.ts index 260181d..17b4212 100644 --- a/serialize/serializer.ts +++ b/serialize/serializer.ts @@ -227,7 +227,9 @@ async function exploreStageNode( type: "stage", uuid, }; - + if (((stageNode as Story).duration !== undefined)) { + serializedStageNode.duration = (stageNode as Story).duration; + } if ( serializedStageNode.okTransition === null && stageNode.class === "StageNode-Story" && From ac9913293cf1bdfec7df6e7fd1767e0f95a96dae Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 15:49:55 +0200 Subject: [PATCH 12/15] chore: gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 23eb259..731df93 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,8 @@ /.idea/inspectionProfiles /.idea/sonarlint/ /.idea/ -/gui/node_modules/ /gui/src-tauri/target/ /gui/dist /gui/.vite +node_modules +vendor \ No newline at end of file From f1cd968bdcdb9f37b622b5e1d809d5f98bd04ef9 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 17:00:38 +0200 Subject: [PATCH 13/15] chore: test fixes --- generate/rss_parser.ts | 4 ++-- types.ts | 2 +- utils/utils.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 2558c0e..45e1b4b 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -58,7 +58,7 @@ export type FolderWithUrlOrData = { export type FileWithUrlOrData = File & { url?: string; } & { - data?: any; + data?: string | Uint8Array | object; }; async function getFolderWithUrlFromRssUrl( @@ -250,7 +250,7 @@ async function writeFileWithUrl(fileWithUrlOrData: FileWithUrlOrData, parentPath if (typeof toWrite === 'string') { toWrite = new TextEncoder().encode(toWrite) } - await Deno.writeFile(filePath, toWrite); + await Deno.writeFile(filePath, toWrite as Uint8Array); } } diff --git a/types.ts b/types.ts index ff9b661..7033aac 100644 --- a/types.ts +++ b/types.ts @@ -1,4 +1,4 @@ -import { RssItem } from "./generate/rss_parser.ts"; +import type { RssItem } from "./generate/rss_parser.ts"; export const OPEN_AI_VOICES = [ "alloy", diff --git a/utils/utils.ts b/utils/utils.ts index c458010..193a21b 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -231,7 +231,7 @@ export function groupBy( } export function cleanOption(opt: ModOptions): ModOptions { - const cleanOpt: { [k: string]: string | number | boolean | any } = {}; + const cleanOpt: { [k: string]: string | number | boolean | object } = {}; Object.entries(opt).filter(([key]) => key.length > 1 && !key.includes("-") && key != "$0" ) From dfcc8c3211be5c7a1e0583a4cd3d65bbae27c2d9 Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 17:34:07 +0200 Subject: [PATCH 14/15] chore: only use itunes:subtitle as title when `rssUseSubtitleAsTitle` is set --- generate/rss_parser.ts | 12 ++++++------ types.ts | 1 + utils/parse_args.ts | 6 ++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index 45e1b4b..c48766a 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -149,8 +149,8 @@ async function getFolderWithUrlFromRssUrl( return fss; } -export function getItemFileName(item: RssItem) { - const title = convertToValidFilename((item['itunes:subtitle'] || item.title)!); +export function getItemFileName(item: RssItem, opt: ModOptions) { + const title = convertToValidFilename((opt.rssUseSubtitleAsTitle && item['itunes:subtitle'] || item.title)!); return ( new Date(item.pubDate).getTime() + ` - ${title}.${getExtension(item.enclosure["@url"])}` @@ -171,18 +171,18 @@ async function getFolderOfStories( name: i18next.t("storyQuestion"), files: (await Promise.all(items.map(async (item) => { const itemFiles = [{ - name: getItemFileName(item), + name: getItemFileName(item, opt), url: fixUrl(item.enclosure["@url"]), sha1: "", }, { - name: getNameWithoutExt(getItemFileName(item)) + '-metadata.json', - data:{...item, title: item['itunes:subtitle'] || item.title}, + name: getNameWithoutExt(getItemFileName(item, opt)) + '-metadata.json', + data:{...item, title: (opt.rssUseSubtitleAsTitle && item['itunes:subtitle']) || item.title}, sha1: "", }]; const imageUrl = opt.customModule?.fetchRssItemImage ? await opt.customModule?.fetchRssItemImage(item, opt): item["itunes:image"]?.["@href"]; if (!opt.skipRssImageDl && imageUrl) { itemFiles.push({ - name: `${getNameWithoutExt(getItemFileName(item))}.item.${ + name: `${getNameWithoutExt(getItemFileName(item, opt))}.item.${ getExtension(imageUrl) }`, url: imageUrl, diff --git a/types.ts b/types.ts index 7033aac..117e6a7 100644 --- a/types.ts +++ b/types.ts @@ -20,6 +20,7 @@ export type ModOptions = { rssSplitSeasons?: boolean; rssMinDuration: number; rssUseImageAsThumbnail?: boolean; + rssUseSubtitleAsTitle?: boolean; skipImageItemGen?: boolean; thumbnailFromFirstItem: boolean; useThumbnailAsRootImage?: boolean; diff --git a/utils/parse_args.ts b/utils/parse_args.ts index d96e1d5..cf8b1c0 100755 --- a/utils/parse_args.ts +++ b/utils/parse_args.ts @@ -181,6 +181,12 @@ export async function parseArgs(args: string[]) { default: 0, describe: "RSS min episode duration", }) + .option("rss-use-subtitle-as-title", { + demandOption: false, + boolean: true, + default: false, + describe: "Use rss items subtitle as title", + }) .option("rss-use-image-as-thumbnail", { demandOption: false, boolean: true, From a04ddda533750aeda57de11c960aae7e8ebd96ed Mon Sep 17 00:00:00 2001 From: farfromrefug Date: Mon, 7 Oct 2024 17:35:25 +0200 Subject: [PATCH 15/15] feat: you can now customize i18n from command line / config --- generate/gen_missing_items.ts | 2 +- generate/rss_parser.ts | 11 ++++++----- types.ts | 1 + utils/i18n.ts | 8 ++++---- utils/parse_args.ts | 7 +++++++ 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/generate/gen_missing_items.ts b/generate/gen_missing_items.ts index 4dca588..8aa82ca 100644 --- a/generate/gen_missing_items.ts +++ b/generate/gen_missing_items.ts @@ -59,7 +59,7 @@ export async function genMissingItems( } if (!opt.skipAudioItemGen && isRoot && !getNightModeAudioItem(folder)) { await generateAudio( - i18next.t("NightModeTransition"), + opt.i18n?.['NightModeTransition'] || i18next.t("NightModeTransition"), `${rootpath}/0-night-mode.wav`, lang, opt, diff --git a/generate/rss_parser.ts b/generate/rss_parser.ts index c48766a..83c4b8d 100644 --- a/generate/rss_parser.ts +++ b/generate/rss_parser.ts @@ -12,6 +12,7 @@ import { blue, green } from "@std/fmt/colors"; import { exists } from "@std/fs"; import { parse } from "@libs/xml"; import i18next from "https://deno.land/x/i18next@v23.15.1/index.js"; +import { sprintf } from "@std/fmt/printf"; import type { File, Metadata } from "../serialize/serialize-types.ts"; import type { ModOptions } from "../types.ts"; import { convertImage } from "./gen_image.ts"; @@ -112,8 +113,8 @@ async function getFolderWithUrlFromRssUrl( const name = rssItems.length > 1 ? `${rssName} ${ seasonIds[index] === "0" - ? i18next.t("special") - : i18next.t("season") + " " + seasonIds[index] + ? opt.i18n?.['special'] || i18next.t("special") + : sprintf(opt.i18n?.['season'] || i18next.t("season"), seasonIds[index]) }` : rssName; return { @@ -168,7 +169,7 @@ async function getFolderOfStories( opt: ModOptions, ): Promise { return { - name: i18next.t("storyQuestion"), + name: opt.i18n?.['storyQuestion'] || i18next.t("storyQuestion"), files: (await Promise.all(items.map(async (item) => { const itemFiles = [{ name: getItemFileName(item, opt), @@ -209,9 +210,9 @@ async function getFolderParts( } return { - name: i18next.t("partQuestion"), + name: opt.i18n?.['partQuestion'] || i18next.t("partQuestion"), files: await Promise.all(parts.map(async (part, index) => ({ - name: `${i18next.t("partTitle")} ${index + 1}`, + name: sprintf( i18next.t("partTitle"),index + 1), files: [await getFolderOfStories(part, opt)], }))), }; diff --git a/types.ts b/types.ts index 117e6a7..6de0002 100644 --- a/types.ts +++ b/types.ts @@ -55,4 +55,5 @@ export type ModOptions = { gui?: boolean; customScript?: string; customModule?: CustomModule; + i18n?:Record }; diff --git a/utils/i18n.ts b/utils/i18n.ts index 8aa6ebf..94f1653 100644 --- a/utils/i18n.ts +++ b/utils/i18n.ts @@ -4,18 +4,18 @@ import { yellow } from "@std/fmt/colors"; import $ from "@david/dax"; const en = { - season: "Season", + season: "Season %d", special: "Special", partQuestion: "Choose your part", - partTitle: "Part", + partTitle: "Part %d", storyQuestion: "Choose your story", NightModeTransition: "Want to listen to a new story?", }; const fr = { - season: "Saison", + season: "Saison %d", special: "Hors-Série", partQuestion: "Choisis ta partie", - partTitle: "Partie", + partTitle: "Partie %d", storyQuestion: "Choisis ton histoire", NightModeTransition: "Tu veux écouter une nouvelle histoire ?", }; diff --git a/utils/parse_args.ts b/utils/parse_args.ts index cf8b1c0..141bb08 100755 --- a/utils/parse_args.ts +++ b/utils/parse_args.ts @@ -319,6 +319,13 @@ export async function parseArgs(args: string[]) { type: "string", describe: "custom script to be used for custom image... handling", }) + .option("i18n", { + demandOption: false, + boolean: false, + default: undefined, + type: "object", + describe: "custom translation options", + }) .version(false) .demandCommand(1) .parse();