Fix database conflict and update schema

This commit is contained in:
2025-06-07 13:58:08 +09:00
parent 9580740398
commit 9b18963041
6 changed files with 260 additions and 214 deletions

View File

@ -10,6 +10,8 @@ import {
getFeedByUrl, getFeedByUrl,
fetchAllEpisodes, fetchAllEpisodes,
fetchEpisodesWithArticles, fetchEpisodesWithArticles,
getFeedRequests,
updateFeedRequestStatus,
} from "./services/database.js"; } from "./services/database.js";
import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js"; import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js";
@ -200,17 +202,102 @@ app.get("/api/admin/episodes/simple", async (c) => {
} }
}); });
// Feed requests management
app.get("/api/admin/feed-requests", async (c) => {
try {
const status = c.req.query("status");
const requests = await getFeedRequests(status);
return c.json(requests);
} catch (error) {
console.error("Error fetching feed requests:", error);
return c.json({ error: "Failed to fetch feed requests" }, 500);
}
});
app.patch("/api/admin/feed-requests/:id/approve", async (c) => {
try {
const requestId = c.req.param("id");
const body = await c.req.json();
const { adminNotes } = body;
// First get the request to get the URL
const requests = await getFeedRequests();
const request = requests.find(r => r.id === requestId);
if (!request) {
return c.json({ error: "Feed request not found" }, 404);
}
if (request.status !== 'pending') {
return c.json({ error: "Feed request already processed" }, 400);
}
// Add the feed
await addNewFeedUrl(request.url);
// Update request status
const updated = await updateFeedRequestStatus(
requestId,
'approved',
'admin',
adminNotes
);
if (!updated) {
return c.json({ error: "Failed to update request status" }, 500);
}
return c.json({
success: true,
message: "Feed request approved and feed added successfully"
});
} catch (error) {
console.error("Error approving feed request:", error);
return c.json({ error: "Failed to approve feed request" }, 500);
}
});
app.patch("/api/admin/feed-requests/:id/reject", async (c) => {
try {
const requestId = c.req.param("id");
const body = await c.req.json();
const { adminNotes } = body;
const updated = await updateFeedRequestStatus(
requestId,
'rejected',
'admin',
adminNotes
);
if (!updated) {
return c.json({ error: "Feed request not found" }, 404);
}
return c.json({
success: true,
message: "Feed request rejected successfully"
});
} catch (error) {
console.error("Error rejecting feed request:", error);
return c.json({ error: "Failed to reject feed request" }, 500);
}
});
// System management // System management
app.get("/api/admin/stats", async (c) => { app.get("/api/admin/stats", async (c) => {
try { try {
const feeds = await getAllFeedsIncludingInactive(); const feeds = await getAllFeedsIncludingInactive();
const episodes = await fetchAllEpisodes(); const episodes = await fetchAllEpisodes();
const feedRequests = await getFeedRequests();
const stats = { const stats = {
totalFeeds: feeds.length, totalFeeds: feeds.length,
activeFeeds: feeds.filter(f => f.active).length, activeFeeds: feeds.filter(f => f.active).length,
inactiveFeeds: feeds.filter(f => !f.active).length, inactiveFeeds: feeds.filter(f => !f.active).length,
totalEpisodes: episodes.length, totalEpisodes: episodes.length,
pendingRequests: feedRequests.filter(r => r.status === 'pending').length,
totalRequests: feedRequests.length,
lastUpdated: new Date().toISOString(), lastUpdated: new Date().toISOString(),
adminPort: config.admin.port, adminPort: config.admin.port,
authEnabled: !!(config.admin.username && config.admin.password), authEnabled: !!(config.admin.username && config.admin.password),

View File

@ -23,7 +23,7 @@ function App() {
className={`tab ${activeTab === 'feeds' ? 'active' : ''}`} className={`tab ${activeTab === 'feeds' ? 'active' : ''}`}
onClick={() => setActiveTab('feeds')} onClick={() => setActiveTab('feeds')}
> >
</button> </button>
</div> </div>

View File

@ -1,141 +1,61 @@
import { useState, useEffect } from 'react' import { useState } from 'react'
interface Feed {
id: string
url: string
title?: string
description?: string
active: boolean
lastUpdated?: string
}
function FeedManager() { function FeedManager() {
const [feeds, setFeeds] = useState<Feed[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null) const [success, setSuccess] = useState<string | null>(null)
const [newFeedUrl, setNewFeedUrl] = useState('') const [newFeedUrl, setNewFeedUrl] = useState('')
const [adding, setAdding] = useState(false) const [requestMessage, setRequestMessage] = useState('')
const [requesting, setRequesting] = useState(false)
useEffect(() => { const submitRequest = async (e: React.FormEvent) => {
fetchFeeds()
}, [])
const fetchFeeds = async () => {
try {
setLoading(true)
const response = await fetch('/api/feeds')
if (!response.ok) throw new Error('フィードの取得に失敗しました')
const data = await response.json()
setFeeds(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'エラーが発生しました')
} finally {
setLoading(false)
}
}
const addFeed = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!newFeedUrl.trim()) return if (!newFeedUrl.trim()) return
try { try {
setAdding(true) setRequesting(true)
setError(null) setError(null)
setSuccess(null) setSuccess(null)
const response = await fetch('/api/feeds', { const response = await fetch('/api/feed-requests', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ url: newFeedUrl.trim() }), body: JSON.stringify({
url: newFeedUrl.trim(),
requestMessage: requestMessage.trim() || undefined
}),
}) })
if (!response.ok) { if (!response.ok) {
const errorData = await response.json() const errorData = await response.json()
throw new Error(errorData.error || 'フィードの追加に失敗しました') throw new Error(errorData.error || 'リクエストの送信に失敗しました')
} }
setSuccess('フィードを追加しました') setSuccess('フィードリクエストを送信しました。管理者の承認をお待ちください。')
setNewFeedUrl('') setNewFeedUrl('')
await fetchFeeds() setRequestMessage('')
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'エラーが発生しました') setError(err instanceof Error ? err.message : 'エラーが発生しました')
} finally { } finally {
setAdding(false) setRequesting(false)
} }
} }
const deleteFeed = async (feedId: string) => {
if (!confirm('このフィードを削除しますか?関連するエピソードも削除されます。')) {
return
}
try {
setError(null)
setSuccess(null)
const response = await fetch(`/api/feeds/${feedId}`, {
method: 'DELETE',
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'フィードの削除に失敗しました')
}
setSuccess('フィードを削除しました')
await fetchFeeds()
} catch (err) {
setError(err instanceof Error ? err.message : 'エラーが発生しました')
}
}
const toggleFeed = async (feedId: string, active: boolean) => {
try {
setError(null)
setSuccess(null)
const response = await fetch(`/api/feeds/${feedId}/toggle`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ active }),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'フィードの状態変更に失敗しました')
}
setSuccess(`フィードを${active ? '有効' : '無効'}にしました`)
await fetchFeeds()
} catch (err) {
setError(err instanceof Error ? err.message : 'エラーが発生しました')
}
}
const formatDate = (dateString?: string) => {
if (!dateString) return '未更新'
return new Date(dateString).toLocaleString('ja-JP')
}
if (loading) {
return <div className="loading">...</div>
}
return ( return (
<div> <div>
{error && <div className="error">{error}</div>} {error && <div className="error">{error}</div>}
{success && <div className="success">{success}</div>} {success && <div className="success">{success}</div>}
<div style={{ marginBottom: '30px' }}> <div style={{ marginBottom: '30px' }}>
<h2></h2> <h2></h2>
<form onSubmit={addFeed}> <p style={{ color: '#666', marginBottom: '20px' }}>
RSSフィードのURLを送信してください
</p>
<form onSubmit={submitRequest}>
<div className="form-group"> <div className="form-group">
<label className="form-label">RSS URL</label> <label className="form-label">RSS URL *</label>
<input <input
type="url" type="url"
className="form-input" className="form-input"
@ -145,66 +65,37 @@ function FeedManager() {
required required
/> />
</div> </div>
<div className="form-group">
<label className="form-label"></label>
<textarea
className="form-input"
value={requestMessage}
onChange={(e) => setRequestMessage(e.target.value)}
placeholder="このフィードについての説明や追加理由があれば記載してください"
rows={3}
style={{ resize: 'vertical', minHeight: '80px' }}
/>
</div>
<button <button
type="submit" type="submit"
className="btn btn-primary" className="btn btn-primary"
disabled={adding} disabled={requesting}
> >
{adding ? '追加中...' : 'フィードを追加'} {requesting ? 'リクエスト送信中...' : 'フィードをリクエスト'}
</button> </button>
</form> </form>
</div> </div>
<div> <div style={{ backgroundColor: '#f8f9fa', padding: '20px', borderRadius: '8px' }}>
<h2> ({feeds.length})</h2> <h3 style={{ marginBottom: '15px' }}></h3>
<ul style={{ paddingLeft: '20px', color: '#666' }}>
{feeds.length === 0 ? ( <li></li>
<div className="empty-state"> <li>RSSフィードと判断された場合</li>
<p></p> <li></li>
</div> <li></li>
) : ( </ul>
<div>
{feeds.map((feed) => (
<div key={feed.id} className="feed-item">
<div className="feed-info">
<div className="feed-title">
{feed.title || 'タイトル不明'}
{!feed.active && (
<span style={{
marginLeft: '8px',
padding: '2px 6px',
backgroundColor: '#dc3545',
color: 'white',
fontSize: '12px',
borderRadius: '3px'
}}>
</span>
)}
</div>
<div className="feed-url">{feed.url}</div>
<div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
: {formatDate(feed.lastUpdated)}
</div>
</div>
<div className="feed-actions">
<button
className={`btn ${feed.active ? 'btn-secondary' : 'btn-primary'}`}
onClick={() => toggleFeed(feed.id, !feed.active)}
>
{feed.active ? '無効化' : '有効化'}
</button>
<button
className="btn btn-danger"
onClick={() => deleteFeed(feed.id)}
>
</button>
</div>
</div>
))}
</div>
)}
</div> </div>
</div> </div>
) )

View File

@ -53,6 +53,19 @@ CREATE TABLE IF NOT EXISTS tts_queue (
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed')) status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed'))
); );
-- Feed requests from users
CREATE TABLE IF NOT EXISTS feed_requests (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
requested_by TEXT,
request_message TEXT,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
created_at TEXT NOT NULL,
reviewed_at TEXT,
reviewed_by TEXT,
admin_notes TEXT
);
-- Create indexes for better performance -- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id); CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date); CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
@ -61,3 +74,5 @@ CREATE INDEX IF NOT EXISTS idx_episodes_article_id ON episodes(article_id);
CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active); CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active);
CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status); CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at); CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);
CREATE INDEX IF NOT EXISTS idx_feed_requests_status ON feed_requests(status);
CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);

