Fix database conflict and update schema
This commit is contained in:
@ -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),
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
15
schema.sql
15
schema.sql
@ -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);
|
||||||
|
74
server.ts
74
server.ts
@ -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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -16,6 +16,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 (
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user