Add grouping categories feature
This commit is contained in:
@ -10,10 +10,12 @@ import {
|
|||||||
deleteFeed,
|
deleteFeed,
|
||||||
fetchAllEpisodes,
|
fetchAllEpisodes,
|
||||||
fetchEpisodesWithArticles,
|
fetchEpisodesWithArticles,
|
||||||
|
getAllCategories,
|
||||||
getAllFeedsIncludingInactive,
|
getAllFeedsIncludingInactive,
|
||||||
getFeedById,
|
|
||||||
getFeedByUrl,
|
getFeedByUrl,
|
||||||
getFeedRequests,
|
getFeedRequests,
|
||||||
|
getFeedsByCategory,
|
||||||
|
getFeedsGroupedByCategory,
|
||||||
toggleFeedActive,
|
toggleFeedActive,
|
||||||
updateFeedRequestStatus,
|
updateFeedRequestStatus,
|
||||||
} from "./services/database.js";
|
} from "./services/database.js";
|
||||||
@ -199,6 +201,38 @@ app.patch("/api/admin/feeds/:id/toggle", async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Categories management
|
||||||
|
app.get("/api/admin/categories", async (c) => {
|
||||||
|
try {
|
||||||
|
const categories = await getAllCategories();
|
||||||
|
return c.json(categories);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching categories:", error);
|
||||||
|
return c.json({ error: "Failed to fetch categories" }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/admin/feeds/by-category", async (c) => {
|
||||||
|
try {
|
||||||
|
const category = c.req.query("category");
|
||||||
|
const feeds = await getFeedsByCategory(category);
|
||||||
|
return c.json(feeds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching feeds by category:", error);
|
||||||
|
return c.json({ error: "Failed to fetch feeds by category" }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/admin/feeds/grouped-by-category", async (c) => {
|
||||||
|
try {
|
||||||
|
const groupedFeeds = await getFeedsGroupedByCategory();
|
||||||
|
return c.json(groupedFeeds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching feeds grouped by category:", error);
|
||||||
|
return c.json({ error: "Failed to fetch feeds grouped by category" }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Episodes management
|
// Episodes management
|
||||||
app.get("/api/admin/episodes", async (c) => {
|
app.get("/api/admin/episodes", async (c) => {
|
||||||
try {
|
try {
|
||||||
@ -349,7 +383,10 @@ app.get("/api/admin/db-diagnostic", async (c) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error running database diagnostic:", error);
|
console.error("Error running database diagnostic:", error);
|
||||||
return c.json(
|
return c.json(
|
||||||
{ error: "Failed to run database diagnostic", details: error.message },
|
{
|
||||||
|
error: "Failed to run database diagnostic",
|
||||||
|
details: error instanceof Error ? error.message : String(error)
|
||||||
|
},
|
||||||
500,
|
500,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -635,16 +672,6 @@ app.get("/", serveAdminIndex);
|
|||||||
app.get("/index.html", serveAdminIndex);
|
app.get("/index.html", serveAdminIndex);
|
||||||
app.get("*", serveAdminIndex);
|
app.get("*", serveAdminIndex);
|
||||||
|
|
||||||
// Utility functions
|
|
||||||
async function runBatchProcess(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await batchProcess();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Admin batch process failed:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start admin server
|
// Start admin server
|
||||||
serve(
|
serve(
|
||||||
{
|
{
|
||||||
|
34
server.ts
34
server.ts
@ -449,6 +449,40 @@ app.get("/api/feeds/:feedId/episodes", async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/api/categories", async (c) => {
|
||||||
|
try {
|
||||||
|
const { getAllCategories } = await import("./services/database.js");
|
||||||
|
const categories = await getAllCategories();
|
||||||
|
return c.json({ categories });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching categories:", error);
|
||||||
|
return c.json({ error: "Failed to fetch categories" }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/feeds/by-category", async (c) => {
|
||||||
|
try {
|
||||||
|
const category = c.req.query("category");
|
||||||
|
const { getFeedsByCategory } = await import("./services/database.js");
|
||||||
|
const feeds = await getFeedsByCategory(category);
|
||||||
|
return c.json({ feeds });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching feeds by category:", error);
|
||||||
|
return c.json({ error: "Failed to fetch feeds by category" }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/feeds/grouped-by-category", async (c) => {
|
||||||
|
try {
|
||||||
|
const { getFeedsGroupedByCategory } = await import("./services/database.js");
|
||||||
|
const groupedFeeds = await getFeedsGroupedByCategory();
|
||||||
|
return c.json({ groupedFeeds });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching feeds grouped by category:", error);
|
||||||
|
return c.json({ error: "Failed to fetch feeds grouped by category" }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/episode-with-source/:episodeId", async (c) => {
|
app.get("/api/episode-with-source/:episodeId", async (c) => {
|
||||||
try {
|
try {
|
||||||
const episodeId = c.req.param("episodeId");
|
const episodeId = c.req.param("episodeId");
|
||||||
|
@ -213,6 +213,7 @@ export interface Feed {
|
|||||||
url: string;
|
url: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
category?: string;
|
||||||
lastUpdated?: string;
|
lastUpdated?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@ -325,6 +326,7 @@ export async function getFeedByUrl(url: string): Promise<Feed | null> {
|
|||||||
url: row.url,
|
url: row.url,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
category: row.category,
|
||||||
lastUpdated: row.last_updated,
|
lastUpdated: row.last_updated,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
active: Boolean(row.active),
|
active: Boolean(row.active),
|
||||||
@ -346,6 +348,7 @@ export async function getFeedById(id: string): Promise<Feed | null> {
|
|||||||
url: row.url,
|
url: row.url,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
category: row.category,
|
||||||
lastUpdated: row.last_updated,
|
lastUpdated: row.last_updated,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
active: Boolean(row.active),
|
active: Boolean(row.active),
|
||||||
@ -368,6 +371,7 @@ export async function getAllFeeds(): Promise<Feed[]> {
|
|||||||
url: row.url,
|
url: row.url,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
category: row.category,
|
||||||
lastUpdated: row.last_updated,
|
lastUpdated: row.last_updated,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
active: Boolean(row.active),
|
active: Boolean(row.active),
|
||||||
@ -549,6 +553,7 @@ export async function getAllFeedsIncludingInactive(): Promise<Feed[]> {
|
|||||||
url: row.url,
|
url: row.url,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
category: row.category,
|
||||||
lastUpdated: row.last_updated,
|
lastUpdated: row.last_updated,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
active: Boolean(row.active),
|
active: Boolean(row.active),
|
||||||
@ -559,6 +564,68 @@ export async function getAllFeedsIncludingInactive(): Promise<Feed[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFeedsByCategory(category?: string): Promise<Feed[]> {
|
||||||
|
try {
|
||||||
|
let stmt;
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
stmt = db.prepare("SELECT * FROM feeds WHERE category = ? AND active = 1 ORDER BY created_at DESC");
|
||||||
|
rows = stmt.all(category) as any[];
|
||||||
|
} else {
|
||||||
|
// If no category specified, return all active feeds
|
||||||
|
stmt = db.prepare("SELECT * FROM feeds WHERE active = 1 ORDER BY created_at DESC");
|
||||||
|
rows = stmt.all() as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
url: row.url,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description,
|
||||||
|
category: row.category,
|
||||||
|
lastUpdated: row.last_updated,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
active: Boolean(row.active),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting feeds by category:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllCategories(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const stmt = db.prepare("SELECT DISTINCT category FROM feeds WHERE category IS NOT NULL AND active = 1 ORDER BY category");
|
||||||
|
const rows = stmt.all() as any[];
|
||||||
|
|
||||||
|
return rows.map((row) => row.category).filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting all categories:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFeedsGroupedByCategory(): Promise<{ [category: string]: Feed[] }> {
|
||||||
|
try {
|
||||||
|
const feeds = await getAllFeeds();
|
||||||
|
const grouped: { [category: string]: Feed[] } = {};
|
||||||
|
|
||||||
|
for (const feed of feeds) {
|
||||||
|
const category = feed.category || 'Uncategorized';
|
||||||
|
if (!grouped[category]) {
|
||||||
|
grouped[category] = [];
|
||||||
|
}
|
||||||
|
grouped[category].push(feed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting feeds grouped by category:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteFeed(feedId: string): Promise<boolean> {
|
export async function deleteFeed(feedId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Start transaction
|
// Start transaction
|
||||||
|
Reference in New Issue
Block a user