View File

@ -119,73 +119,29 @@ app.get("/api/episodes", async (c) => {
} }
}); });
app.get("/api/feeds", async (c) => { app.post("/api/feed-requests", async (c) => {
try {
const { getAllFeedsIncludingInactive } = await import("./services/database.js");
const feeds = await getAllFeedsIncludingInactive();
return c.json(feeds);
} catch (error) {
console.error("Error fetching feeds:", error);
return c.json({ error: "Failed to fetch feeds" }, 500);
}
});
app.post("/api/feeds", async (c) => {
try { try {
const body = await c.req.json(); const body = await c.req.json();
const { url } = body; const { url, requestMessage } = body;
if (!url || typeof url !== 'string') { if (!url || typeof url !== 'string') {
return c.json({ error: "URL is required" }, 400); return c.json({ error: "URL is required" }, 400);
} }
const { addNewFeedUrl } = await import("./scripts/fetch_and_generate.js"); const { submitFeedRequest } = await import("./services/database.js");
await addNewFeedUrl(url); const requestId = await submitFeedRequest({
return c.json({ success: true, message: "Feed added successfully" }); url,
requestMessage,
});
return c.json({
success: true,
message: "Feed request submitted successfully",
requestId
});
} catch (error) { } catch (error) {
console.error("Error adding feed:", error); console.error("Error submitting feed request:", error);
return c.json({ error: "Failed to add feed" }, 500); return c.json({ error: "Failed to submit feed request" }, 500);
}
});
app.delete("/api/feeds/:id", async (c) => {
try {
const feedId = c.req.param("id");
const { deleteFeed } = await import("./services/database.js");
const success = await deleteFeed(feedId);
if (!success) {
return c.json({ error: "Feed not found" }, 404);
}
return c.json({ success: true, message: "Feed deleted successfully" });
} catch (error) {
console.error("Error deleting feed:", error);
return c.json({ error: "Failed to delete feed" }, 500);
}
});
app.patch("/api/feeds/:id/toggle", async (c) => {
try {
const feedId = c.req.param("id");
const body = await c.req.json();
const { active } = body;
if (typeof active !== 'boolean') {
return c.json({ error: "Active status must be boolean" }, 400);
}
const { toggleFeedActive } = await import("./services/database.js");
const success = await toggleFeedActive(feedId, active);
if (!success) {
return c.json({ error: "Feed not found" }, 404);
}
return c.json({ success: true, message: "Feed status updated successfully" });
} catch (error) {
console.error("Error toggling feed:", error);
return c.json({ error: "Failed to update feed status" }, 500);
} }
}); });

