@rmdes/indiekit-endpoint-blogroll 1.0.20 → 1.0.22
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 +3 -0
- package/lib/controllers/api.js +5 -3
- package/lib/controllers/dashboard.js +7 -7
- package/lib/controllers/sources.js +58 -0
- package/lib/storage/blogs.js +11 -2
- package/lib/storage/sources.js +6 -2
- package/lib/sync/feedland.js +140 -0
- package/lib/sync/scheduler.js +4 -1
- package/locales/de.json +11 -2
- package/locales/en.json +11 -2
- package/locales/es-419.json +11 -2
- package/locales/es.json +11 -2
- package/locales/fr.json +11 -2
- package/locales/hi.json +11 -2
- package/locales/id.json +11 -2
- package/locales/it.json +11 -2
- package/locales/nl.json +11 -2
- package/locales/pl.json +11 -2
- package/locales/pt-BR.json +11 -2
- package/locales/pt.json +11 -2
- package/locales/sr.json +11 -2
- package/locales/sv.json +11 -2
- package/locales/zh-Hans-CN.json +11 -2
- package/package.json +1 -1
- package/views/blogroll-source-edit.njk +92 -0
package/index.js
CHANGED
|
@@ -93,6 +93,9 @@ export default class BlogrollEndpoint {
|
|
|
93
93
|
protectedRouter.post("/api/microsub-webhook", apiController.microsubWebhook);
|
|
94
94
|
protectedRouter.get("/api/microsub-status", apiController.microsubStatus);
|
|
95
95
|
|
|
96
|
+
// FeedLand integration (protected - category discovery)
|
|
97
|
+
protectedRouter.get("/api/feedland-categories", sourcesController.feedlandCategories);
|
|
98
|
+
|
|
96
99
|
return protectedRouter;
|
|
97
100
|
}
|
|
98
101
|
|
package/lib/controllers/api.js
CHANGED
|
@@ -18,16 +18,18 @@ import { handleMicrosubWebhook, isMicrosubAvailable } from "../sync/microsub.js"
|
|
|
18
18
|
async function listBlogs(request, response) {
|
|
19
19
|
const { application } = request.app.locals;
|
|
20
20
|
|
|
21
|
-
const { category, limit = 100, offset = 0 } = request.query;
|
|
21
|
+
const { category, source, sort, limit = 100, offset = 0 } = request.query;
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
24
|
const blogs = await getBlogs(application, {
|
|
25
25
|
category,
|
|
26
|
+
source,
|
|
27
|
+
sort,
|
|
26
28
|
limit: Number(limit),
|
|
27
29
|
offset: Number(offset),
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
const total = await countBlogs(application, { category });
|
|
32
|
+
const total = await countBlogs(application, { category, source });
|
|
31
33
|
|
|
32
34
|
response.json({
|
|
33
35
|
items: blogs.map(sanitizeBlog),
|
|
@@ -232,11 +234,11 @@ function sanitizeBlog(blog) {
|
|
|
232
234
|
itemCount: blog.itemCount,
|
|
233
235
|
pinned: blog.pinned,
|
|
234
236
|
lastFetchAt: blog.lastFetchAt,
|
|
237
|
+
source: blog.source || null,
|
|
235
238
|
};
|
|
236
239
|
|
|
237
240
|
// Include Microsub metadata if applicable
|
|
238
241
|
if (blog.source === "microsub") {
|
|
239
|
-
sanitized.source = "microsub";
|
|
240
242
|
sanitized.microsubChannel = blog.microsubChannelName;
|
|
241
243
|
}
|
|
242
244
|
|
|
@@ -77,13 +77,13 @@ async function sync(request, response) {
|
|
|
77
77
|
|
|
78
78
|
if (result.skipped) {
|
|
79
79
|
request.session.messages = [
|
|
80
|
-
{ type: "warning", content: request.__("blogroll.
|
|
80
|
+
{ type: "warning", content: request.__("blogroll.syncResult.already_running") },
|
|
81
81
|
];
|
|
82
82
|
} else if (result.success) {
|
|
83
83
|
request.session.messages = [
|
|
84
84
|
{
|
|
85
85
|
type: "success",
|
|
86
|
-
content: request.__("blogroll.
|
|
86
|
+
content: request.__("blogroll.syncResult.success", {
|
|
87
87
|
blogs: result.blogs.success,
|
|
88
88
|
items: result.items.added,
|
|
89
89
|
}),
|
|
@@ -91,13 +91,13 @@ async function sync(request, response) {
|
|
|
91
91
|
];
|
|
92
92
|
} else {
|
|
93
93
|
request.session.messages = [
|
|
94
|
-
{ type: "error", content: request.__("blogroll.
|
|
94
|
+
{ type: "error", content: request.__("blogroll.syncResult.error", { error: result.error }) },
|
|
95
95
|
];
|
|
96
96
|
}
|
|
97
97
|
} catch (error) {
|
|
98
98
|
console.error("[Blogroll] Manual sync error:", error);
|
|
99
99
|
request.session.messages = [
|
|
100
|
-
{ type: "error", content: request.__("blogroll.
|
|
100
|
+
{ type: "error", content: request.__("blogroll.syncResult.error", { error: error.message }) },
|
|
101
101
|
];
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -118,7 +118,7 @@ async function clearResync(request, response) {
|
|
|
118
118
|
request.session.messages = [
|
|
119
119
|
{
|
|
120
120
|
type: "success",
|
|
121
|
-
content: request.__("blogroll.
|
|
121
|
+
content: request.__("blogroll.syncResult.cleared_success", {
|
|
122
122
|
blogs: result.blogs.success,
|
|
123
123
|
items: result.items.added,
|
|
124
124
|
}),
|
|
@@ -126,13 +126,13 @@ async function clearResync(request, response) {
|
|
|
126
126
|
];
|
|
127
127
|
} else {
|
|
128
128
|
request.session.messages = [
|
|
129
|
-
{ type: "error", content: request.__("blogroll.
|
|
129
|
+
{ type: "error", content: request.__("blogroll.syncResult.error", { error: result.error }) },
|
|
130
130
|
];
|
|
131
131
|
}
|
|
132
132
|
} catch (error) {
|
|
133
133
|
console.error("[Blogroll] Clear resync error:", error);
|
|
134
134
|
request.session.messages = [
|
|
135
|
-
{ type: "error", content: request.__("blogroll.
|
|
135
|
+
{ type: "error", content: request.__("blogroll.syncResult.error", { error: error.message }) },
|
|
136
136
|
];
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
getMicrosubChannels,
|
|
17
17
|
isMicrosubAvailable,
|
|
18
18
|
} from "../sync/microsub.js";
|
|
19
|
+
import {
|
|
20
|
+
syncFeedlandSource,
|
|
21
|
+
fetchFeedlandCategories,
|
|
22
|
+
} from "../sync/feedland.js";
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* List sources
|
|
@@ -95,6 +99,9 @@ async function create(request, response) {
|
|
|
95
99
|
enabled,
|
|
96
100
|
channelFilter,
|
|
97
101
|
categoryPrefix,
|
|
102
|
+
feedlandInstance,
|
|
103
|
+
feedlandUsername,
|
|
104
|
+
feedlandCategory,
|
|
98
105
|
} = request.body;
|
|
99
106
|
|
|
100
107
|
try {
|
|
@@ -120,6 +127,13 @@ async function create(request, response) {
|
|
|
120
127
|
return response.redirect(`${request.baseUrl}/sources/new`);
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
if (type === "feedland" && (!feedlandInstance || !feedlandUsername)) {
|
|
131
|
+
request.session.messages = [
|
|
132
|
+
{ type: "error", content: request.__("blogroll.sources.form.feedlandRequired") },
|
|
133
|
+
];
|
|
134
|
+
return response.redirect(`${request.baseUrl}/sources/new`);
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
const sourceData = {
|
|
124
138
|
name,
|
|
125
139
|
type,
|
|
@@ -135,12 +149,21 @@ async function create(request, response) {
|
|
|
135
149
|
sourceData.categoryPrefix = categoryPrefix || "";
|
|
136
150
|
}
|
|
137
151
|
|
|
152
|
+
// Add feedland-specific fields
|
|
153
|
+
if (type === "feedland") {
|
|
154
|
+
sourceData.feedlandInstance = feedlandInstance.replace(/\/+$/, "");
|
|
155
|
+
sourceData.feedlandUsername = feedlandUsername;
|
|
156
|
+
sourceData.feedlandCategory = feedlandCategory || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
const source = await createSource(application, sourceData);
|
|
139
160
|
|
|
140
161
|
// Trigger initial sync based on source type
|
|
141
162
|
try {
|
|
142
163
|
if (type === "microsub") {
|
|
143
164
|
await syncMicrosubSource(application, source);
|
|
165
|
+
} else if (type === "feedland") {
|
|
166
|
+
await syncFeedlandSource(application, source);
|
|
144
167
|
} else {
|
|
145
168
|
await syncOpmlSource(application, source);
|
|
146
169
|
}
|
|
@@ -223,6 +246,9 @@ async function update(request, response) {
|
|
|
223
246
|
enabled,
|
|
224
247
|
channelFilter,
|
|
225
248
|
categoryPrefix,
|
|
249
|
+
feedlandInstance,
|
|
250
|
+
feedlandUsername,
|
|
251
|
+
feedlandCategory,
|
|
226
252
|
} = request.body;
|
|
227
253
|
|
|
228
254
|
try {
|
|
@@ -247,6 +273,15 @@ async function update(request, response) {
|
|
|
247
273
|
updateData.categoryPrefix = categoryPrefix || "";
|
|
248
274
|
}
|
|
249
275
|
|
|
276
|
+
// Add feedland-specific fields
|
|
277
|
+
if (type === "feedland") {
|
|
278
|
+
updateData.feedlandInstance = feedlandInstance
|
|
279
|
+
? feedlandInstance.replace(/\/+$/, "")
|
|
280
|
+
: null;
|
|
281
|
+
updateData.feedlandUsername = feedlandUsername || null;
|
|
282
|
+
updateData.feedlandCategory = feedlandCategory || null;
|
|
283
|
+
}
|
|
284
|
+
|
|
250
285
|
await updateSource(application, id, updateData);
|
|
251
286
|
|
|
252
287
|
request.session.messages = [
|
|
@@ -313,6 +348,8 @@ async function sync(request, response) {
|
|
|
313
348
|
let result;
|
|
314
349
|
if (source.type === "microsub") {
|
|
315
350
|
result = await syncMicrosubSource(application, source);
|
|
351
|
+
} else if (source.type === "feedland") {
|
|
352
|
+
result = await syncFeedlandSource(application, source);
|
|
316
353
|
} else {
|
|
317
354
|
result = await syncOpmlSource(application, source);
|
|
318
355
|
}
|
|
@@ -358,6 +395,26 @@ function consumeFlashMessage(request) {
|
|
|
358
395
|
return result;
|
|
359
396
|
}
|
|
360
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Fetch FeedLand categories (AJAX endpoint)
|
|
400
|
+
* GET /api/feedland-categories?instance=...&username=...
|
|
401
|
+
*/
|
|
402
|
+
async function feedlandCategories(request, response) {
|
|
403
|
+
const { instance, username } = request.query;
|
|
404
|
+
|
|
405
|
+
if (!instance || !username) {
|
|
406
|
+
return response.status(400).json({ error: "instance and username are required" });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const data = await fetchFeedlandCategories(instance, username);
|
|
411
|
+
response.json(data);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error("[Blogroll] FeedLand categories fetch error:", error.message);
|
|
414
|
+
response.status(502).json({ error: error.message });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
361
418
|
export const sourcesController = {
|
|
362
419
|
list,
|
|
363
420
|
newForm,
|
|
@@ -366,4 +423,5 @@ export const sourcesController = {
|
|
|
366
423
|
update,
|
|
367
424
|
remove,
|
|
368
425
|
sync,
|
|
426
|
+
feedlandCategories,
|
|
369
427
|
};
|
package/lib/storage/blogs.js
CHANGED
|
@@ -29,10 +29,18 @@ export async function getBlogs(application, options = {}) {
|
|
|
29
29
|
if (!includeHidden) query.hidden = { $ne: true };
|
|
30
30
|
if (category) query.category = category;
|
|
31
31
|
if (sourceId) query.sourceId = new ObjectId(sourceId);
|
|
32
|
+
if (options.source) query.source = options.source;
|
|
33
|
+
|
|
34
|
+
// Default sort: pinned first, then alphabetical
|
|
35
|
+
// "recent" sort: pinned first, then by last fetch time (newest first)
|
|
36
|
+
const sortOrder =
|
|
37
|
+
options.sort === "recent"
|
|
38
|
+
? { pinned: -1, lastFetchAt: -1, title: 1 }
|
|
39
|
+
: { pinned: -1, title: 1 };
|
|
32
40
|
|
|
33
41
|
return collection
|
|
34
42
|
.find(query)
|
|
35
|
-
.sort(
|
|
43
|
+
.sort(sortOrder)
|
|
36
44
|
.skip(offset)
|
|
37
45
|
.limit(limit)
|
|
38
46
|
.toArray();
|
|
@@ -46,11 +54,12 @@ export async function getBlogs(application, options = {}) {
|
|
|
46
54
|
*/
|
|
47
55
|
export async function countBlogs(application, options = {}) {
|
|
48
56
|
const collection = getCollection(application);
|
|
49
|
-
const { category, includeHidden = false } = options;
|
|
57
|
+
const { category, source, includeHidden = false } = options;
|
|
50
58
|
|
|
51
59
|
const query = { status: { $ne: "deleted" } };
|
|
52
60
|
if (!includeHidden) query.hidden = { $ne: true };
|
|
53
61
|
if (category) query.category = category;
|
|
62
|
+
if (source) query.source = source;
|
|
54
63
|
|
|
55
64
|
return collection.countDocuments(query);
|
|
56
65
|
}
|
package/lib/storage/sources.js
CHANGED
|
@@ -48,13 +48,17 @@ export async function createSource(application, data) {
|
|
|
48
48
|
const now = new Date().toISOString();
|
|
49
49
|
|
|
50
50
|
const source = {
|
|
51
|
-
type: data.type, // "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub"
|
|
51
|
+
type: data.type, // "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub" | "feedland"
|
|
52
52
|
name: data.name,
|
|
53
53
|
url: data.url || null,
|
|
54
54
|
opmlContent: data.opmlContent || null,
|
|
55
55
|
// Microsub-specific fields
|
|
56
56
|
channelFilter: data.channelFilter || null,
|
|
57
57
|
categoryPrefix: data.categoryPrefix || "",
|
|
58
|
+
// FeedLand-specific fields
|
|
59
|
+
feedlandInstance: data.feedlandInstance || null,
|
|
60
|
+
feedlandUsername: data.feedlandUsername || null,
|
|
61
|
+
feedlandCategory: data.feedlandCategory || null,
|
|
58
62
|
enabled: data.enabled !== false,
|
|
59
63
|
syncInterval: data.syncInterval || 60, // minutes
|
|
60
64
|
lastSyncAt: null,
|
|
@@ -160,7 +164,7 @@ export async function getSourcesDueForSync(application) {
|
|
|
160
164
|
return collection
|
|
161
165
|
.find({
|
|
162
166
|
enabled: true,
|
|
163
|
-
type: { $in: ["opml_url", "json_feed", "microsub"] },
|
|
167
|
+
type: { $in: ["opml_url", "json_feed", "microsub", "feedland"] },
|
|
164
168
|
$or: [
|
|
165
169
|
{ lastSyncAt: null },
|
|
166
170
|
{
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeedLand synchronization
|
|
3
|
+
* @module sync/feedland
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetchAndParseOpml } from "./opml.js";
|
|
7
|
+
import { upsertBlog } from "../storage/blogs.js";
|
|
8
|
+
import { updateSourceSyncStatus } from "../storage/sources.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fetch user categories from a FeedLand instance
|
|
12
|
+
* @param {string} instanceUrl - FeedLand instance URL (e.g., https://feedland.com)
|
|
13
|
+
* @param {string} username - FeedLand username
|
|
14
|
+
* @param {number} timeout - Fetch timeout in ms
|
|
15
|
+
* @returns {Promise<object>} Category data { categories: string[], homePageCategories: string[] }
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchFeedlandCategories(instanceUrl, username, timeout = 10000) {
|
|
18
|
+
const baseUrl = instanceUrl.replace(/\/+$/, "");
|
|
19
|
+
const url = `${baseUrl}/getusercategories?screenname=${encodeURIComponent(username)}`;
|
|
20
|
+
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
headers: {
|
|
28
|
+
"User-Agent": "Indiekit-Blogroll/1.0",
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
|
|
41
|
+
// FeedLand returns comma-separated strings
|
|
42
|
+
const categories = data.categories
|
|
43
|
+
? data.categories.split(",").map((c) => c.trim()).filter(Boolean)
|
|
44
|
+
: [];
|
|
45
|
+
const homePageCategories = data.homePageCategories
|
|
46
|
+
? data.homePageCategories.split(",").map((c) => c.trim()).filter(Boolean)
|
|
47
|
+
: [];
|
|
48
|
+
|
|
49
|
+
return { categories, homePageCategories, screenname: data.screenname };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
if (error.name === "AbortError") {
|
|
53
|
+
throw new Error("Request timed out");
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the OPML URL for a FeedLand source
|
|
61
|
+
* @param {object} source - Source document with feedland fields
|
|
62
|
+
* @returns {string} OPML URL
|
|
63
|
+
*/
|
|
64
|
+
export function buildFeedlandOpmlUrl(source) {
|
|
65
|
+
const baseUrl = source.feedlandInstance.replace(/\/+$/, "");
|
|
66
|
+
let url = `${baseUrl}/opml?screenname=${encodeURIComponent(source.feedlandUsername)}`;
|
|
67
|
+
|
|
68
|
+
if (source.feedlandCategory) {
|
|
69
|
+
url += `&catname=${encodeURIComponent(source.feedlandCategory)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return url;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the FeedLand river URL for linking back
|
|
77
|
+
* @param {object} source - Source document with feedland fields
|
|
78
|
+
* @returns {string} River URL
|
|
79
|
+
*/
|
|
80
|
+
export function buildFeedlandRiverUrl(source) {
|
|
81
|
+
const baseUrl = source.feedlandInstance.replace(/\/+$/, "");
|
|
82
|
+
return `${baseUrl}/?river=true&screenname=${encodeURIComponent(source.feedlandUsername)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sync blogs from a FeedLand source
|
|
87
|
+
* @param {object} application - Application instance
|
|
88
|
+
* @param {object} source - Source document
|
|
89
|
+
* @returns {Promise<object>} Sync result
|
|
90
|
+
*/
|
|
91
|
+
export async function syncFeedlandSource(application, source) {
|
|
92
|
+
try {
|
|
93
|
+
const opmlUrl = buildFeedlandOpmlUrl(source);
|
|
94
|
+
const blogs = await fetchAndParseOpml(opmlUrl);
|
|
95
|
+
|
|
96
|
+
let added = 0;
|
|
97
|
+
let updated = 0;
|
|
98
|
+
|
|
99
|
+
for (const blog of blogs) {
|
|
100
|
+
// FeedLand OPML includes a category attribute with comma-separated categories.
|
|
101
|
+
// Use the first category, or fall back to the source's feedlandCategory filter,
|
|
102
|
+
// or use the FeedLand username as a category grouping.
|
|
103
|
+
const category = blog.category
|
|
104
|
+
|| source.feedlandCategory
|
|
105
|
+
|| source.feedlandUsername
|
|
106
|
+
|| "";
|
|
107
|
+
|
|
108
|
+
const result = await upsertBlog(application, {
|
|
109
|
+
...blog,
|
|
110
|
+
category,
|
|
111
|
+
source: "feedland",
|
|
112
|
+
sourceId: source._id,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (result.upserted) added++;
|
|
116
|
+
else if (result.modified) updated++;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Update source sync status
|
|
120
|
+
await updateSourceSyncStatus(application, source._id, { success: true });
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`[Blogroll] Synced FeedLand source "${source.name}" (${source.feedlandUsername}@${source.feedlandInstance}): ${added} added, ${updated} updated, ${blogs.length} total`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return { success: true, added, updated, total: blogs.length };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Update source with error status
|
|
129
|
+
await updateSourceSyncStatus(application, source._id, {
|
|
130
|
+
success: false,
|
|
131
|
+
error: error.message,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.error(
|
|
135
|
+
`[Blogroll] FeedLand sync failed for "${source.name}":`,
|
|
136
|
+
error.message
|
|
137
|
+
);
|
|
138
|
+
return { success: false, error: error.message };
|
|
139
|
+
}
|
|
140
|
+
}
|
package/lib/sync/scheduler.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getBlogs, countBlogs } from "../storage/blogs.js";
|
|
|
8
8
|
import { countItems, deleteOldItems } from "../storage/items.js";
|
|
9
9
|
import { syncOpmlSource } from "./opml.js";
|
|
10
10
|
import { syncMicrosubSource } from "./microsub.js";
|
|
11
|
+
import { syncFeedlandSource } from "./feedland.js";
|
|
11
12
|
import { syncBlogItems } from "./feed.js";
|
|
12
13
|
|
|
13
14
|
let syncInterval = null;
|
|
@@ -42,7 +43,7 @@ export async function runFullSync(application, options = {}) {
|
|
|
42
43
|
// Sync all enabled sources (OPML, JSON, Microsub)
|
|
43
44
|
const sources = await getSources(application);
|
|
44
45
|
const enabledSources = sources.filter(
|
|
45
|
-
(s) => s.enabled && ["opml_url", "opml_file", "json_feed", "microsub"].includes(s.type)
|
|
46
|
+
(s) => s.enabled && ["opml_url", "opml_file", "json_feed", "microsub", "feedland"].includes(s.type)
|
|
46
47
|
);
|
|
47
48
|
|
|
48
49
|
let sourcesSuccess = 0;
|
|
@@ -53,6 +54,8 @@ export async function runFullSync(application, options = {}) {
|
|
|
53
54
|
let result;
|
|
54
55
|
if (source.type === "microsub") {
|
|
55
56
|
result = await syncMicrosubSource(application, source);
|
|
57
|
+
} else if (source.type === "feedland") {
|
|
58
|
+
result = await syncFeedlandSource(application, source);
|
|
56
59
|
} else {
|
|
57
60
|
result = await syncOpmlSource(application, source);
|
|
58
61
|
}
|
package/locales/de.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Dadurch werden alle zwischengespeicherten Einträge gelöscht und alles neu abgerufen. Fortfahren?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/en.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "This will delete all cached items and re-fetch everything. Continue?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/es-419.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a descargar todo. ¿Continuar?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/es.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a obtenerlo todo. ¿Continuar?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/fr.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Cela supprimera toutes les entrées mises en cache et récupérera tout à nouveau. Continuer ?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/hi.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "इससे सभी कैश किए गए आइटम हटा दिए जाएंगे और सब कुछ फिर से प्राप्त किया जाएगा। जारी रखें?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/id.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Ini akan menghapus semua item yang di-cache dan mengambil semuanya lagi. Lanjutkan?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/it.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Questo cancellerà tutti gli elementi memorizzati e recupererà tutto nuovamente. Continuare?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/nl.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Dit verwijdert alle gecachte items en haalt alles opnieuw op. Doorgaan?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/pl.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Spowoduje to usunięcie wszystkich elementów w pamięci podręcznej i ponowne pobranie wszystkiego. Kontynuować?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/pt-BR.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Isso excluirá todos os itens em cache e buscará tudo novamente. Continuar?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/pt.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Isto eliminará todos os itens em cache e voltará a obter tudo. Continuar?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/sr.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Ово ће обрисати све кеширане ставке и поново преузети све. Наставити?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/sv.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "Detta kommer att ta bort alla cachade poster och hämta allt igen. Fortsätta?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/locales/zh-Hans-CN.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clearConfirm": "这将删除所有缓存的条目并重新获取所有内容。继续吗?"
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
"
|
|
29
|
+
"syncResult": {
|
|
30
30
|
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
31
|
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
{% if microsubAvailable %}
|
|
16
16
|
<option value="microsub" {% if source.type == 'microsub' %}selected{% endif %}>Microsub Subscriptions</option>
|
|
17
17
|
{% endif %}
|
|
18
|
+
<option value="feedland" {% if source.type == 'feedland' %}selected{% endif %}>FeedLand</option>
|
|
18
19
|
</select>
|
|
19
20
|
<span class="hint">{{ __("blogroll.sources.form.typeHint") }}</span>
|
|
20
21
|
</div>
|
|
@@ -48,6 +49,32 @@
|
|
|
48
49
|
<span class="hint">{{ __("blogroll.sources.form.categoryPrefixHint") | default("Optional prefix for blog categories (e.g., 'Following: ')") }}</span>
|
|
49
50
|
</div>
|
|
50
51
|
|
|
52
|
+
<div class="blogroll-field" id="feedlandInstanceField" style="display: none;">
|
|
53
|
+
<label class="label" for="feedlandInstance">{{ __("blogroll.sources.form.feedlandInstance") | default("FeedLand Instance URL") }}</label>
|
|
54
|
+
<input class="input" type="url" id="feedlandInstance" name="feedlandInstance" value="{{ source.feedlandInstance if source else 'https://feedland.com' }}" placeholder="https://feedland.com">
|
|
55
|
+
<span class="hint">{{ __("blogroll.sources.form.feedlandInstanceHint") | default("FeedLand instance URL (feedland.com or self-hosted)") }}</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="blogroll-field" id="feedlandUsernameField" style="display: none;">
|
|
59
|
+
<label class="label" for="feedlandUsername">{{ __("blogroll.sources.form.feedlandUsername") | default("FeedLand Username") }}</label>
|
|
60
|
+
<input class="input" type="text" id="feedlandUsername" name="feedlandUsername" value="{{ source.feedlandUsername if source else '' }}" placeholder="e.g., davewiner">
|
|
61
|
+
<span class="hint">{{ __("blogroll.sources.form.feedlandUsernameHint") | default("Your FeedLand screen name") }}</span>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="blogroll-field" id="feedlandCategoryField" style="display: none;">
|
|
65
|
+
<label class="label" for="feedlandCategory">{{ __("blogroll.sources.form.feedlandCategory") | default("FeedLand Category") }}</label>
|
|
66
|
+
<div style="display: flex; gap: 0.5rem; align-items: flex-start;">
|
|
67
|
+
<select class="select" id="feedlandCategory" name="feedlandCategory" style="flex: 1;">
|
|
68
|
+
<option value="">{{ __("blogroll.sources.form.feedlandCategoryAll") | default("All subscriptions") }}</option>
|
|
69
|
+
{% if source.feedlandCategory %}
|
|
70
|
+
<option value="{{ source.feedlandCategory }}" selected>{{ source.feedlandCategory }}</option>
|
|
71
|
+
{% endif %}
|
|
72
|
+
</select>
|
|
73
|
+
<button type="button" class="button button--secondary" onclick="loadFeedlandCategories()" id="feedlandLoadBtn">{{ __("blogroll.sources.form.feedlandLoadCategories") | default("Load") }}</button>
|
|
74
|
+
</div>
|
|
75
|
+
<span class="hint" id="feedlandCategoryHint">{{ __("blogroll.sources.form.feedlandCategoryHint") | default("Optional: sync only feeds from a specific category") }}</span>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
51
78
|
<div class="blogroll-field">
|
|
52
79
|
<label class="label" for="syncInterval">{{ __("blogroll.sources.form.syncInterval") }}</label>
|
|
53
80
|
<select class="select" id="syncInterval" name="syncInterval">
|
|
@@ -78,12 +105,18 @@ function toggleTypeFields() {
|
|
|
78
105
|
const opmlContentField = document.getElementById('opmlContentField');
|
|
79
106
|
const microsubChannelField = document.getElementById('microsubChannelField');
|
|
80
107
|
const categoryPrefixField = document.getElementById('categoryPrefixField');
|
|
108
|
+
const feedlandInstanceField = document.getElementById('feedlandInstanceField');
|
|
109
|
+
const feedlandUsernameField = document.getElementById('feedlandUsernameField');
|
|
110
|
+
const feedlandCategoryField = document.getElementById('feedlandCategoryField');
|
|
81
111
|
|
|
82
112
|
// Hide all type-specific fields first
|
|
83
113
|
urlField.style.display = 'none';
|
|
84
114
|
opmlContentField.style.display = 'none';
|
|
85
115
|
if (microsubChannelField) microsubChannelField.style.display = 'none';
|
|
86
116
|
if (categoryPrefixField) categoryPrefixField.style.display = 'none';
|
|
117
|
+
if (feedlandInstanceField) feedlandInstanceField.style.display = 'none';
|
|
118
|
+
if (feedlandUsernameField) feedlandUsernameField.style.display = 'none';
|
|
119
|
+
if (feedlandCategoryField) feedlandCategoryField.style.display = 'none';
|
|
87
120
|
|
|
88
121
|
// Show fields based on type
|
|
89
122
|
if (type === 'opml_url') {
|
|
@@ -93,7 +126,66 @@ function toggleTypeFields() {
|
|
|
93
126
|
} else if (type === 'microsub') {
|
|
94
127
|
if (microsubChannelField) microsubChannelField.style.display = 'flex';
|
|
95
128
|
if (categoryPrefixField) categoryPrefixField.style.display = 'flex';
|
|
129
|
+
} else if (type === 'feedland') {
|
|
130
|
+
if (feedlandInstanceField) feedlandInstanceField.style.display = 'flex';
|
|
131
|
+
if (feedlandUsernameField) feedlandUsernameField.style.display = 'flex';
|
|
132
|
+
if (feedlandCategoryField) feedlandCategoryField.style.display = 'flex';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function loadFeedlandCategories() {
|
|
137
|
+
const instance = document.getElementById('feedlandInstance').value;
|
|
138
|
+
const username = document.getElementById('feedlandUsername').value;
|
|
139
|
+
const select = document.getElementById('feedlandCategory');
|
|
140
|
+
const btn = document.getElementById('feedlandLoadBtn');
|
|
141
|
+
const hint = document.getElementById('feedlandCategoryHint');
|
|
142
|
+
const currentValue = select.value;
|
|
143
|
+
|
|
144
|
+
if (!instance || !username) {
|
|
145
|
+
hint.textContent = 'Please enter instance URL and username first';
|
|
146
|
+
return;
|
|
96
147
|
}
|
|
148
|
+
|
|
149
|
+
btn.disabled = true;
|
|
150
|
+
btn.textContent = '...';
|
|
151
|
+
hint.textContent = 'Loading categories...';
|
|
152
|
+
|
|
153
|
+
const baseUrl = '{{ baseUrl }}';
|
|
154
|
+
const url = baseUrl + '/api/feedland-categories?instance=' + encodeURIComponent(instance) + '&username=' + encodeURIComponent(username);
|
|
155
|
+
|
|
156
|
+
fetch(url, { credentials: 'same-origin' })
|
|
157
|
+
.then(function(r) { return r.json(); })
|
|
158
|
+
.then(function(data) {
|
|
159
|
+
if (data.error) {
|
|
160
|
+
hint.textContent = 'Error: ' + data.error;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clear existing options and rebuild safely using DOM methods
|
|
165
|
+
while (select.options.length > 0) select.remove(0);
|
|
166
|
+
var allOpt = document.createElement('option');
|
|
167
|
+
allOpt.value = '';
|
|
168
|
+
allOpt.textContent = '{{ __("blogroll.sources.form.feedlandCategoryAll") | default("All subscriptions") }}';
|
|
169
|
+
select.appendChild(allOpt);
|
|
170
|
+
|
|
171
|
+
var cats = data.categories || [];
|
|
172
|
+
cats.forEach(function(cat) {
|
|
173
|
+
var opt = document.createElement('option');
|
|
174
|
+
opt.value = cat;
|
|
175
|
+
opt.textContent = cat;
|
|
176
|
+
if (cat === currentValue) opt.selected = true;
|
|
177
|
+
select.appendChild(opt);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
hint.textContent = cats.length + ' categories found' + (data.screenname ? ' for ' + data.screenname : '');
|
|
181
|
+
})
|
|
182
|
+
.catch(function(err) {
|
|
183
|
+
hint.textContent = 'Failed to load: ' + err.message;
|
|
184
|
+
})
|
|
185
|
+
.finally(function() {
|
|
186
|
+
btn.disabled = false;
|
|
187
|
+
btn.textContent = '{{ __("blogroll.sources.form.feedlandLoadCategories") | default("Load") }}';
|
|
188
|
+
});
|
|
97
189
|
}
|
|
98
190
|
|
|
99
191
|
// Initialize on load
|