feedscout 1.6.1 → 1.7.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.
Files changed (98) hide show
  1. package/README.md +14 -0
  2. package/dist/blogrolls/extractors.cjs +1 -1
  3. package/dist/blogrolls/extractors.js +1 -1
  4. package/dist/blogrolls/index.cjs +3 -2
  5. package/dist/blogrolls/index.js +5 -4
  6. package/dist/common/discover/index.cjs +21 -9
  7. package/dist/common/discover/index.js +21 -9
  8. package/dist/common/discover/utils.cjs +44 -18
  9. package/dist/common/discover/utils.d.cts +8 -0
  10. package/dist/common/discover/utils.d.ts +8 -0
  11. package/dist/common/discover/utils.js +43 -19
  12. package/dist/common/locales.cjs +3 -1
  13. package/dist/common/locales.js +3 -1
  14. package/dist/common/types.d.cts +6 -4
  15. package/dist/common/types.d.ts +6 -4
  16. package/dist/common/uris/guess/utils.cjs +2 -1
  17. package/dist/common/uris/guess/utils.js +2 -1
  18. package/dist/common/uris/headers/index.cjs +2 -1
  19. package/dist/common/uris/headers/index.js +2 -1
  20. package/dist/common/utils.cjs +10 -3
  21. package/dist/common/utils.d.cts +2 -1
  22. package/dist/common/utils.d.ts +2 -1
  23. package/dist/common/utils.js +9 -3
  24. package/dist/favicons/extractors.cjs +16 -4
  25. package/dist/favicons/extractors.js +16 -4
  26. package/dist/favicons/index.cjs +3 -2
  27. package/dist/favicons/index.js +5 -4
  28. package/dist/favicons/platform/handlers/bluesky.cjs +3 -3
  29. package/dist/favicons/platform/handlers/bluesky.js +3 -3
  30. package/dist/favicons/platform/handlers/mastodon.cjs +7 -5
  31. package/dist/favicons/platform/handlers/mastodon.js +7 -5
  32. package/dist/favicons/platform/handlers/reddit.cjs +5 -6
  33. package/dist/favicons/platform/handlers/reddit.js +5 -6
  34. package/dist/favicons/utils.cjs +10 -0
  35. package/dist/favicons/utils.js +9 -0
  36. package/dist/feeds/defaults.cjs +2 -0
  37. package/dist/feeds/defaults.js +2 -0
  38. package/dist/feeds/extractors.cjs +10 -8
  39. package/dist/feeds/extractors.js +10 -8
  40. package/dist/feeds/index.cjs +2 -2
  41. package/dist/feeds/index.js +3 -3
  42. package/dist/feeds/platform/handlers/blogspot.cjs +2 -1
  43. package/dist/feeds/platform/handlers/blogspot.js +2 -1
  44. package/dist/feeds/platform/handlers/bluesky.cjs +2 -1
  45. package/dist/feeds/platform/handlers/bluesky.js +2 -1
  46. package/dist/feeds/platform/handlers/csdn.cjs +2 -1
  47. package/dist/feeds/platform/handlers/csdn.js +2 -1
  48. package/dist/feeds/platform/handlers/deviantart.cjs +8 -4
  49. package/dist/feeds/platform/handlers/deviantart.js +8 -4
  50. package/dist/feeds/platform/handlers/douban.cjs +4 -2
  51. package/dist/feeds/platform/handlers/douban.js +4 -2
  52. package/dist/feeds/platform/handlers/github.cjs +12 -6
  53. package/dist/feeds/platform/handlers/github.js +12 -6
  54. package/dist/feeds/platform/handlers/githubGist.cjs +6 -3
  55. package/dist/feeds/platform/handlers/githubGist.js +6 -3
  56. package/dist/feeds/platform/handlers/gitlab.cjs +15 -2
  57. package/dist/feeds/platform/handlers/gitlab.js +16 -3
  58. package/dist/feeds/platform/handlers/lemmy.cjs +46 -0
  59. package/dist/feeds/platform/handlers/lemmy.js +46 -0
  60. package/dist/feeds/platform/handlers/mastodon.cjs +5 -3
  61. package/dist/feeds/platform/handlers/mastodon.js +5 -3
  62. package/dist/feeds/platform/handlers/medium.cjs +10 -5
  63. package/dist/feeds/platform/handlers/medium.js +10 -5
  64. package/dist/feeds/platform/handlers/reddit.cjs +10 -5
  65. package/dist/feeds/platform/handlers/reddit.js +10 -5
  66. package/dist/feeds/platform/handlers/soundcloud.cjs +3 -2
  67. package/dist/feeds/platform/handlers/soundcloud.js +3 -2
  68. package/dist/feeds/platform/handlers/stackExchange.cjs +6 -3
  69. package/dist/feeds/platform/handlers/stackExchange.js +6 -3
  70. package/dist/feeds/platform/handlers/steam.cjs +4 -2
  71. package/dist/feeds/platform/handlers/steam.js +4 -2
  72. package/dist/feeds/platform/handlers/v2ex.cjs +4 -2
  73. package/dist/feeds/platform/handlers/v2ex.js +4 -2
  74. package/dist/feeds/platform/handlers/vimeo.cjs +3 -2
  75. package/dist/feeds/platform/handlers/vimeo.js +3 -2
  76. package/dist/feeds/platform/handlers/ximalaya.cjs +2 -1
  77. package/dist/feeds/platform/handlers/ximalaya.js +2 -1
  78. package/dist/feeds/platform/handlers/youtube.cjs +10 -9
  79. package/dist/feeds/platform/handlers/youtube.js +10 -9
  80. package/dist/hubs/discover/index.cjs +4 -4
  81. package/dist/hubs/discover/index.js +5 -5
  82. package/dist/hubs/discover/types.d.cts +2 -2
  83. package/dist/hubs/discover/types.d.ts +2 -2
  84. package/dist/hubs/feed/index.cjs +7 -5
  85. package/dist/hubs/feed/index.js +7 -5
  86. package/dist/hubs/headers/index.cjs +3 -3
  87. package/dist/hubs/headers/index.js +4 -4
  88. package/dist/hubs/html/index.cjs +3 -3
  89. package/dist/hubs/html/index.js +4 -4
  90. package/dist/index.cjs +3 -0
  91. package/dist/index.d.cts +3 -2
  92. package/dist/index.d.ts +3 -2
  93. package/dist/index.js +2 -1
  94. package/dist/utils.cjs +1 -0
  95. package/dist/utils.d.cts +2 -2
  96. package/dist/utils.d.ts +2 -2
  97. package/dist/utils.js +2 -2
  98. package/package.json +5 -5
