This commit is contained in:
2025-06-08 16:35:06 +09:00
parent 230b3558b9
commit be026db3d8
20 changed files with 182 additions and 73 deletions

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import type React from "react";
import { useEffect, useState } from "react";
interface Feed { interface Feed {
id: string; id: string;

View File

@ -1,5 +1,5 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],

View File

@ -1,22 +1,22 @@
import { Hono } from "hono"; import { Database } from "bun:sqlite";
import { serve } from "@hono/node-server";
import { basicAuth } from "hono/basic-auth";
import path from "path"; import path from "path";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { basicAuth } from "hono/basic-auth";
import { addNewFeedUrl, batchProcess } from "./scripts/fetch_and_generate.js";
import { batchScheduler } from "./services/batch-scheduler.js";
import { config, validateConfig } from "./services/config.js"; import { config, validateConfig } from "./services/config.js";
import { import {
getAllFeedsIncludingInactive,
deleteFeed, deleteFeed,
toggleFeedActive,
getFeedByUrl,
getFeedById,
fetchAllEpisodes, fetchAllEpisodes,
fetchEpisodesWithArticles, fetchEpisodesWithArticles,
getAllFeedsIncludingInactive,
getFeedById,
getFeedByUrl,
getFeedRequests, getFeedRequests,
toggleFeedActive,
updateFeedRequestStatus, updateFeedRequestStatus,
} from "./services/database.js"; } from "./services/database.js";
import { Database } from "bun:sqlite";
import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js";
import { batchScheduler } from "./services/batch-scheduler.js";
// Validate configuration on startup // Validate configuration on startup
try { try {

View File

@ -13,6 +13,7 @@
"openai": "^4.104.0", "openai": "^4.104.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-helmet-async": "^2.0.5",
"react-router-dom": "^7.6.2", "react-router-dom": "^7.6.2",
"rss-parser": "^3.13.0", "rss-parser": "^3.13.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
@ -478,12 +479,16 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@ -526,6 +531,10 @@
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
"react-helmet-async": ["react-helmet-async@2.0.5", "", { "dependencies": { "invariant": "^2.2.4", "react-fast-compare": "^3.2.2", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-router": ["react-router@7.6.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w=="], "react-router": ["react-router@7.6.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w=="],
@ -550,6 +559,8 @@
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],

View File

@ -1,7 +1,7 @@
import js from "@eslint/js"; import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks"; import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh"; import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
export default tseslint.config( export default tseslint.config(

View File

@ -1,9 +1,9 @@
import { Routes, Route, Link, useLocation } from "react-router-dom"; import { Link, Route, Routes, useLocation } from "react-router-dom";
import EpisodeList from "./components/EpisodeList";
import FeedManager from "./components/FeedManager";
import FeedList from "./components/FeedList";
import FeedDetail from "./components/FeedDetail";
import EpisodeDetail from "./components/EpisodeDetail"; import EpisodeDetail from "./components/EpisodeDetail";
import EpisodeList from "./components/EpisodeList";
import FeedDetail from "./components/FeedDetail";
import FeedList from "./components/FeedList";
import FeedManager from "./components/FeedManager";
function App() { function App() {
const location = useLocation(); const location = useLocation();

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { useParams, Link } from "react-router-dom"; import { Helmet } from "react-helmet-async";
import { Link, useParams } from "react-router-dom";
interface EpisodeWithFeedInfo { interface EpisodeWithFeedInfo {
id: string; id: string;
@ -162,6 +163,66 @@ function EpisodeDetail() {
return ( return (
<div className="container"> <div className="container">
<Helmet>
<title>{episode.title} - Voice RSS Summary</title>
<meta
name="description"
content={episode.description || `${episode.title}のエピソード詳細`}
/>
{/* OpenGraph metadata */}
<meta property="og:title" content={episode.title} />
<meta
property="og:description"
content={episode.description || `${episode.title}のエピソード詳細`}
/>
<meta property="og:type" content="article" />
<meta property="og:url" content={window.location.href} />
<meta property="og:site_name" content="Voice RSS Summary" />
<meta property="og:locale" content="ja_JP" />
{/* Image metadata */}
<meta property="og:image" content={`${window.location.origin}/default-thumbnail.svg`} />
<meta property="og:image:type" content="image/svg+xml" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={`${episode.title} - Voice RSS Summary`} />
{/* Twitter Card metadata */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={episode.title} />
<meta
name="twitter:description"
content={episode.description || `${episode.title}のエピソード詳細`}
/>
<meta name="twitter:image" content={`${window.location.origin}/default-thumbnail.svg`} />
<meta name="twitter:image:alt" content={`${episode.title} - Voice RSS Summary`} />
{/* Audio-specific metadata */}
<meta
property="og:audio"
content={
episode.audioPath.startsWith("/")
? `${window.location.origin}${episode.audioPath}`
: `${window.location.origin}/podcast_audio/${episode.audioPath}`
}
/>
<meta property="og:audio:type" content="audio/mpeg" />
{/* Article metadata */}
<meta property="article:published_time" content={episode.createdAt} />
{episode.articlePubDate &&
episode.articlePubDate !== episode.createdAt && (
<meta
property="article:modified_time"
content={episode.articlePubDate}
/>
)}
{episode.feedTitle && (
<meta property="article:section" content={episode.feedTitle} />
)}
</Helmet>
<div className="header"> <div className="header">
<Link <Link
to="/" to="/"
@ -189,7 +250,11 @@ function EpisodeDetail() {
<audio <audio
controls controls
className="audio-player" className="audio-player"
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`} src={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
style={{ width: "100%", height: "60px" }} style={{ width: "100%", height: "60px" }}
> >
使 使
@ -277,7 +342,11 @@ function EpisodeDetail() {
<div> <div>
<strong>URL:</strong> <strong>URL:</strong>
<a <a
href={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`} href={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ marginLeft: "5px" }} style={{ marginLeft: "5px" }}

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
interface Episode { interface Episode {
@ -303,7 +303,13 @@ function EpisodeList() {
<div style={{ display: "flex", gap: "8px" }}> <div style={{ display: "flex", gap: "8px" }}>
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)} onClick={() =>
playAudio(
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`,
)
}
> >
</button> </button>
@ -314,13 +320,20 @@ function EpisodeList() {
</button> </button>
</div> </div>
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && ( {currentAudio ===
(episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`) && (
<div> <div>
<audio <audio
id={episode.audioPath} id={episode.audioPath}
controls controls
className="audio-player" className="audio-player"
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`} src={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
onEnded={() => setCurrentAudio(null)} onEnded={() => setCurrentAudio(null)}
/> />
</div> </div>

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { useParams, Link } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
interface Feed { interface Feed {
id: string; id: string;
@ -284,7 +284,13 @@ function FeedDetail() {
<div style={{ display: "flex", gap: "8px" }}> <div style={{ display: "flex", gap: "8px" }}>
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)} onClick={() =>
playAudio(
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`,
)
}
> >
</button> </button>
@ -295,13 +301,20 @@ function FeedDetail() {
</button> </button>
</div> </div>
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && ( {currentAudio ===
(episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`) && (
<div> <div>
<audio <audio
id={episode.audioPath} id={episode.audioPath}
controls controls
className="audio-player" className="audio-player"
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`} src={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
onEnded={() => setCurrentAudio(null)} onEnded={() => setCurrentAudio(null)}
/> />
</div> </div>

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
interface Feed { interface Feed {

View File

@ -1,13 +1,16 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import App from "./App.tsx"; import App from "./App.tsx";
import "./styles.css"; import "./styles.css";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<BrowserRouter> <HelmetProvider>
<App /> <BrowserRouter>
</BrowserRouter> <App />
</BrowserRouter>
</HelmetProvider>
</StrictMode>, </StrictMode>,
); );

View File

@ -1,5 +1,5 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({
root: ".", // プロジェクトルートを明示 root: ".", // プロジェクトルートを明示

View File

@ -25,6 +25,7 @@
"openai": "^4.104.0", "openai": "^4.104.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-helmet-async": "^2.0.5",
"react-router-dom": "^7.6.2", "react-router-dom": "^7.6.2",
"rss-parser": "^3.13.0", "rss-parser": "^3.13.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"

View File

@ -1,23 +1,23 @@
import crypto from "crypto";
import fs from "fs/promises";
import Parser from "rss-parser"; import Parser from "rss-parser";
import { config } from "../services/config.js";
import { enhanceArticleContent } from "../services/content-extractor.js";
import {
getFeedById,
getFeedByUrl,
getUnprocessedArticles,
markArticleAsProcessed,
saveArticle,
saveEpisode,
saveFeed,
} from "../services/database.js";
import { import {
openAI_ClassifyFeed, openAI_ClassifyFeed,
openAI_GeneratePodcastContent, openAI_GeneratePodcastContent,
} from "../services/llm.js"; } from "../services/llm.js";
import { generateTTS, generateTTSWithoutQueue } from "../services/tts.js";
import { enhanceArticleContent } from "../services/content-extractor.js";
import {
saveFeed,
getFeedByUrl,
getFeedById,
saveArticle,
getUnprocessedArticles,
markArticleAsProcessed,
saveEpisode,
} from "../services/database.js";
import { updatePodcastRSS } from "../services/podcast.js"; import { updatePodcastRSS } from "../services/podcast.js";
import { config } from "../services/config.js"; import { generateTTS, generateTTSWithoutQueue } from "../services/tts.js";
import crypto from "crypto";
import fs from "fs/promises";
interface FeedItem { interface FeedItem {
id?: string; id?: string;

View File

@ -1,8 +1,8 @@
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import path from "path"; import path from "path";
import { config, validateConfig } from "./services/config.js"; import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { batchScheduler } from "./services/batch-scheduler.js"; import { batchScheduler } from "./services/batch-scheduler.js";
import { config, validateConfig } from "./services/config.js";
// Validate configuration on startup // Validate configuration on startup
try { try {
@ -61,8 +61,8 @@ app.get("/podcast_audio/*", async (c) => {
if (range) { if (range) {
// Handle range requests for streaming // Handle range requests for streaming
const parts = range.replace(/bytes=/, "").split("-"); const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0] || "0", 10); const start = Number.parseInt(parts[0] || "0", 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const end = parts[1] ? Number.parseInt(parts[1], 10) : fileSize - 1;
if (start >= fileSize) { if (start >= fileSize) {
return c.text("Requested range not satisfiable", 416, { return c.text("Requested range not satisfiable", 416, {
@ -70,7 +70,7 @@ app.get("/podcast_audio/*", async (c) => {
}); });
} }
const chunkSize = (end - start) + 1; const chunkSize = end - start + 1;
const stream = file.stream(); const stream = file.stream();
return c.body(stream, 206, { return c.body(stream, 206, {

View File

@ -82,7 +82,7 @@ function createConfig(): Config {
voicevox: { voicevox: {
host: getOptionalEnv("VOICEVOX_HOST", "http://localhost:50021"), host: getOptionalEnv("VOICEVOX_HOST", "http://localhost:50021"),
styleId: parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")), styleId: Number.parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")),
}, },
podcast: { podcast: {
@ -100,7 +100,7 @@ function createConfig(): Config {
}, },
admin: { admin: {
port: parseInt(getOptionalEnv("ADMIN_PORT", "3001")), port: Number.parseInt(getOptionalEnv("ADMIN_PORT", "3001")),
username: import.meta.env["ADMIN_USERNAME"], username: import.meta.env["ADMIN_USERNAME"],
password: import.meta.env["ADMIN_PASSWORD"], password: import.meta.env["ADMIN_PASSWORD"],
}, },

View File

@ -1,6 +1,6 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import fs from "fs";
import crypto from "crypto"; import crypto from "crypto";
import fs from "fs";
import { config } from "./config.js"; import { config } from "./config.js";
// Database integrity fixes function // Database integrity fixes function
@ -879,7 +879,7 @@ export interface TTSQueueItem {
export async function addToQueue( export async function addToQueue(
itemId: string, itemId: string,
scriptText: string, scriptText: string,
retryCount: number = 0, retryCount = 0,
): Promise<string> { ): Promise<string> {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const createdAt = new Date().toISOString(); const createdAt = new Date().toISOString();
@ -897,9 +897,7 @@ export async function addToQueue(
} }
} }
export async function getQueueItems( export async function getQueueItems(limit = 10): Promise<TTSQueueItem[]> {
limit: number = 10,
): Promise<TTSQueueItem[]> {
try { try {
const stmt = db.prepare(` const stmt = db.prepare(`
SELECT * FROM tts_queue SELECT * FROM tts_queue

View File

@ -1,4 +1,4 @@
import { OpenAI, ClientOptions } from "openai"; import { type ClientOptions, OpenAI } from "openai";
import { config, validateConfig } from "./config.js"; import { config, validateConfig } from "./config.js";
// Validate config on module load // Validate config on module load

View File

@ -1,9 +1,9 @@
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import { dirname } from "path";
import { fetchEpisodesWithFeedInfo } from "./database.js";
import path from "node:path";
import fsSync from "node:fs"; import fsSync from "node:fs";
import path from "node:path";
import { dirname } from "path";
import { config } from "./config.js"; import { config } from "./config.js";
import { fetchEpisodesWithFeedInfo } from "./database.js";
function escapeXml(text: string): string { function escapeXml(text: string): string {
return text return text

View File

@ -7,7 +7,7 @@ import { config } from "./config.js";
* Split text into natural chunks for TTS processing * Split text into natural chunks for TTS processing
* Aims for approximately 50 characters per chunk, breaking at natural points * Aims for approximately 50 characters per chunk, breaking at natural points
*/ */
function splitTextIntoChunks(text: string, maxLength: number = 50): string[] { function splitTextIntoChunks(text: string, maxLength = 50): string[] {
if (text.length <= maxLength) { if (text.length <= maxLength) {
return [text]; return [text];
} }
@ -242,7 +242,7 @@ async function concatenateAudioFiles(
export async function generateTTSWithoutQueue( export async function generateTTSWithoutQueue(
itemId: string, itemId: string,
scriptText: string, scriptText: string,
retryCount: number = 0, retryCount = 0,
): Promise<string> { ): Promise<string> {
if (!itemId || itemId.trim() === "") { if (!itemId || itemId.trim() === "") {
throw new Error("Item ID is required for TTS generation"); throw new Error("Item ID is required for TTS generation");
@ -337,7 +337,7 @@ export async function generateTTSWithoutQueue(
export async function generateTTS( export async function generateTTS(
itemId: string, itemId: string,
scriptText: string, scriptText: string,
retryCount: number = 0, retryCount = 0,
): Promise<string> { ): Promise<string> {
const maxRetries = 2; const maxRetries = 2;