Update
This commit is contained in:
109
admin-server.ts
109
admin-server.ts
@ -14,6 +14,7 @@ import {
|
||||
getFeedRequests,
|
||||
updateFeedRequestStatus,
|
||||
} 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";
|
||||
|
||||
@ -204,6 +205,114 @@ app.get("/api/admin/episodes/simple", async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Database diagnostic endpoint
|
||||
app.get("/api/admin/db-diagnostic", async (c) => {
|
||||
try {
|
||||
const db = new Database(config.paths.dbPath);
|
||||
|
||||
// 1. Check episodes table
|
||||
const episodeCount = db.prepare("SELECT COUNT(*) as count FROM episodes").get() as any;
|
||||
|
||||
// 2. Check articles table
|
||||
const articleCount = db.prepare("SELECT COUNT(*) as count FROM articles").get() as any;
|
||||
|
||||
// 3. Check feeds table
|
||||
const feedCount = db.prepare("SELECT COUNT(*) as count FROM feeds").get() as any;
|
||||
const activeFeedCount = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 1").get() as any;
|
||||
const inactiveFeedCount = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 0 OR active IS NULL").get() as any;
|
||||
|
||||
// 4. Check orphaned episodes
|
||||
const orphanedEpisodes = db.prepare(`
|
||||
SELECT e.id, e.title, e.article_id
|
||||
FROM episodes e
|
||||
LEFT JOIN articles a ON e.article_id = a.id
|
||||
WHERE a.id IS NULL
|
||||
`).all() as any[];
|
||||
|
||||
// 5. Check orphaned articles
|
||||
const orphanedArticles = db.prepare(`
|
||||
SELECT a.id, a.title, a.feed_id
|
||||
FROM articles a
|
||||
LEFT JOIN feeds f ON a.feed_id = f.id
|
||||
WHERE f.id IS NULL
|
||||
`).all() as any[];
|
||||
|
||||
// 6. Check episodes with articles but feeds are inactive
|
||||
const episodesInactiveFeeds = db.prepare(`
|
||||
SELECT e.id, e.title, f.active, f.title as feed_title
|
||||
FROM episodes e
|
||||
JOIN articles a ON e.article_id = a.id
|
||||
JOIN feeds f ON a.feed_id = f.id
|
||||
WHERE f.active = 0 OR f.active IS NULL
|
||||
`).all() as any[];
|
||||
|
||||
// 7. Test the JOIN query
|
||||
const joinResult = db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM episodes e
|
||||
JOIN articles a ON e.article_id = a.id
|
||||
JOIN feeds f ON a.feed_id = f.id
|
||||
WHERE f.active = 1
|
||||
`).get() as any;
|
||||
|
||||
// 8. Sample feed details
|
||||
const sampleFeeds = db.prepare(`
|
||||
SELECT id, title, url, active, created_at
|
||||
FROM feeds
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
`).all() as any[];
|
||||
|
||||
// 9. Sample episode-article-feed chain
|
||||
const sampleChain = db.prepare(`
|
||||
SELECT
|
||||
e.id as episode_id, e.title as episode_title,
|
||||
a.id as article_id, a.title as article_title,
|
||||
f.id as feed_id, f.title as feed_title, f.active
|
||||
FROM episodes e
|
||||
LEFT JOIN articles a ON e.article_id = a.id
|
||||
LEFT JOIN feeds f ON a.feed_id = f.id
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 5
|
||||
`).all() as any[];
|
||||
|
||||
db.close();
|
||||
|
||||
const diagnosticResult = {
|
||||
counts: {
|
||||
episodes: episodeCount.count,
|
||||
articles: articleCount.count,
|
||||
feeds: feedCount.count,
|
||||
activeFeeds: activeFeedCount.count,
|
||||
inactiveFeeds: inactiveFeedCount.count,
|
||||
},
|
||||
orphaned: {
|
||||
episodes: orphanedEpisodes.length,
|
||||
episodeDetails: orphanedEpisodes.slice(0, 3),
|
||||
articles: orphanedArticles.length,
|
||||
articleDetails: orphanedArticles.slice(0, 3),
|
||||
},
|
||||
episodesFromInactiveFeeds: {
|
||||
count: episodesInactiveFeeds.length,
|
||||
details: episodesInactiveFeeds.slice(0, 3),
|
||||
},
|
||||
joinQuery: {
|
||||
episodesWithActiveFeeds: joinResult.count,
|
||||
},
|
||||
samples: {
|
||||
feeds: sampleFeeds,
|
||||
episodeChain: sampleChain,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return c.json(diagnosticResult);
|
||||
} catch (error) {
|
||||
console.error("Error running database diagnostic:", error);
|
||||
return c.json({ error: "Failed to run database diagnostic", details: error.message }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Feed requests management
|
||||
app.get("/api/admin/feed-requests", async (c) => {
|
||||
try {
|
||||
|
@ -4,59 +4,88 @@ import crypto from "crypto";
|
||||
import { config } from "./config.js";
|
||||
|
||||
// Database integrity fixes function
|
||||
function performDatabaseIntegrityFixes(db: Database): void {
|
||||
export function performDatabaseIntegrityFixes(db: Database): void {
|
||||
console.log("🔧 Performing database integrity checks...");
|
||||
|
||||
try {
|
||||
// Fix 1: Set active flag to 1 for feeds where it's NULL
|
||||
const nullActiveFeeds = db.prepare("UPDATE feeds SET active = 1 WHERE active IS NULL").run();
|
||||
const nullActiveFeeds = db
|
||||
.prepare("UPDATE feeds SET active = 1 WHERE active IS NULL")
|
||||
.run();
|
||||
if (nullActiveFeeds.changes > 0) {
|
||||
console.log(`✅ Fixed ${nullActiveFeeds.changes} feeds with NULL active flag`);
|
||||
console.log(
|
||||
`✅ Fixed ${nullActiveFeeds.changes} feeds with NULL active flag`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fix 2: Fix orphaned articles (articles referencing non-existent feeds)
|
||||
const orphanedArticles = db.prepare(`
|
||||
const orphanedArticles = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT a.id, a.link, a.title
|
||||
FROM articles a
|
||||
LEFT JOIN feeds f ON a.feed_id = f.id
|
||||
WHERE f.id IS NULL
|
||||
`).all() as any[];
|
||||
`,
|
||||
)
|
||||
.all() as any[];
|
||||
|
||||
if (orphanedArticles.length > 0) {
|
||||
console.log(`🔍 Found ${orphanedArticles.length} orphaned articles, attempting to fix...`);
|
||||
console.log(
|
||||
`🔍 Found ${orphanedArticles.length} orphaned articles, attempting to fix...`,
|
||||
);
|
||||
|
||||
for (const article of orphanedArticles) {
|
||||
// Try to match article to feed based on URL domain
|
||||
const articleDomain = extractDomain(article.link);
|
||||
if (articleDomain) {
|
||||
const matchingFeed = db.prepare(`
|
||||
const matchingFeed = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id FROM feeds
|
||||
WHERE url LIKE ? OR url LIKE ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`).get(`%${articleDomain}%`, `%${articleDomain.replace('www.', '')}%`) as any;
|
||||
`,
|
||||
)
|
||||
.get(
|
||||
`%${articleDomain}%`,
|
||||
`%${articleDomain.replace("www.", "")}%`,
|
||||
) as any;
|
||||
|
||||
if (matchingFeed) {
|
||||
db.prepare("UPDATE articles SET feed_id = ? WHERE id = ?")
|
||||
.run(matchingFeed.id, article.id);
|
||||
console.log(`✅ Fixed article "${article.title}" -> feed ${matchingFeed.id}`);
|
||||
db.prepare("UPDATE articles SET feed_id = ? WHERE id = ?").run(
|
||||
matchingFeed.id,
|
||||
article.id,
|
||||
);
|
||||
console.log(
|
||||
`✅ Fixed article "${article.title}" -> feed ${matchingFeed.id}`,
|
||||
);
|
||||
} else {
|
||||
console.log(`⚠️ Could not find matching feed for article: ${article.title} (${articleDomain})`);
|
||||
console.log(
|
||||
`⚠️ Could not find matching feed for article: ${article.title} (${articleDomain})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix 3: Ensure all episodes have valid article references
|
||||
const orphanedEpisodes = db.prepare(`
|
||||
const orphanedEpisodes = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT e.id, e.title, e.article_id
|
||||
FROM episodes e
|
||||
LEFT JOIN articles a ON e.article_id = a.id
|
||||
WHERE a.id IS NULL
|
||||
`).all() as any[];
|
||||
`,
|
||||
)
|
||||
.all() as any[];
|
||||
|
||||
if (orphanedEpisodes.length > 0) {
|
||||
console.log(`⚠️ Found ${orphanedEpisodes.length} episodes with invalid article references`);
|
||||
console.log(
|
||||
`⚠️ Found ${orphanedEpisodes.length} episodes with invalid article references`,
|
||||
);
|
||||
// We could delete these or try to fix them, but for now just log
|
||||
}
|
||||
|
||||
@ -259,6 +288,17 @@ export async function saveFeed(
|
||||
createdAt,
|
||||
feed.active !== undefined ? (feed.active ? 1 : 0) : 1, // Default to active=1 if not specified
|
||||
);
|
||||
|
||||
try {
|
||||
performDatabaseIntegrityFixes(db);
|
||||
console.log(`Feed saved: ${feed.url}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error performing integrity fixes after saving feed:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error("Error saving feed:", error);
|
||||
@ -336,7 +376,9 @@ export async function fetchActiveFeeds(): Promise<Feed[]> {
|
||||
}
|
||||
|
||||
// Get episodes with feed information for enhanced display
|
||||
export async function fetchEpisodesWithFeedInfo(): Promise<EpisodeWithFeedInfo[]> {
|
||||
export async function fetchEpisodesWithFeedInfo(): Promise<
|
||||
EpisodeWithFeedInfo[]
|
||||
> {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
@ -386,7 +428,9 @@ export async function fetchEpisodesWithFeedInfo(): Promise<EpisodeWithFeedInfo[]
|
||||
}
|
||||
|
||||
// Get episodes by feed ID
|
||||
export async function fetchEpisodesByFeedId(feedId: string): Promise<EpisodeWithFeedInfo[]> {
|
||||
export async function fetchEpisodesByFeedId(
|
||||
feedId: string,
|
||||
): Promise<EpisodeWithFeedInfo[]> {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
@ -436,7 +480,9 @@ export async function fetchEpisodesByFeedId(feedId: string): Promise<EpisodeWith
|
||||
}
|
||||
|
||||
// Get single episode with source information
|
||||
export async function fetchEpisodeWithSourceInfo(episodeId: string): Promise<EpisodeWithFeedInfo | null> {
|
||||
export async function fetchEpisodeWithSourceInfo(
|
||||
episodeId: string,
|
||||
): Promise<EpisodeWithFeedInfo | null> {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
@ -487,9 +533,7 @@ export async function fetchEpisodeWithSourceInfo(episodeId: string): Promise<Epi
|
||||
|
||||
export async function getAllFeedsIncludingInactive(): Promise<Feed[]> {
|
||||
try {
|
||||
const stmt = db.prepare(
|
||||
"SELECT * FROM feeds ORDER BY created_at DESC",
|
||||
);
|
||||
const stmt = db.prepare("SELECT * FROM feeds ORDER BY created_at DESC");
|
||||
const rows = stmt.all() as any[];
|
||||
|
||||
return rows.map((row) => ({
|
||||
@ -522,7 +566,9 @@ export async function deleteFeed(feedId: string): Promise<boolean> {
|
||||
deleteEpisodesStmt.run(feedId);
|
||||
|
||||
// Delete all articles for this feed
|
||||
const deleteArticlesStmt = db.prepare("DELETE FROM articles WHERE feed_id = ?");
|
||||
const deleteArticlesStmt = db.prepare(
|
||||
"DELETE FROM articles WHERE feed_id = ?",
|
||||
);
|
||||
deleteArticlesStmt.run(feedId);
|
||||
|
||||
// Delete the feed itself
|
||||
@ -539,7 +585,10 @@ export async function deleteFeed(feedId: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleFeedActive(feedId: string, active: boolean): Promise<boolean> {
|
||||
export async function toggleFeedActive(
|
||||
feedId: string,
|
||||
active: boolean,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const stmt = db.prepare("UPDATE feeds SET active = ? WHERE id = ?");
|
||||
const result = stmt.run(active ? 1 : 0, feedId);
|
||||
@ -816,7 +865,7 @@ export interface TTSQueueItem {
|
||||
retryCount: number;
|
||||
createdAt: string;
|
||||
lastAttemptedAt?: string;
|
||||
status: 'pending' | 'processing' | 'failed';
|
||||
status: "pending" | "processing" | "failed";
|
||||
}
|
||||
|
||||
export async function addToQueue(
|
||||
@ -840,7 +889,9 @@ export async function addToQueue(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQueueItems(limit: number = 10): Promise<TTSQueueItem[]> {
|
||||
export async function getQueueItems(
|
||||
limit: number = 10,
|
||||
): Promise<TTSQueueItem[]> {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM tts_queue
|
||||
@ -867,7 +918,7 @@ export async function getQueueItems(limit: number = 10): Promise<TTSQueueItem[]>
|
||||
|
||||
export async function updateQueueItemStatus(
|
||||
queueId: string,
|
||||
status: 'pending' | 'processing' | 'failed',
|
||||
status: "pending" | "processing" | "failed",
|
||||
lastAttemptedAt?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
@ -897,7 +948,7 @@ export interface FeedRequest {
|
||||
url: string;
|
||||
requestedBy?: string;
|
||||
requestMessage?: string;
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
status: "pending" | "approved" | "rejected";
|
||||
createdAt: string;
|
||||
reviewedAt?: string;
|
||||
reviewedBy?: string;
|
||||
@ -905,7 +956,7 @@ export interface FeedRequest {
|
||||
}
|
||||
|
||||
export async function submitFeedRequest(
|
||||
request: Omit<FeedRequest, "id" | "createdAt" | "status">
|
||||
request: Omit<FeedRequest, "id" | "createdAt" | "status">,
|
||||
): Promise<string> {
|
||||
const id = crypto.randomUUID();
|
||||
const createdAt = new Date().toISOString();
|
||||
@ -914,7 +965,13 @@ export async function submitFeedRequest(
|
||||
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);
|
||||
stmt.run(
|
||||
id,
|
||||
request.url,
|
||||
request.requestedBy || null,
|
||||
request.requestMessage || null,
|
||||
createdAt,
|
||||
);
|
||||
console.log(`Feed request submitted: ${request.url}`);
|
||||
return id;
|
||||
} catch (error) {
|
||||
@ -951,7 +1008,7 @@ export async function getFeedRequests(status?: string): Promise<FeedRequest[]> {
|
||||
|
||||
export async function updateFeedRequestStatus(
|
||||
requestId: string,
|
||||
status: 'approved' | 'rejected',
|
||||
status: "approved" | "rejected",
|
||||
reviewedBy?: string,
|
||||
adminNotes?: string,
|
||||
): Promise<boolean> {
|
||||
@ -960,7 +1017,13 @@ export async function updateFeedRequestStatus(
|
||||
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);
|
||||
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);
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { promises as fs } from "fs";
|
||||
import { dirname } from "path";
|
||||
import { Episode, fetchAllEpisodes } from "./database.js";
|
||||
import {
|
||||
Episode,
|
||||
fetchAllEpisodes,
|
||||
performDatabaseIntegrityFixes,
|
||||
} from "./database.js";
|
||||
import path from "node:path";
|
||||
import fsSync from "node:fs";
|
||||
import { config } from "./config.js";
|
||||
@ -20,7 +24,10 @@ function createItemXml(episode: Episode): string {
|
||||
|
||||
let fileSize = 0;
|
||||
try {
|
||||
const audioPath = path.join(config.paths.podcastAudioDir, episode.audioPath);
|
||||
const audioPath = path.join(
|
||||
config.paths.podcastAudioDir,
|
||||
episode.audioPath,
|
||||
);
|
||||
if (fsSync.existsSync(audioPath)) {
|
||||
fileSize = fsSync.statSync(audioPath).size;
|
||||
}
|
||||
@ -47,9 +54,12 @@ export async function updatePodcastRSS(): Promise<void> {
|
||||
const episodes: Episode[] = await fetchAllEpisodes();
|
||||
|
||||
// Filter episodes to only include those with valid audio files
|
||||
const validEpisodes = episodes.filter(episode => {
|
||||
const validEpisodes = episodes.filter((episode) => {
|
||||
try {
|
||||
const audioPath = path.join(config.paths.podcastAudioDir, episode.audioPath);
|
||||
const audioPath = path.join(
|
||||
config.paths.podcastAudioDir,
|
||||
episode.audioPath,
|
||||
);
|
||||
return fsSync.existsSync(audioPath);
|
||||
} catch (error) {
|
||||
console.warn(`Audio file not found for episode: ${episode.title}`);
|
||||
@ -57,7 +67,9 @@ export async function updatePodcastRSS(): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Found ${episodes.length} episodes, ${validEpisodes.length} with valid audio files`);
|
||||
console.log(
|
||||
`Found ${episodes.length} episodes, ${validEpisodes.length} with valid audio files`,
|
||||
);
|
||||
|
||||
const lastBuildDate = new Date().toUTCString();
|
||||
const itemsXml = validEpisodes.map(createItemXml).join("\n");
|
||||
@ -82,7 +94,9 @@ export async function updatePodcastRSS(): Promise<void> {
|
||||
await fs.mkdir(dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, rssXml);
|
||||
|
||||
console.log(`RSS feed updated with ${validEpisodes.length} episodes (audio files verified)`);
|
||||
console.log(
|
||||
`RSS feed updated with ${validEpisodes.length} episodes (audio files verified)`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating podcast RSS:", error);
|
||||
throw error;
|
||||
|
Reference in New Issue
Block a user