package/README.md CHANGED
@@ -22,6 +22,7 @@ Finds feeds by scanning links and anchors in HTML content, parsing HTTP headers,
22
22
  | --- | --- |
23
23
  | Feeds | RSS, Atom, JSON Feed, and RDF. Each feed is validated and returns metadata like format, title, description, and site URL. |
24
24
  | Blogrolls | OPML files containing feed subscriptions. Validated and returns title. |
25
+ | Favicons | Site icons from HTML, feeds, platform APIs, and common paths. Validated to ensure URLs point to actual images. |
25
26
  | WebSub hubs | Find hubs for real-time feed update notifications. |
26
27
 
27
28
  ### Discovery Methods
@@ -144,6 +145,19 @@ const blogrolls = await discoverBlogrolls('https://example.com')
144
145
  // }]
145
146
  ```
146
147
 
148
+ ### Discover Favicons
149
+
150
+ ```typescript
151
+ import { discoverFavicons } from 'feedscout'
152
+
153
+ const favicons = await discoverFavicons('https://example.com')
154
+
155
+ // [{
156
+ // url: 'https://example.com/apple-touch-icon.png',
157
+ // isValid: true,
158
+ // }]
159
+ ```
160
+
147
161
  ### Discover WebSub Hubs
148
162
 
149
163
  ```typescript
@@ -1,6 +1,6 @@
1
1
  let feedsmith = require("feedsmith");
2
2
  //#region src/blogrolls/extractors.ts
