@rmdes/indiekit-endpoint-rss 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +108 -0
- package/lib/controllers/dashboard.js +106 -0
- package/lib/controllers/feeds.js +198 -0
- package/lib/controllers/items.js +95 -0
- package/lib/controllers/status.js +90 -0
- package/lib/rss-client.js +408 -0
- package/lib/sync.js +299 -0
- package/lib/utils.js +204 -0
- package/locales/en.json +50 -0
- package/package.json +54 -0
- package/views/rss.njk +498 -0
package/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
6
|
+
import { feedsController } from "./lib/controllers/feeds.js";
|
|
7
|
+
import { itemsController } from "./lib/controllers/items.js";
|
|
8
|
+
import { statusController } from "./lib/controllers/status.js";
|
|
9
|
+
import { startSync } from "./lib/sync.js";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
const protectedRouter = express.Router();
|
|
14
|
+
const publicRouter = express.Router();
|
|
15
|
+
|
|
16
|
+
const defaults = {
|
|
17
|
+
mountPath: "/rssapi",
|
|
18
|
+
syncInterval: 900_000, // 15 minutes
|
|
19
|
+
maxItemsPerFeed: 50,
|
|
20
|
+
fetchTimeout: 10_000,
|
|
21
|
+
maxConcurrentFetches: 3,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default class RssEndpoint {
|
|
25
|
+
name = "RSS feed reader endpoint";
|
|
26
|
+
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.options = { ...defaults, ...options };
|
|
29
|
+
this.mountPath = this.options.mountPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get localesDirectory() {
|
|
33
|
+
return path.join(__dirname, "locales");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get navigationItems() {
|
|
37
|
+
return {
|
|
38
|
+
href: this.options.mountPath,
|
|
39
|
+
text: "rss.title",
|
|
40
|
+
requiresDatabase: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get shortcutItems() {
|
|
45
|
+
return {
|
|
46
|
+
url: this.options.mountPath,
|
|
47
|
+
name: "rss.feeds",
|
|
48
|
+
iconName: "syndicate",
|
|
49
|
+
requiresDatabase: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Protected routes (require authentication)
|
|
55
|
+
* Admin dashboard and feed management
|
|
56
|
+
*/
|
|
57
|
+
get routes() {
|
|
58
|
+
// Dashboard
|
|
59
|
+
protectedRouter.get("/", dashboardController.get);
|
|
60
|
+
|
|
61
|
+
// Manual sync trigger
|
|
62
|
+
protectedRouter.post("/sync", dashboardController.sync);
|
|
63
|
+
|
|
64
|
+
return protectedRouter;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Public routes (no authentication required)
|
|
69
|
+
* JSON API endpoints for frontend
|
|
70
|
+
*/
|
|
71
|
+
get routesPublic() {
|
|
72
|
+
// Feeds API
|
|
73
|
+
publicRouter.get("/api/feeds", feedsController.list);
|
|
74
|
+
publicRouter.post("/api/feeds", express.json(), feedsController.add);
|
|
75
|
+
publicRouter.delete("/api/feeds/:id", feedsController.remove);
|
|
76
|
+
publicRouter.patch("/api/feeds/:id", express.json(), feedsController.toggle);
|
|
77
|
+
|
|
78
|
+
// Items API
|
|
79
|
+
publicRouter.get("/api/items", itemsController.list);
|
|
80
|
+
publicRouter.get("/api/items/:id", itemsController.get);
|
|
81
|
+
|
|
82
|
+
// Status API
|
|
83
|
+
publicRouter.get("/api/status", statusController.status);
|
|
84
|
+
publicRouter.post("/api/refresh", statusController.refresh);
|
|
85
|
+
|
|
86
|
+
return publicRouter;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
init(Indiekit) {
|
|
90
|
+
Indiekit.addEndpoint(this);
|
|
91
|
+
|
|
92
|
+
// Add MongoDB collections
|
|
93
|
+
Indiekit.addCollection("rssFeeds");
|
|
94
|
+
Indiekit.addCollection("rssItems");
|
|
95
|
+
|
|
96
|
+
// Store config in application for controller access
|
|
97
|
+
Indiekit.config.application.rssConfig = this.options;
|
|
98
|
+
Indiekit.config.application.rssEndpoint = this.mountPath;
|
|
99
|
+
|
|
100
|
+
// Store database getter for controller access
|
|
101
|
+
Indiekit.config.application.getRssDb = () => Indiekit.database;
|
|
102
|
+
|
|
103
|
+
// Start background sync if database is available
|
|
104
|
+
if (Indiekit.config.application.mongodbUrl) {
|
|
105
|
+
startSync(Indiekit, this.options);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { getSyncState, runSync } from "../sync.js";
|
|
2
|
+
import { formatFeed, formatItem } from "../utils.js";
|
|
3
|
+
|
|
4
|
+
export const dashboardController = {
|
|
5
|
+
/**
|
|
6
|
+
* Render admin dashboard
|
|
7
|
+
* GET /
|
|
8
|
+
*/
|
|
9
|
+
async get(request, response, next) {
|
|
10
|
+
try {
|
|
11
|
+
const { rssConfig, rssEndpoint } = request.app.locals.application;
|
|
12
|
+
|
|
13
|
+
if (!rssConfig) {
|
|
14
|
+
return response.status(500).render("rss", {
|
|
15
|
+
title: response.locals.__("rss.title"),
|
|
16
|
+
error: { message: response.locals.__("rss.error.noConfig") },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
21
|
+
if (!db) {
|
|
22
|
+
return response.render("rss", {
|
|
23
|
+
title: response.locals.__("rss.title"),
|
|
24
|
+
error: { message: response.locals.__("rss.error.noDatabase") },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const feedsCollection = db.collection("rssFeeds");
|
|
29
|
+
const itemsCollection = db.collection("rssItems");
|
|
30
|
+
|
|
31
|
+
// Get feeds and recent items
|
|
32
|
+
const [feeds, recentItems, totalItems] = await Promise.all([
|
|
33
|
+
feedsCollection.find({}).sort({ addedAt: -1 }).toArray(),
|
|
34
|
+
itemsCollection
|
|
35
|
+
.find({})
|
|
36
|
+
.sort({ pubDate: -1 })
|
|
37
|
+
.limit(10)
|
|
38
|
+
.toArray(),
|
|
39
|
+
itemsCollection.countDocuments({}),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const syncState = getSyncState();
|
|
43
|
+
|
|
44
|
+
response.render("rss", {
|
|
45
|
+
title: response.locals.__("rss.title"),
|
|
46
|
+
feeds: feeds.map(formatFeed),
|
|
47
|
+
recentItems: recentItems.map((item) => formatItem(item)),
|
|
48
|
+
totalFeeds: feeds.length,
|
|
49
|
+
totalItems,
|
|
50
|
+
syncState: {
|
|
51
|
+
syncing: syncState.syncing,
|
|
52
|
+
lastSync: syncState.lastSync?.toISOString(),
|
|
53
|
+
lastError: syncState.lastError,
|
|
54
|
+
},
|
|
55
|
+
publicUrl: rssEndpoint,
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
next(error);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Trigger manual sync
|
|
64
|
+
* POST /sync
|
|
65
|
+
*/
|
|
66
|
+
async sync(request, response) {
|
|
67
|
+
try {
|
|
68
|
+
const Indiekit = request.app.locals.indiekit;
|
|
69
|
+
const { rssConfig } = request.app.locals.application;
|
|
70
|
+
|
|
71
|
+
if (!Indiekit || !rssConfig) {
|
|
72
|
+
return response.status(500).json({
|
|
73
|
+
error: response.locals.__("rss.error.noConfig"),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const syncState = getSyncState();
|
|
78
|
+
if (syncState.syncing) {
|
|
79
|
+
return response.status(409).json({
|
|
80
|
+
error: "Sync already in progress",
|
|
81
|
+
syncing: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Start sync and wait for result
|
|
86
|
+
const result = await runSync(Indiekit, rssConfig);
|
|
87
|
+
|
|
88
|
+
if (result.error) {
|
|
89
|
+
return response.status(500).json({
|
|
90
|
+
success: false,
|
|
91
|
+
error: result.error,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
response.json({
|
|
96
|
+
success: true,
|
|
97
|
+
message: response.locals.__("rss.success.syncComplete"),
|
|
98
|
+
feedsProcessed: result.feedsProcessed,
|
|
99
|
+
itemsAdded: result.itemsAdded,
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("[RSS] Sync error:", error.message);
|
|
103
|
+
response.status(500).json({ error: error.message });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
import { RssClient } from "../rss-client.js";
|
|
3
|
+
import { formatFeed, isValidUrl, normalizeUrl } from "../utils.js";
|
|
4
|
+
|
|
5
|
+
export const feedsController = {
|
|
6
|
+
/**
|
|
7
|
+
* List all feeds
|
|
8
|
+
* GET /api/feeds
|
|
9
|
+
*/
|
|
10
|
+
async list(request, response) {
|
|
11
|
+
try {
|
|
12
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
13
|
+
if (!db) {
|
|
14
|
+
return response.status(500).json({ error: "Database not available" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const feedsCollection = db.collection("rssFeeds");
|
|
18
|
+
const feeds = await feedsCollection
|
|
19
|
+
.find({})
|
|
20
|
+
.sort({ addedAt: -1 })
|
|
21
|
+
.toArray();
|
|
22
|
+
|
|
23
|
+
response.json({
|
|
24
|
+
feeds: feeds.map(formatFeed),
|
|
25
|
+
total: feeds.length,
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("[RSS] Error listing feeds:", error.message);
|
|
29
|
+
response.status(500).json({ error: error.message });
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add a new feed
|
|
35
|
+
* POST /api/feeds
|
|
36
|
+
* Body: { url: string }
|
|
37
|
+
*/
|
|
38
|
+
async add(request, response) {
|
|
39
|
+
try {
|
|
40
|
+
const { url } = request.body;
|
|
41
|
+
|
|
42
|
+
if (!url || !isValidUrl(url)) {
|
|
43
|
+
return response.status(400).json({
|
|
44
|
+
error: response.locals.__("rss.error.invalidUrl"),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const normalizedUrl = normalizeUrl(url);
|
|
49
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
50
|
+
if (!db) {
|
|
51
|
+
return response.status(500).json({ error: "Database not available" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const feedsCollection = db.collection("rssFeeds");
|
|
55
|
+
|
|
56
|
+
// Check if feed already exists
|
|
57
|
+
const existing = await feedsCollection.findOne({ url: normalizedUrl });
|
|
58
|
+
if (existing) {
|
|
59
|
+
return response.status(409).json({
|
|
60
|
+
error: response.locals.__("rss.error.feedExists"),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fetch feed to validate and get metadata
|
|
65
|
+
const { rssConfig } = request.app.locals.application;
|
|
66
|
+
const client = new RssClient({
|
|
67
|
+
timeout: rssConfig?.fetchTimeout || 10_000,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let feedMeta;
|
|
71
|
+
try {
|
|
72
|
+
const result = await client.fetchFeed(normalizedUrl);
|
|
73
|
+
feedMeta = result.feed;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return response.status(400).json({
|
|
76
|
+
error: `${response.locals.__("rss.error.fetchFailed")}: ${error.message}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Insert feed
|
|
81
|
+
const feed = {
|
|
82
|
+
url: normalizedUrl,
|
|
83
|
+
title: feedMeta.title,
|
|
84
|
+
siteUrl: feedMeta.siteUrl,
|
|
85
|
+
description: feedMeta.description,
|
|
86
|
+
imageUrl: feedMeta.imageUrl,
|
|
87
|
+
enabled: true,
|
|
88
|
+
addedAt: new Date(),
|
|
89
|
+
lastFetchedAt: null,
|
|
90
|
+
lastError: null,
|
|
91
|
+
itemCount: 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = await feedsCollection.insertOne(feed);
|
|
95
|
+
feed._id = result.insertedId;
|
|
96
|
+
|
|
97
|
+
response.status(201).json({
|
|
98
|
+
message: response.locals.__("rss.success.feedAdded"),
|
|
99
|
+
feed: formatFeed(feed),
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("[RSS] Error adding feed:", error.message);
|
|
103
|
+
response.status(500).json({ error: error.message });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove a feed
|
|
109
|
+
* DELETE /api/feeds/:id
|
|
110
|
+
*/
|
|
111
|
+
async remove(request, response) {
|
|
112
|
+
try {
|
|
113
|
+
const { id } = request.params;
|
|
114
|
+
|
|
115
|
+
if (!ObjectId.isValid(id)) {
|
|
116
|
+
return response.status(400).json({ error: "Invalid feed ID" });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
120
|
+
if (!db) {
|
|
121
|
+
return response.status(500).json({ error: "Database not available" });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const feedsCollection = db.collection("rssFeeds");
|
|
125
|
+
const itemsCollection = db.collection("rssItems");
|
|
126
|
+
const feedId = new ObjectId(id);
|
|
127
|
+
|
|
128
|
+
// Check if feed exists
|
|
129
|
+
const feed = await feedsCollection.findOne({ _id: feedId });
|
|
130
|
+
if (!feed) {
|
|
131
|
+
return response.status(404).json({
|
|
132
|
+
error: response.locals.__("rss.error.feedNotFound"),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Delete feed and its items
|
|
137
|
+
await itemsCollection.deleteMany({ feedId });
|
|
138
|
+
await feedsCollection.deleteOne({ _id: feedId });
|
|
139
|
+
|
|
140
|
+
response.json({
|
|
141
|
+
message: response.locals.__("rss.success.feedRemoved"),
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error("[RSS] Error removing feed:", error.message);
|
|
145
|
+
response.status(500).json({ error: error.message });
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Toggle feed enabled/disabled
|
|
151
|
+
* PATCH /api/feeds/:id
|
|
152
|
+
* Body: { enabled: boolean }
|
|
153
|
+
*/
|
|
154
|
+
async toggle(request, response) {
|
|
155
|
+
try {
|
|
156
|
+
const { id } = request.params;
|
|
157
|
+
const { enabled } = request.body;
|
|
158
|
+
|
|
159
|
+
if (!ObjectId.isValid(id)) {
|
|
160
|
+
return response.status(400).json({ error: "Invalid feed ID" });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeof enabled !== "boolean") {
|
|
164
|
+
return response.status(400).json({ error: "enabled must be boolean" });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
168
|
+
if (!db) {
|
|
169
|
+
return response.status(500).json({ error: "Database not available" });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const feedsCollection = db.collection("rssFeeds");
|
|
173
|
+
const feedId = new ObjectId(id);
|
|
174
|
+
|
|
175
|
+
const result = await feedsCollection.findOneAndUpdate(
|
|
176
|
+
{ _id: feedId },
|
|
177
|
+
{ $set: { enabled } },
|
|
178
|
+
{ returnDocument: "after" }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (!result) {
|
|
182
|
+
return response.status(404).json({
|
|
183
|
+
error: response.locals.__("rss.error.feedNotFound"),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
response.json({
|
|
188
|
+
message: enabled
|
|
189
|
+
? response.locals.__("rss.success.feedEnabled")
|
|
190
|
+
: response.locals.__("rss.success.feedDisabled"),
|
|
191
|
+
feed: formatFeed(result),
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error("[RSS] Error toggling feed:", error.message);
|
|
195
|
+
response.status(500).json({ error: error.message });
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
import { formatItem } from "../utils.js";
|
|
3
|
+
|
|
4
|
+
export const itemsController = {
|
|
5
|
+
/**
|
|
6
|
+
* List feed items with pagination
|
|
7
|
+
* GET /api/items
|
|
8
|
+
* Query: page, limit, feedId, includeContent
|
|
9
|
+
*/
|
|
10
|
+
async list(request, response) {
|
|
11
|
+
try {
|
|
12
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
13
|
+
if (!db) {
|
|
14
|
+
return response.status(500).json({ error: "Database not available" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const page = Math.max(1, parseInt(request.query.page) || 1);
|
|
18
|
+
const limit = Math.min(100, Math.max(1, parseInt(request.query.limit) || 20));
|
|
19
|
+
const feedId = request.query.feedId;
|
|
20
|
+
const includeContent = request.query.includeContent === "true";
|
|
21
|
+
const skip = (page - 1) * limit;
|
|
22
|
+
|
|
23
|
+
const itemsCollection = db.collection("rssItems");
|
|
24
|
+
|
|
25
|
+
// Build query
|
|
26
|
+
const query = {};
|
|
27
|
+
if (feedId && ObjectId.isValid(feedId)) {
|
|
28
|
+
query.feedId = new ObjectId(feedId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get total count
|
|
32
|
+
const total = await itemsCollection.countDocuments(query);
|
|
33
|
+
|
|
34
|
+
// Get items
|
|
35
|
+
const items = await itemsCollection
|
|
36
|
+
.find(query)
|
|
37
|
+
.sort({ pubDate: -1 })
|
|
38
|
+
.skip(skip)
|
|
39
|
+
.limit(limit)
|
|
40
|
+
.toArray();
|
|
41
|
+
|
|
42
|
+
const totalPages = Math.ceil(total / limit);
|
|
43
|
+
|
|
44
|
+
response.json({
|
|
45
|
+
items: items.map((item) =>
|
|
46
|
+
formatItem(item, { includeContent })
|
|
47
|
+
),
|
|
48
|
+
pagination: {
|
|
49
|
+
page,
|
|
50
|
+
limit,
|
|
51
|
+
total,
|
|
52
|
+
totalPages,
|
|
53
|
+
hasNext: page < totalPages,
|
|
54
|
+
hasPrev: page > 1,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("[RSS] Error listing items:", error.message);
|
|
59
|
+
response.status(500).json({ error: error.message });
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get a single item by ID
|
|
65
|
+
* GET /api/items/:id
|
|
66
|
+
*/
|
|
67
|
+
async get(request, response) {
|
|
68
|
+
try {
|
|
69
|
+
const { id } = request.params;
|
|
70
|
+
|
|
71
|
+
if (!ObjectId.isValid(id)) {
|
|
72
|
+
return response.status(400).json({ error: "Invalid item ID" });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
76
|
+
if (!db) {
|
|
77
|
+
return response.status(500).json({ error: "Database not available" });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const itemsCollection = db.collection("rssItems");
|
|
81
|
+
const item = await itemsCollection.findOne({ _id: new ObjectId(id) });
|
|
82
|
+
|
|
83
|
+
if (!item) {
|
|
84
|
+
return response.status(404).json({ error: "Item not found" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
response.json({
|
|
88
|
+
item: formatItem(item, { includeContent: true }),
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error("[RSS] Error getting item:", error.message);
|
|
92
|
+
response.status(500).json({ error: error.message });
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getSyncState, runSync } from "../sync.js";
|
|
2
|
+
|
|
3
|
+
export const statusController = {
|
|
4
|
+
/**
|
|
5
|
+
* Get sync status
|
|
6
|
+
* GET /api/status
|
|
7
|
+
*/
|
|
8
|
+
async status(request, response) {
|
|
9
|
+
try {
|
|
10
|
+
const db = request.app.locals.application.getRssDb?.();
|
|
11
|
+
const syncState = getSyncState();
|
|
12
|
+
const { rssConfig } = request.app.locals.application;
|
|
13
|
+
|
|
14
|
+
let feedsCount = 0;
|
|
15
|
+
let itemsCount = 0;
|
|
16
|
+
let enabledFeedsCount = 0;
|
|
17
|
+
|
|
18
|
+
if (db) {
|
|
19
|
+
const feedsCollection = db.collection("rssFeeds");
|
|
20
|
+
const itemsCollection = db.collection("rssItems");
|
|
21
|
+
|
|
22
|
+
feedsCount = await feedsCollection.countDocuments({});
|
|
23
|
+
enabledFeedsCount = await feedsCollection.countDocuments({ enabled: true });
|
|
24
|
+
itemsCount = await itemsCollection.countDocuments({});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const syncIntervalMs = rssConfig?.syncInterval || 900_000;
|
|
28
|
+
const nextSync = syncState.lastSync
|
|
29
|
+
? new Date(syncState.lastSync.getTime() + syncIntervalMs)
|
|
30
|
+
: null;
|
|
31
|
+
|
|
32
|
+
response.json({
|
|
33
|
+
status: syncState.syncing ? "syncing" : "idle",
|
|
34
|
+
lastSync: syncState.lastSync?.toISOString() || null,
|
|
35
|
+
nextSync: nextSync?.toISOString() || null,
|
|
36
|
+
lastError: syncState.lastError,
|
|
37
|
+
stats: {
|
|
38
|
+
feedsCount,
|
|
39
|
+
enabledFeedsCount,
|
|
40
|
+
itemsCount,
|
|
41
|
+
lastFeedsProcessed: syncState.feedsProcessed,
|
|
42
|
+
lastItemsAdded: syncState.itemsAdded,
|
|
43
|
+
},
|
|
44
|
+
config: {
|
|
45
|
+
syncInterval: syncIntervalMs,
|
|
46
|
+
maxItemsPerFeed: rssConfig?.maxItemsPerFeed || 50,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("[RSS] Error getting status:", error.message);
|
|
51
|
+
response.status(500).json({ error: error.message });
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Trigger manual sync
|
|
57
|
+
* POST /api/refresh
|
|
58
|
+
*/
|
|
59
|
+
async refresh(request, response) {
|
|
60
|
+
try {
|
|
61
|
+
const Indiekit = request.app.locals.indiekit;
|
|
62
|
+
const { rssConfig } = request.app.locals.application;
|
|
63
|
+
|
|
64
|
+
if (!Indiekit || !rssConfig) {
|
|
65
|
+
return response.status(500).json({ error: "Plugin not configured" });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const syncState = getSyncState();
|
|
69
|
+
if (syncState.syncing) {
|
|
70
|
+
return response.status(409).json({
|
|
71
|
+
error: "Sync already in progress",
|
|
72
|
+
status: "syncing",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Run sync asynchronously
|
|
77
|
+
runSync(Indiekit, rssConfig).catch((err) => {
|
|
78
|
+
console.error("[RSS] Manual sync error:", err.message);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
response.json({
|
|
82
|
+
message: response.locals.__("rss.syncing"),
|
|
83
|
+
status: "started",
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("[RSS] Error triggering refresh:", error.message);
|
|
87
|
+
response.status(500).json({ error: error.message });
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|