View File

@ -17,6 +17,12 @@ function initializeDatabase(): Database {
const db = new Database(config.paths.dbPath); const db = new Database(config.paths.dbPath);
// Enable WAL mode for better concurrent access
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA synchronous = NORMAL;");
db.exec("PRAGMA cache_size = 1000;");
db.exec("PRAGMA temp_store = memory;");
// Ensure schema is set up - use the complete schema // Ensure schema is set up - use the complete schema
db.exec(`CREATE TABLE IF NOT EXISTS feeds ( db.exec(`CREATE TABLE IF NOT EXISTS feeds (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -70,13 +76,27 @@ function initializeDatabase(): Database {
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed')) status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'failed'))
); );
CREATE TABLE IF NOT EXISTS feed_requests (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
requested_by TEXT,
request_message TEXT,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
created_at TEXT NOT NULL,
reviewed_at TEXT,
reviewed_by TEXT,
admin_notes TEXT
);
CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id); CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id);
CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date); CREATE INDEX IF NOT EXISTS idx_articles_pub_date ON articles(pub_date);
CREATE INDEX IF NOT EXISTS idx_articles_processed ON articles(processed); CREATE INDEX IF NOT EXISTS idx_articles_processed ON articles(processed);
CREATE INDEX IF NOT EXISTS idx_episodes_article_id ON episodes(article_id); CREATE INDEX IF NOT EXISTS idx_episodes_article_id ON episodes(article_id);
CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active); CREATE INDEX IF NOT EXISTS idx_feeds_active ON feeds(active);
CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status); CREATE INDEX IF NOT EXISTS idx_tts_queue_status ON tts_queue(status);
CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);`); CREATE INDEX IF NOT EXISTS idx_tts_queue_created_at ON tts_queue(created_at);
CREATE INDEX IF NOT EXISTS idx_feed_requests_status ON feed_requests(status);
CREATE INDEX IF NOT EXISTS idx_feed_requests_created_at ON feed_requests(created_at);`);
return db; return db;
} }
@ -601,6 +621,83 @@ export async function removeFromQueue(queueId: string): Promise<void> {
} }
} }
// Feed Request management functions
export interface FeedRequest {
id: string;
url: string;
requestedBy?: string;
requestMessage?: string;
status: 'pending' | 'approved' | 'rejected';
createdAt: string;
reviewedAt?: string;
reviewedBy?: string;
adminNotes?: string;
}
export async function submitFeedRequest(
request: Omit<FeedRequest, "id" | "createdAt" | "status">
): Promise<string> {
const id = crypto.randomUUID();
const createdAt = new Date().toISOString();
try {
const stmt = db.prepare(
"INSERT INTO feed_requests (id, url, requested_by, request_message, status, created_at) VALUES (?, ?, ?, ?, 'pending', ?)",
);
stmt.run(id, request.url, request.requestedBy || null, request.requestMessage || null, createdAt);
console.log(`Feed request submitted: ${request.url}`);
return id;
} catch (error) {
console.error("Error submitting feed request:", error);
throw error;
}
}
export async function getFeedRequests(status?: string): Promise<FeedRequest[]> {
try {
const sql = status
? "SELECT * FROM feed_requests WHERE status = ? ORDER BY created_at DESC"
: "SELECT * FROM feed_requests ORDER BY created_at DESC";
const stmt = db.prepare(sql);
const rows = status ? stmt.all(status) : stmt.all();
return (rows as any[]).map((row) => ({
id: row.id,
url: row.url,
requestedBy: row.requested_by,
requestMessage: row.request_message,
status: row.status,
createdAt: row.created_at,
reviewedAt: row.reviewed_at,
reviewedBy: row.reviewed_by,
adminNotes: row.admin_notes,
}));
} catch (error) {
console.error("Error getting feed requests:", error);
throw error;
}
}
export async function updateFeedRequestStatus(
requestId: string,
status: 'approved' | 'rejected',
reviewedBy?: string,
adminNotes?: string,
): Promise<boolean> {
try {
const reviewedAt = new Date().toISOString();
const stmt = db.prepare(
"UPDATE feed_requests SET status = ?, reviewed_at = ?, reviewed_by = ?, admin_notes = ? WHERE id = ?",
);
const result = stmt.run(status, reviewedAt, reviewedBy || null, adminNotes || null, requestId);
return result.changes > 0;
} catch (error) {
console.error("Error updating feed request status:", error);
throw error;
}
}
export function closeDatabase(): void { export function closeDatabase(): void {
db.close(); db.close();
} }