3
- const defaultExtractor = async ({ content, url }) => {
3
+ const defaultExtractor = ({ content, url }) => {
4
4
  if (!content) return {
5
5
  url,
6
6
  isValid: false
@@ -1,6 +1,6 @@
1
1
  import { parseOpml } from "feedsmith";
2
2
  //#region src/blogrolls/extractors.ts
3
- const defaultExtractor = async ({ content, url }) => {
3
+ const defaultExtractor = ({ content, url }) => {
4
4
  if (!content) return {
5
5
  url,
6
6
  isValid: false
@@ -4,7 +4,7 @@ const require_index = require("../common/discover/index.cjs");
4
4
  const require_defaults = require("./defaults.cjs");
5
5
  const require_extractors = require("./extractors.cjs");
6
6
  //#region src/blogrolls/index.ts
7
- const discoverBlogrolls = async (input, options = {}) => {
7
+ const discoverBlogrolls = (input, options = {}) => {
8
8
  return require_index.discover(input, {
9
9
  ...options,
10
10
  methods: options.methods ?? [
@@ -14,7 +14,8 @@ const discoverBlogrolls = async (input, options = {}) => {
14
14
  ],
15
15
  fetchFn: options.fetchFn ?? require_utils$1.defaultFetchFn,
16
16
  extractFn: options.extractFn ?? require_extractors.defaultExtractor,
17
- normalizeUrlFn: options.normalizeUrlFn ?? require_utils.normalizeUrl
17
+ resolveUrlFn: options.resolveUrlFn ?? require_utils.resolveUrl,
18
+ resolveSiteUrlFn: options.resolveSiteUrlFn ?? require_utils$1.defaultResolveSiteUrlFn
18
19
  }, {
19
20
  html: require_defaults.defaultHtmlOptions,
20
21
  headers: require_defaults.defaultHeadersOptions,
@@ -1,10 +1,10 @@
1
- import { normalizeUrl } from "../common/utils.js";
2
- import { defaultFetchFn } from "../common/discover/utils.js";
1
+ import { resolveUrl } from "../common/utils.js";
2
+ import { defaultFetchFn, defaultResolveSiteUrlFn } from "../common/discover/utils.js";
3
3
  import { discover } from "../common/discover/index.js";
4
4
  import { defaultGuessOptions, defaultHeadersOptions, defaultHtmlOptions } from "./defaults.js";
5
5
  import { defaultExtractor } from "./extractors.js";
6
6
  //#region src/blogrolls/index.ts
7
- const discoverBlogrolls = async (input, options = {}) => {
7
+ const discoverBlogrolls = (input, options = {}) => {
8
8
  return discover(input, {
9
9
  ...options,
10
10
  methods: options.methods ?? [
@@ -14,7 +14,8 @@ const discoverBlogrolls = async (input, options = {}) => {
14
14
  ],
15
15
  fetchFn: options.fetchFn ?? defaultFetchFn,
16
16
  extractFn: options.extractFn ?? defaultExtractor,
17
- normalizeUrlFn: options.normalizeUrlFn ?? normalizeUrl
17
+ resolveUrlFn: options.resolveUrlFn ?? resolveUrl,
18
+ resolveSiteUrlFn: options.resolveSiteUrlFn ?? defaultResolveSiteUrlFn
18
19
  }, {
19
20
  html: defaultHtmlOptions,
20
21
  headers: defaultHeadersOptions,
@@ -4,24 +4,36 @@ const require_index = require("../uris/index.cjs");
4
4
  const require_utils$1 = require("./utils.cjs");
5
5
  //#region src/common/discover/index.ts
6
6
  const discover = async (input, options, defaults) => {
7
- const { methods, fetchFn, extractFn, normalizeUrlFn, stopOnFirstMethod = false, stopOnFirstResult = false, concurrency = 3, includeInvalid = false, onProgress } = options;
8
- const normalizedInput = await require_utils$1.normalizeInput(input, fetchFn);
9
- if (normalizedInput.content) {
7
+ const { methods, fetchFn, extractFn, resolveUrlFn, resolveSiteUrlFn, stopOnFirstMethod = false, stopOnFirstResult = false, concurrency = 3, includeInvalid = false, onProgress } = options;
8
+ const sourceInput = await require_utils$1.normalizeInput(input, fetchFn);
9
+ if (sourceInput.content) {
10
10
  const result = await extractFn({
11
- url: normalizedInput.url,
12
- content: normalizedInput.content,
13
- headers: normalizedInput.headers
11
+ url: sourceInput.url,
12
+ content: sourceInput.content,
13
+ headers: sourceInput.headers
14
14
  });
15
15
  if (result.isValid) return [result];
16
16
  }
17
- const urisByMethod = await require_index.discoverUris(require_utils$1.normalizeMethodsConfig(normalizedInput, methods, defaults), fetchFn);
17
+ let siteInput;
18
+ if (resolveSiteUrlFn) {
19
+ const siteUrl = resolveSiteUrlFn(sourceInput);
20
+ if (siteUrl) try {
21
+ const response = await fetchFn(siteUrl);
22
+ siteInput = {
23
+ url: response.url,
24
+ content: typeof response.body === "string" ? response.body : "",
25
+ headers: response.headers
26
+ };
27
+ } catch {}
28
+ }
29
+ const urisByMethod = await require_index.discoverUris(require_utils$1.normalizeMethodsConfig(sourceInput, siteInput, methods, defaults), fetchFn);
18
30
  const seen = /* @__PURE__ */ new Set();
19
31
  const methodGroups = [];
20
32
  for (const method of require_types.discoverMethodOrder) {
21
33
  const rawUris = urisByMethod[method];
22
- if (!rawUris?.length) continue;
34
+ if (!rawUris || rawUris.length === 0) continue;
23
35
  const unique = rawUris.map((entry) => {
24
- return require_utils$1.normalizeUriEntry(entry, normalizeUrlFn, normalizedInput.url);
36
+ return require_utils$1.normalizeUriEntry(entry, resolveUrlFn, sourceInput.url);
25
37
  }).filter((entry) => {
26
38
  const key = typeof entry.uri === "string" ? entry.uri : entry.uri.join("\0");
27
39
  if (seen.has(key)) return false;
@@ -4,24 +4,36 @@ import { discoverUris } from "../uris/index.js";
4
4
  import { normalizeInput, normalizeMethodsConfig, normalizeUriEntry } from "./utils.js";
5
5
  //#region src/common/discover/index.ts
6
6
  const discover = async (input, options, defaults) => {
7
- const { methods, fetchFn, extractFn, normalizeUrlFn, stopOnFirstMethod = false, stopOnFirstResult = false, concurrency = 3, includeInvalid = false, onProgress } = options;
8
- const normalizedInput = await normalizeInput(input, fetchFn);
9
- if (normalizedInput.content) {
7
+ const { methods, fetchFn, extractFn, resolveUrlFn, resolveSiteUrlFn, stopOnFirstMethod = false, stopOnFirstResult = false, concurrency = 3, includeInvalid = false, onProgress } = options;
8
+ const sourceInput = await normalizeInput(input, fetchFn);
9
+ if (sourceInput.content) {
10
10
  const result = await extractFn({
11
- url: normalizedInput.url,
12
- content: normalizedInput.content,
13
- headers: normalizedInput.headers
11
+ url: sourceInput.url,
12
+ content: sourceInput.content,
13
+ headers: sourceInput.headers
14
14
  });
15
15
  if (result.isValid) return [result];
16
16
  }
17
- const urisByMethod = await discoverUris(normalizeMethodsConfig(normalizedInput, methods, defaults), fetchFn);
17
+ let siteInput;
18
+ if (resolveSiteUrlFn) {
19
+ const siteUrl = resolveSiteUrlFn(sourceInput);
20
+ if (siteUrl) try {
21
+ const response = await fetchFn(siteUrl);
22
+ siteInput = {
23
+ url: response.url,
24
+ content: typeof response.body === "string" ? response.body : "",
25
+ headers: response.headers
26
+ };
27
+ } catch {}
28
+ }
29
+ const urisByMethod = await discoverUris(normalizeMethodsConfig(sourceInput, siteInput, methods, defaults), fetchFn);
18
30
  const seen = /* @__PURE__ */ new Set();
19
31
  const methodGroups = [];
20
32
  for (const method of discoverMethodOrder) {
21
33
  const rawUris = urisByMethod[method];
22
- if (!rawUris?.length) continue;
34
+ if (!rawUris || rawUris.length === 0) continue;
23
35
  const unique = rawUris.map((entry) => {
24
- return normalizeUriEntry(entry, normalizeUrlFn, normalizedInput.url);
36
+ return normalizeUriEntry(entry, resolveUrlFn, sourceInput.url);
25
37
  }).filter((entry) => {
26
38
  const key = typeof entry.uri === "string" ? entry.uri : entry.uri.join("\0");
27
39
  if (seen.has(key)) return false;
@@ -1,4 +1,6 @@
1
1
  const require_locales = require("../locales.cjs");
2
+ const require_utils = require("../utils.cjs");
3
+ let feedsmith = require("feedsmith");
2
4
  //#region src/common/discover/utils.ts
3
5
  const defaultFetchFn = async (url, options) => {
4
6
  const response = await fetch(url, {
@@ -22,37 +24,59 @@ const normalizeInput = async (input, fetchFn) => {
22
24
  headers: response.headers
23
25
  };
24
26
  };
25
- const normalizeUriEntry = (entry, normalizeUrlFn, baseUrl) => {
27
+ const getLinkOfType = (links, rel) => {
28
+ return links?.find((link) => link.rel === rel);
29
+ };
30
+ const getFeedSiteUrl = (parsed) => {
31
+ const { format, feed } = parsed;
32
+ if (format === "rss" || format === "rdf") return getLinkOfType(feed.atom?.links, "alternate")?.href ?? feed.link;
33
+ if (format === "atom") return getLinkOfType(feed.links, "alternate")?.href;
34
+ if (format === "json") return feed.home_page_url;
35
+ };
36
+ const defaultResolveSiteUrlFn = (input) => {
37
+ if (!input.content) return;
38
+ try {
39
+ let siteUrl = getFeedSiteUrl((0, feedsmith.parseFeed)(input.content));
40
+ if (!siteUrl) try {
41
+ siteUrl = new URL(input.url).origin;
42
+ } catch {}
43
+ else siteUrl = require_utils.resolveUrl(siteUrl, input.url);
44
+ if (siteUrl && new URL(siteUrl).href === new URL(input.url).href) return;
45
+ return siteUrl;
46
+ } catch {}
47
+ };
48
+ const normalizeUriEntry = (entry, resolveUrlFn, baseUrl) => {
26
49
  if (typeof entry.uri === "string") return {
27
50
  ...entry,
28
- uri: normalizeUrlFn(entry.uri, baseUrl)
51
+ uri: resolveUrlFn(entry.uri, baseUrl)
29
52
  };
30
53
  return {
31
54
  ...entry,
32
- uri: entry.uri.map((uri) => normalizeUrlFn(uri, baseUrl))
55
+ uri: entry.uri.map((uri) => resolveUrlFn(uri, baseUrl))
33
56
  };
34
57
  };
35
- const normalizeMethodsConfig = (input, methods, defaults) => {
58
+ const normalizeMethodsConfig = (sourceInput, siteInput, methods, defaults) => {
59
+ const resolvedInput = siteInput ?? sourceInput;
36
60
  const methodsObj = Array.isArray(methods) ? Object.fromEntries(methods.map((method) => [method, true])) : methods;
37
61
  const methodsConfig = {};
38
62
  if (methodsObj.platform && defaults.platform) {
39
- if (!input.url || input.url === "") throw new Error(require_locales.errors.platformMethodRequiresUrl);
63
+ if (!resolvedInput.url || resolvedInput.url === "") throw new Error(require_locales.errors.platformMethodRequiresUrl);
40
64
  const platformOptions = methodsObj.platform === true ? {} : methodsObj.platform;
41
65
  methodsConfig.platform = {
42
- content: input.content,
43
- headers: input.headers,
66
+ content: resolvedInput.content,
67
+ headers: resolvedInput.headers,
44
68
  options: {
45
69
  ...defaults.platform,
46
70
  ...platformOptions,
47
- baseUrl: input.url
71
+ baseUrl: resolvedInput.url
48
72
  }
49
73
  };
50
74
  }
51
75
  if (methodsObj.feed && defaults.feed) {
52
- if (input.content === void 0) throw new Error(require_locales.errors.feedMethodRequiresContent);
76
+ if (sourceInput.content === void 0) throw new Error(require_locales.errors.feedMethodRequiresContent);
53
77
  const feedOptions = methodsObj.feed === true ? {} : methodsObj.feed;
54
78
  methodsConfig.feed = {
55
- content: input.content,
79
+ content: sourceInput.content,
56
80
  options: {
57
81
  ...defaults.feed,
58
82
  ...feedOptions
@@ -60,42 +84,44 @@ const normalizeMethodsConfig = (input, methods, defaults) => {
60
84
  };
61
85
  }
62
86
  if (methodsObj.html && defaults.html) {
63
- if (input.content === void 0) throw new Error(require_locales.errors.htmlMethodRequiresContent);
87
+ if (resolvedInput.content === void 0) throw new Error(require_locales.errors.htmlMethodRequiresContent);
64
88
  const htmlOptions = methodsObj.html === true ? {} : methodsObj.html;
65
89
  methodsConfig.html = {
66
- html: input.content,
90
+ html: resolvedInput.content,
67
91
  options: {
68
92
  ...defaults.html,
69
93
  ...htmlOptions,
70
- baseUrl: input.url
94
+ baseUrl: resolvedInput.url
71
95
  }
72
96
  };
73
97
  }
74
98
  if (methodsObj.headers && defaults.headers) {
75
- if (input.headers === void 0) throw new Error(require_locales.errors.headersMethodRequiresHeaders);
99
+ if (resolvedInput.headers === void 0) throw new Error(require_locales.errors.headersMethodRequiresHeaders);
76
100
  const headersOptions = methodsObj.headers === true ? {} : methodsObj.headers;
77
101
  methodsConfig.headers = {
78
- headers: input.headers,
102
+ headers: resolvedInput.headers,
79
103
  options: {
80
104
  ...defaults.headers,
81
105
  ...headersOptions,
82
- baseUrl: input.url
106
+ baseUrl: resolvedInput.url
83
107
  }
84
108
  };
85
109
  }
86
110
  if (methodsObj.guess && defaults.guess) {
87
- if (!input.url || input.url === "") throw new Error(require_locales.errors.guessMethodRequiresUrl);
111
+ if (!resolvedInput.url || resolvedInput.url === "") throw new Error(require_locales.errors.guessMethodRequiresUrl);
88
112
  const guessOptions = methodsObj.guess === true ? {} : methodsObj.guess;
89
113
  methodsConfig.guess = { options: {
90
114
  ...defaults.guess,
91
115
  ...guessOptions,
92
- baseUrl: input.url
116
+ baseUrl: resolvedInput.url
93
117
  } };
94
118
  }
95
119
  return methodsConfig;
96
120
  };
97
121
  //#endregion
98
122
  exports.defaultFetchFn = defaultFetchFn;
123
+ exports.defaultResolveSiteUrlFn = defaultResolveSiteUrlFn;
124
+ exports.getFeedSiteUrl = getFeedSiteUrl;
99
125
  exports.normalizeInput = normalizeInput;
100
126
  exports.normalizeMethodsConfig = normalizeMethodsConfig;
101
127
  exports.normalizeUriEntry = normalizeUriEntry;
@@ -0,0 +1,8 @@
1
+ import { DiscoverResolveSiteUrlFn } from "../types.cjs";
2
+ import { parseFeed } from "feedsmith";
3
+
4
+ //#region src/common/discover/utils.d.ts
5
+ declare const getFeedSiteUrl: (parsed: ReturnType<typeof parseFeed>) => string | undefined;
6
+ declare const defaultResolveSiteUrlFn: DiscoverResolveSiteUrlFn;
7
+ //#endregion
8
+ export { defaultResolveSiteUrlFn, getFeedSiteUrl };
@@ -0,0 +1,8 @@
1
+ import { DiscoverResolveSiteUrlFn } from "../types.js";
2
+ import { parseFeed } from "feedsmith";
3
+
4
+ //#region src/common/discover/utils.d.ts
5
+ declare const getFeedSiteUrl: (parsed: ReturnType<typeof parseFeed>) => string | undefined;
6
+ declare const defaultResolveSiteUrlFn: DiscoverResolveSiteUrlFn;
7
+ //#endregion
8
+ export { defaultResolveSiteUrlFn, getFeedSiteUrl };
@@ -1,4 +1,6 @@
1
1
  import { errors } from "../locales.js";
2
+ import { resolveUrl } from "../utils.js";
3
+ import { parseFeed } from "feedsmith";
2
4
  //#region src/common/discover/utils.ts
3
5
  const defaultFetchFn = async (url, options) => {
4
6
  const response = await fetch(url, {
@@ -22,37 +24,59 @@ const normalizeInput = async (input, fetchFn) => {
22
24
  headers: response.headers
23
25
  };
24
26
  };
25
- const normalizeUriEntry = (entry, normalizeUrlFn, baseUrl) => {
27
+ const getLinkOfType = (links, rel) => {
28
+ return links?.find((link) => link.rel === rel);
29
+ };
30
+ const getFeedSiteUrl = (parsed) => {
31
+ const { format, feed } = parsed;
32
+ if (format === "rss" || format === "rdf") return getLinkOfType(feed.atom?.links, "alternate")?.href ?? feed.link;
33
+ if (format === "atom") return getLinkOfType(feed.links, "alternate")?.href;
34
+ if (format === "json") return feed.home_page_url;
35
+ };
36
+ const defaultResolveSiteUrlFn = (input) => {
37
+ if (!input.content) return;
38
+ try {
39
+ let siteUrl = getFeedSiteUrl(parseFeed(input.content));
40
+ if (!siteUrl) try {
41
+ siteUrl = new URL(input.url).origin;
42
+ } catch {}
43
+ else siteUrl = resolveUrl(siteUrl, input.url);
44
+ if (siteUrl && new URL(siteUrl).href === new URL(input.url).href) return;
45
+ return siteUrl;
46
+ } catch {}
47
+ };
48
+ const normalizeUriEntry = (entry, resolveUrlFn, baseUrl) => {
26
49
  if (typeof entry.uri === "string") return {
27
50
  ...entry,
28
- uri: normalizeUrlFn(entry.uri, baseUrl)
51
+ uri: resolveUrlFn(entry.uri, baseUrl)
29
52
  };
30
53
  return {
31
54
  ...entry,
32
- uri: entry.uri.map((uri) => normalizeUrlFn(uri, baseUrl))
55
+ uri: entry.uri.map((uri) => resolveUrlFn(uri, baseUrl))
33
56
  };
34
57
  };
35
- const normalizeMethodsConfig = (input, methods, defaults) => {
58
+ const normalizeMethodsConfig = (sourceInput, siteInput, methods, defaults) => {
59
+ const resolvedInput = siteInput ?? sourceInput;
36
60
  const methodsObj = Array.isArray(methods) ? Object.fromEntries(methods.map((method) => [method, true])) : methods;
37
61
  const methodsConfig = {};
38
62
  if (methodsObj.platform && defaults.platform) {
39
- if (!input.url || input.url === "") throw new Error(errors.platformMethodRequiresUrl);
63
+ if (!resolvedInput.url || resolvedInput.url === "") throw new Error(errors.platformMethodRequiresUrl);
40
64
  const platformOptions = methodsObj.platform === true ? {} : methodsObj.platform;
41
65
  methodsConfig.platform = {
42
- content: input.content,
43
- headers: input.headers,
66
+ content: resolvedInput.content,
67
+ headers: resolvedInput.headers,
44
68
  options: {
45
69
  ...defaults.platform,
46
70
  ...platformOptions,
47
- baseUrl: input.url
71
+ baseUrl: resolvedInput.url
48
72
  }
49
73
  };
50
74
  }
51
75
  if (methodsObj.feed && defaults.feed) {
52
- if (input.content === void 0) throw new Error(errors.feedMethodRequiresContent);
76
+ if (sourceInput.content === void 0) throw new Error(errors.feedMethodRequiresContent);
53
77
  const feedOptions = methodsObj.feed === true ? {} : methodsObj.feed;
54
78
  methodsConfig.feed = {
55
- content: input.content,
79
+ content: sourceInput.content,
56
80
  options: {
57
81
  ...defaults.feed,
58
82
  ...feedOptions
@@ -60,39 +84,39 @@ const normalizeMethodsConfig = (input, methods, defaults) => {
60
84
  };
61
85
  }
62
86
  if (methodsObj.html && defaults.html) {
63
- if (input.content === void 0) throw new Error(errors.htmlMethodRequiresContent);
87
+ if (resolvedInput.content === void 0) throw new Error(errors.htmlMethodRequiresContent);
64
88
  const htmlOptions = methodsObj.html === true ? {} : methodsObj.html;
65
89
  methodsConfig.html = {
66
- html: input.content,
90
+ html: resolvedInput.content,
67
91
  options: {
68
92
  ...defaults.html,
69
93
  ...htmlOptions,
70
- baseUrl: input.url
94
+ baseUrl: resolvedInput.url
71
95
  }
72
96
  };
73
97
  }
74
98
  if (methodsObj.headers && defaults.headers) {
75
- if (input.headers === void 0) throw new Error(errors.headersMethodRequiresHeaders);
99
+ if (resolvedInput.headers === void 0) throw new Error(errors.headersMethodRequiresHeaders);
76
100
  const headersOptions = methodsObj.headers === true ? {} : methodsObj.headers;
77
101
  methodsConfig.headers = {
78
- headers: input.headers,
102
+ headers: resolvedInput.headers,
79
103
  options: {
80
104
  ...defaults.headers,
81
105
  ...headersOptions,
82
- baseUrl: input.url
106
+ baseUrl: resolvedInput.url
83
107
  }
84
108
  };
85
109
  }
86
110
  if (methodsObj.guess && defaults.guess) {
87
- if (!input.url || input.url === "") throw new Error(errors.guessMethodRequiresUrl);
111
+ if (!resolvedInput.url || resolvedInput.url === "") throw new Error(errors.guessMethodRequiresUrl);
88
112
  const guessOptions = methodsObj.guess === true ? {} : methodsObj.guess;
89
113
  methodsConfig.guess = { options: {
90
114
  ...defaults.guess,
91
115
  ...guessOptions,
92
- baseUrl: input.url
116
+ baseUrl: resolvedInput.url
93
117
  } };
94
118
  }
95
119
  return methodsConfig;
96
120
  };
97
121
  //#endregion
98
- export { defaultFetchFn, normalizeInput, normalizeMethodsConfig, normalizeUriEntry };
122
+ export { defaultFetchFn, defaultResolveSiteUrlFn, getFeedSiteUrl, normalizeInput, normalizeMethodsConfig, normalizeUriEntry };
@@ -118,7 +118,9 @@ var hints = {
118
118
  "v2ex:node": "Node",
119
119
  "v2ex:member": "Member",
120
120
  "v2ex:tab": "Tab",
121
- "ximalaya:album": "Album"
121
+ "ximalaya:album": "Album",
122
+ "lemmy:community": "Community",
123
+ "lemmy:user": "User"
122
124
  };
123
125
  //#endregion
124
126
  Object.defineProperty(exports, "errors", {
@@ -118,7 +118,9 @@ var hints = {
118
118
  "v2ex:node": "Node",
119
119
  "v2ex:member": "Member",
120
120
  "v2ex:tab": "Tab",
121
- "ximalaya:album": "Album"
121
+ "ximalaya:album": "Album",
122
+ "lemmy:community": "Community",
123
+ "lemmy:user": "User"
122
124
  };
123
125
  //#endregion
124
126
  export { errors, hints };
@@ -20,7 +20,8 @@ type LinkSelector = {
20
20
  rel: string;
21
21
  types?: Array<string>;
22
22
  };
23
- type DiscoverNormalizeUrlFn = (url: string, baseUrl: string | undefined) => string;
23
+ type DiscoverResolveUrlFn = (url: string, baseUrl: string | undefined) => string;
24
+ type DiscoverResolveSiteUrlFn = (input: DiscoverInputObject) => string | undefined;
24
25
  type DiscoverFetchFnOptions = {
25
26
  method?: 'GET' | 'HEAD';
26
27
  headers?: Record<string, string>;
@@ -57,7 +58,7 @@ type DiscoverExtractFn<TValid> = (input: {
57
58
  content: string;
58
59
  headers?: Headers;
59
60
  status?: number;
60
- }) => Promise<DiscoverResult<TValid>>;
61
+ }) => Promise<DiscoverResult<TValid>> | DiscoverResult<TValid>;
61
62
  type DiscoverInputObject = {
62
63
  url: string;
63
64
  content?: string;
@@ -75,7 +76,8 @@ type DiscoverOptions<TValid, TMethods extends DiscoverMethod = DiscoverMethod> =
75
76
  methods?: DiscoverMethodsConfig<TMethods>;
76
77
  fetchFn?: DiscoverFetchFn;
77
78
  extractFn?: DiscoverExtractFn<TValid>;
78
- normalizeUrlFn?: DiscoverNormalizeUrlFn;
79
+ resolveUrlFn?: DiscoverResolveUrlFn;
80
+ resolveSiteUrlFn?: DiscoverResolveSiteUrlFn;
79
81
  stopOnFirstMethod?: boolean;
80
82
  stopOnFirstResult?: boolean;
81
83
  concurrency?: number;
@@ -83,4 +85,4 @@ type DiscoverOptions<TValid, TMethods extends DiscoverMethod = DiscoverMethod> =
83
85
  includeInvalid?: boolean;
84
86
  };
85
87
  //#endregion
86
- export { DiscoverExtractFn, DiscoverFetchFn, DiscoverFetchFnOptions, DiscoverFetchFnResponse, DiscoverInput, DiscoverInputObject, DiscoverMethod, DiscoverMethodsConfig, DiscoverNormalizeUrlFn, DiscoverOnProgressFn, DiscoverOptions, DiscoverProgress, DiscoverResult, DiscoverUriEntry, DiscoverUriHint, LinkSelector, UriEntry };
88
+ export { DiscoverExtractFn, DiscoverFetchFn, DiscoverFetchFnOptions, DiscoverFetchFnResponse, DiscoverInput, DiscoverInputObject, DiscoverMethod, DiscoverMethodsConfig, DiscoverOnProgressFn, DiscoverOptions, DiscoverProgress, DiscoverResolveSiteUrlFn, DiscoverResolveUrlFn, DiscoverResult, DiscoverUriEntry, DiscoverUriHint, LinkSelector, UriEntry };
@@ -20,7 +20,8 @@ type LinkSelector = {
20
20
  rel: string;
21
21
  types?: Array<string>;
22
22
  };
23
- type DiscoverNormalizeUrlFn = (url: string, baseUrl: string | undefined) => string;
23
+ type DiscoverResolveUrlFn = (url: string, baseUrl: string | undefined) => string;
24
+ type DiscoverResolveSiteUrlFn = (input: DiscoverInputObject) => string | undefined;
24
25
  type DiscoverFetchFnOptions = {
25
26
  method?: 'GET' | 'HEAD';
26
27
  headers?: Record<string, string>;
@@ -57,7 +58,7 @@ type DiscoverExtractFn<TValid> = (input: {
57
58
  content: string;
58
59
  headers?: Headers;
59
60
  status?: number;
60
- }) => Promise<DiscoverResult<TValid>>;
61
+ }) => Promise<DiscoverResult<TValid>> | DiscoverResult<TValid>;
61
62
  type DiscoverInputObject = {
62
63
  url: string;
63
64
  content?: string;
@@ -75,7 +76,8 @@ type DiscoverOptions<TValid, TMethods extends DiscoverMethod = DiscoverMethod> =
75
76
  methods?: DiscoverMethodsConfig<TMethods>;
76
77
  fetchFn?: DiscoverFetchFn;
77
78
  extractFn?: DiscoverExtractFn<TValid>;
78
- normalizeUrlFn?: DiscoverNormalizeUrlFn;
79
+ resolveUrlFn?: DiscoverResolveUrlFn;
80
+ resolveSiteUrlFn?: DiscoverResolveSiteUrlFn;
79
81
  stopOnFirstMethod?: boolean;
80
82
  stopOnFirstResult?: boolean;
81
83
  concurrency?: number;
@@ -83,4 +85,4 @@ type DiscoverOptions<TValid, TMethods extends DiscoverMethod = DiscoverMethod> =
83
85
  includeInvalid?: boolean;
84
86
  };
85
87
  //#endregion
86
- export { DiscoverExtractFn, DiscoverFetchFn, DiscoverFetchFnOptions, DiscoverFetchFnResponse, DiscoverInput, DiscoverInputObject, DiscoverMethod, DiscoverMethodsConfig, DiscoverNormalizeUrlFn, DiscoverOnProgressFn, DiscoverOptions, DiscoverProgress, DiscoverResult, DiscoverUriEntry, DiscoverUriHint, LinkSelector, UriEntry };
88
+ export { DiscoverExtractFn, DiscoverFetchFn, DiscoverFetchFnOptions, DiscoverFetchFnResponse, DiscoverInput, DiscoverInputObject, DiscoverMethod, DiscoverMethodsConfig, DiscoverOnProgressFn, DiscoverOptions, DiscoverProgress, DiscoverResolveSiteUrlFn, DiscoverResolveUrlFn, DiscoverResult, DiscoverUriEntry, DiscoverUriHint, LinkSelector, UriEntry };
@@ -1,4 +1,5 @@
1
1
  //#region src/common/uris/guess/utils.ts
2
+ const ipAddressRegex = /^\d+\.\d+\.\d+\.\d+$/;
2
3
  const resolveUri = (uri, base, origin, pathname) => {
3
4
  if (uri.startsWith("/")) return `${origin}${uri}`;
4
5
  if (uri.startsWith("?")) return `${origin}${pathname}${uri}`;
@@ -24,7 +25,7 @@ const getWwwCounterpart = (baseUrl) => {
24
25
  const getSubdomainVariants = (baseUrl, prefixes) => {
25
26
  const url = new URL(baseUrl);
26
27
  const hostname = url.hostname;
27
- const isIpAddress = /^\d+\.\d+\.\d+\.\d+$/.test(hostname);
28
+ const isIpAddress = ipAddressRegex.test(hostname);
28
29
  if (hostname === "localhost" || isIpAddress) return [];
29
30
  const hostnameParts = hostname.split(".");
30
31
  if (hostnameParts.length < 2) return [];
@@ -1,4 +1,5 @@
1
1
  //#region src/common/uris/guess/utils.ts
2
+ const ipAddressRegex = /^\d+\.\d+\.\d+\.\d+$/;
2
3
  const resolveUri = (uri, base, origin, pathname) => {
3
4
  if (uri.startsWith("/")) return `${origin}${uri}`;
4
5
  if (uri.startsWith("?")) return `${origin}${pathname}${uri}`;
@@ -24,7 +25,7 @@ const getWwwCounterpart = (baseUrl) => {
24
25
  const getSubdomainVariants = (baseUrl, prefixes) => {
25
26
  const url = new URL(baseUrl);
26
27
  const hostname = url.hostname;
27
- const isIpAddress = /^\d+\.\d+\.\d+\.\d+$/.test(hostname);
28
+ const isIpAddress = ipAddressRegex.test(hostname);
28
29
  if (hostname === "localhost" || isIpAddress) return [];
29
30
  const hostnameParts = hostname.split(".");
30
31
  if (hostnameParts.length < 2) return [];
@@ -3,11 +3,12 @@ const require_utils = require("../../utils.cjs");
3
3
  const urlRegex = /<([^<>]+)>/;
4
4
  const relRegex = /rel\s*=\s*["']?([^"';,]+)["']?/i;
5
5
  const typeRegex = /type\s*=\s*["']?([^"';,]+)["']?/i;
6
+ const linkSplitRegex = /,(?=\s*<)/;
6
7
  const discoverUrisFromHeaders = (headers, options) => {
7
8
  const uris = /* @__PURE__ */ new Set();
8
9
  const linkHeader = headers.get("link");
9
10
  if (!linkHeader) return [];
10
- const links = linkHeader.split(/,(?=\s*<)/);
11
+ const links = linkHeader.split(linkSplitRegex);
11
12
  for (const link of links) {
12
13
  const urlMatch = link.match(urlRegex);
13
14
  const relMatch = link.match(relRegex);
@@ -3,11 +3,12 @@ import { matchesAnyOfLinkSelectors } from "../../utils.js";
3
3
  const urlRegex = /<([^<>]+)>/;
4
4
  const relRegex = /rel\s*=\s*["']?([^"';,]+)["']?/i;
5
5
  const typeRegex = /type\s*=\s*["']?([^"';,]+)["']?/i;
6
+ const linkSplitRegex = /,(?=\s*<)/;
6
7
  const discoverUrisFromHeaders = (headers, options) => {
7
8
  const uris = /* @__PURE__ */ new Set();
8
9
  const linkHeader = headers.get("link");
9
10
  if (!linkHeader) return [];
10
- const links = linkHeader.split(/,(?=\s*<)/);
11
+ const links = linkHeader.split(linkSplitRegex);
11
12
  for (const link of links) {
12
13
  const urlMatch = link.match(urlRegex);
13
14
  const relMatch = link.match(relRegex);