podverse-parser 1.0.0 → 5.0.0-beta.1

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 (59) hide show
  1. package/.nvmrc +1 -0
  2. package/dist/index.js +7 -81
  3. package/eslint.config.mjs +38 -0
  4. package/package.json +20 -29
  5. package/src/index.ts +3 -0
  6. package/src/lib/chapters/chapters.ts +54 -0
  7. package/src/lib/compat/channel.ts +287 -0
  8. package/src/lib/compat/chapters/chapters.ts +23 -0
  9. package/src/lib/compat/partytime/channel.ts +289 -0
  10. package/src/lib/compat/partytime/compatFull.ts +7 -0
  11. package/src/lib/compat/partytime/funding.ts +8 -0
  12. package/src/lib/compat/partytime/item.ts +273 -0
  13. package/src/lib/compat/partytime/liveItem.ts +18 -0
  14. package/src/lib/compat/partytime/value.ts +64 -0
  15. package/src/lib/rss/base/handleParsedManyData.ts +16 -0
  16. package/src/lib/rss/base/handleParsedOneData.ts +17 -0
  17. package/src/lib/rss/channel/channel.ts +55 -0
  18. package/src/lib/rss/channel/channelAbout.ts +13 -0
  19. package/src/lib/rss/channel/channelChat.ts +14 -0
  20. package/src/lib/rss/channel/channelDescription.ts +14 -0
  21. package/src/lib/rss/channel/channelFunding.ts +14 -0
  22. package/src/lib/rss/channel/channelImage.ts +14 -0
  23. package/src/lib/rss/channel/channelLicense.ts +14 -0
  24. package/src/lib/rss/channel/channelLocation.ts +14 -0
  25. package/src/lib/rss/channel/channelPerson.ts +14 -0
  26. package/src/lib/rss/channel/channelPodroll.ts +21 -0
  27. package/src/lib/rss/channel/channelRemoteItem.ts +14 -0
  28. package/src/lib/rss/channel/channelSeason.ts +14 -0
  29. package/src/lib/rss/channel/channelSocialInteract.ts +14 -0
  30. package/src/lib/rss/channel/channelTrailer.ts +31 -0
  31. package/src/lib/rss/channel/channelTxt.ts +14 -0
  32. package/src/lib/rss/channel/channelValue.ts +30 -0
  33. package/src/lib/rss/feed/feed.ts +79 -0
  34. package/src/lib/rss/hash/parsedFeed.ts +9 -0
  35. package/src/lib/rss/item/item.ts +85 -0
  36. package/src/lib/rss/item/itemAbout.ts +13 -0
  37. package/src/lib/rss/item/itemChaptersFeed.ts +14 -0
  38. package/src/lib/rss/item/itemChat.ts +14 -0
  39. package/src/lib/rss/item/itemContentLink.ts +0 -0
  40. package/src/lib/rss/item/itemDescription.ts +14 -0
  41. package/src/lib/rss/item/itemEnclosure.ts +35 -0
  42. package/src/lib/rss/item/itemImage.ts +14 -0
  43. package/src/lib/rss/item/itemLicense.ts +14 -0
  44. package/src/lib/rss/item/itemLocation.ts +14 -0
  45. package/src/lib/rss/item/itemPerson.ts +14 -0
  46. package/src/lib/rss/item/itemSeason.ts +26 -0
  47. package/src/lib/rss/item/itemSeasonEpisode.ts +14 -0
  48. package/src/lib/rss/item/itemSocialInteract.ts +14 -0
  49. package/src/lib/rss/item/itemSoundbite.ts +14 -0
  50. package/src/lib/rss/item/itemTranscript.ts +14 -0
  51. package/src/lib/rss/item/itemTxt.ts +14 -0
  52. package/src/lib/rss/item/itemValue.ts +60 -0
  53. package/src/lib/rss/liveItem/liveItem.ts +34 -0
  54. package/src/lib/rss/parser.ts +85 -0
  55. package/src/module-alias-config.ts +8 -0
  56. package/tsconfig.json +25 -0
  57. package/LICENSE +0 -674
  58. package/README.md +0 -2
  59. package/dist/index.js.map +0 -7
@@ -0,0 +1,38 @@
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+ import tseslint from "@typescript-eslint/eslint-plugin";
4
+ import tsParser from "@typescript-eslint/parser";
5
+
6
+ const pluginJsRecommended = {
7
+ rules: {
8
+ ...pluginJs.configs.recommended.rules,
9
+ },
10
+ };
11
+
12
+ const tseslintRecommended = {
13
+ rules: {
14
+ ...tseslint.configs.recommended.rules,
15
+ },
16
+ };
17
+
18
+ export default [
19
+ {
20
+ files: ["src/**/*.{js,ts}"],
21
+ languageOptions: {
22
+ globals: globals.node,
23
+ parser: tsParser,
24
+ },
25
+ plugins: {
26
+ "@typescript-eslint": tseslint,
27
+ },
28
+ rules: {
29
+ ...pluginJsRecommended.rules,
30
+ ...tseslintRecommended.rules,
31
+ "semi": ["error", "always"],
32
+ "indent": ["error", 2],
33
+ "@typescript-eslint/no-require-imports": "off",
34
+ },
35
+ ignores: ["**/eslint.config.mjs"],
36
+ }
37
+ ];
38
+
package/package.json CHANGED
@@ -1,41 +1,32 @@
1
1
  {
2
2
  "name": "podverse-parser",
3
- "version": "1.0.0",
4
- "description": "The RSS parser system for podverse-parser",
3
+ "version": "5.0.0-beta.1",
4
+ "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
- "dev": "nodemon --watch ./src -x \"npm run build\"",
9
- "build": "esbuild src/index.ts --bundle --minify --sourcemap --platform=node --target=es2020 --outfile=dist/index.js",
8
+ "dev:watch": "nodemon --watch 'src' --watch $(realpath node_modules/podverse-external-services) --watch $(realpath node_modules/podverse-orm) --watch $(realpath node_modules/podverse-shared) -x \"npm run build\"",
9
+ "build": "tsc",
10
10
  "lint": "eslint ./src --ext .ts",
11
- "lint-fix": "eslint --fix",
12
- "prepare": "npm run build"
13
- },
14
- "files": [
15
- "/dist"
16
- ],
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/podverse/podverse-parser.git"
11
+ "prepare": "npm run build",
12
+ "start": "ts-node ./dist/index.js"
20
13
  },
14
+ "author": "",
21
15
  "license": "AGPLv3",
22
- "bugs": {
23
- "url": "https://github.com/podverse/podverse-parser/issues"
16
+ "dependencies": {
17
+ "module-alias": "^2.2.3",
18
+ "podcast-partytime": "^4.8.1",
19
+ "podverse-helpers": "^5.0.0-beta.1",
20
+ "podverse-orm": "^5.0.0-beta.1"
24
21
  },
25
- "homepage": "https://github.com/podverse/podverse-parser#readme",
26
22
  "devDependencies": {
27
- "@typescript-eslint/eslint-plugin": "^6.13.0",
28
- "@typescript-eslint/parser": "^6.13.0",
29
- "esbuild": "^0.19.8",
30
- "eslint": "^8.54.0",
31
- "nodemon": "^3.0.2",
32
- "typescript": "^5.3.2"
33
- },
34
- "dependencies": {
35
- "aws-sdk": "2.814.0",
36
- "axios": "^1.6.2",
37
- "podverse-shared": "^4.15.2",
38
- "request-promise-native": "1.0.8",
39
- "sharp": "^0.33.0"
23
+ "@eslint/config-array": "^0.18.0",
24
+ "@eslint/eslintrc": "^3.1.0",
25
+ "@eslint/js": "^9.8.0",
26
+ "@types/node": "^22.1.0",
27
+ "eslint": "^9.9.0",
28
+ "ts-node": "^10.9.2",
29
+ "typescript": "^5.5.4",
30
+ "typescript-eslint": "^8.0.1"
40
31
  }
41
32
  }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import './module-alias-config';
2
+
3
+ export { parseAllRSSFeeds, parseRSSFeedAndSaveToDatabase } from './lib/rss/parser';
@@ -0,0 +1,54 @@
1
+ import { request, throwRequestError } from "podverse-helpers";
2
+ import { Item, ItemChaptersFeed, ItemChapterService, ItemChaptersFeedLogService } from "podverse-orm";
3
+ import { compatParsedChapters, PIChapter } from "@parser/lib/compat/chapters/chapters";
4
+
5
+ const getParsedChapters = async (item_chapters_feed: ItemChaptersFeed) => {
6
+ const itemChaptersFeedLogService = new ItemChaptersFeedLogService();
7
+ try {
8
+ const response: { chapters: PIChapter[] } = await request(item_chapters_feed.url);
9
+ await itemChaptersFeedLogService.update(item_chapters_feed, {
10
+ last_http_status: 200,
11
+ last_good_http_status_time: new Date()
12
+ });
13
+ return compatParsedChapters(response.chapters);
14
+ } catch (error) {
15
+ // TODO: how to handle errors?
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ const statusCode = (error as any).statusCode as number;
18
+ const item_chapters_feed_log = await itemChaptersFeedLogService.get(item_chapters_feed);
19
+ if (statusCode) {
20
+ await itemChaptersFeedLogService.update(item_chapters_feed, {
21
+ last_http_status: statusCode,
22
+ parse_errors: (item_chapters_feed_log?.parse_errors || 0) + 1,
23
+ });
24
+ }
25
+ return throwRequestError(error);
26
+ }
27
+ };
28
+
29
+ export const parseChapters = async (item: Item): Promise<void> => {
30
+ const item_chapters_feed = item.item_chapters_feed;
31
+
32
+ if (!item_chapters_feed) {
33
+ return;
34
+ }
35
+
36
+ const parsedChapters = await getParsedChapters(item_chapters_feed);
37
+
38
+ const itemChapterService = new ItemChapterService();
39
+
40
+ const existingChapters = await itemChapterService.getAll(item_chapters_feed, { select: ['id'] });
41
+ const existingChaptersIds = existingChapters.map(item_chapter => item_chapter.id);
42
+ const updatedChaptersIds: number[] = [];
43
+
44
+ for (const parsedChapter of parsedChapters) {
45
+ await itemChapterService.update(item_chapters_feed, parsedChapter);
46
+ updatedChaptersIds.push(item_chapters_feed.id);
47
+ }
48
+
49
+ const itemChapterIdsToDelete = existingChaptersIds.filter(id => !updatedChaptersIds.includes(id));
50
+ await itemChapterService.deleteMany(itemChapterIdsToDelete);
51
+
52
+ const itemChaptersFeedLogService = new ItemChaptersFeedLogService();
53
+ await itemChaptersFeedLogService.update(item_chapters_feed, { last_finished_parse_time: new Date() });
54
+ };
@@ -0,0 +1,287 @@
1
+ import { FeedObject, Phase4Medium } from "podcast-partytime";
2
+ import { Phase4PodcastImage } from "podcast-partytime/dist/parser/phase/phase-4";
3
+ import { createSortableTitle, getBooleanOrNull } from "podverse-helpers";
4
+ import { getChannelItunesTypeItunesTypeEnumValue, getMediumEnumValue } from "podverse-orm";
5
+ import { compatChannelValue } from "@parser/lib/compat/partytime/value";
6
+
7
+ export const compatChannelDto = (parsedFeed: FeedObject) => ({
8
+ podcast_guid: parsedFeed.guid,
9
+ title: parsedFeed.title,
10
+ sortable_title: createSortableTitle(parsedFeed.title),
11
+ medium: getMediumEnumValue(parsedFeed.medium ?? Phase4Medium.Podcast)
12
+ });
13
+
14
+ export const compatChannelAboutDto = (parsedFeed: FeedObject) => ({
15
+ author: (Array.isArray(parsedFeed.author) ? parsedFeed.author : parsedFeed.author ? [parsedFeed.author] : [])?.join(', ') || null,
16
+ explicit: getBooleanOrNull(parsedFeed.explicit),
17
+ language: parsedFeed.language || null,
18
+ website_link_url: parsedFeed.link || null,
19
+ itunes_type: getChannelItunesTypeItunesTypeEnumValue(parsedFeed.itunesType || 'episodic')
20
+ });
21
+
22
+ export const compatChannelChatDto = (parsedFeed: FeedObject) => {
23
+ if (!parsedFeed.chat) {
24
+ return null;
25
+ }
26
+ return {
27
+ server: parsedFeed.chat.server,
28
+ protocol: parsedFeed.chat.protocol,
29
+ account_id: parsedFeed.chat.accountId || null,
30
+ space: parsedFeed.chat.space || null
31
+ };
32
+ };
33
+
34
+ export const compatChannelDescriptionDto = (parsedFeed: FeedObject) => {
35
+ if (!parsedFeed.description) {
36
+ return null;
37
+ }
38
+ return {
39
+ value: parsedFeed.description
40
+ };
41
+ };
42
+
43
+ export const compatChannelFundingDtos = (parsedFeed: FeedObject) => {
44
+ const dtos = [];
45
+
46
+ if (Array.isArray(parsedFeed.podcastFunding)) {
47
+ for (const f of parsedFeed.podcastFunding) {
48
+ if (f.url) {
49
+ dtos.push({
50
+ url: f.url,
51
+ title: f.message || null
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ return dtos;
58
+ };
59
+
60
+ export const compatChannelImageDtos = (parsedFeed: FeedObject) => {
61
+ const dtos = [];
62
+ if (parsedFeed.itunesImage) {
63
+ dtos.push({
64
+ url: parsedFeed.itunesImage,
65
+ image_width_size: null
66
+ });
67
+ } else if (parsedFeed.image?.url) {
68
+ dtos.push({
69
+ url: parsedFeed.image.url,
70
+ image_width_size: null
71
+ });
72
+ }
73
+
74
+ function hasWidth(image: Phase4PodcastImage['parsed']): image is { url: string; width: number } {
75
+ return (image as { width: number }).width !== undefined;
76
+ }
77
+
78
+ if (Array.isArray(parsedFeed.podcastImages)) {
79
+ for (const image of parsedFeed.podcastImages) {
80
+ if (image.parsed.url && hasWidth(image.parsed)) {
81
+ dtos.push({
82
+ url: image.parsed.url,
83
+ image_width_size: image.parsed.width
84
+ });
85
+ }
86
+ }
87
+ }
88
+
89
+ return dtos;
90
+ };
91
+
92
+ export const compatChannelLicenseDto = (parsedFeed: FeedObject) => {
93
+ if (!parsedFeed?.license?.identifier) {
94
+ return null;
95
+ }
96
+ return {
97
+ identifier: parsedFeed.license.identifier,
98
+ url: parsedFeed.license.url || null
99
+ };
100
+ };
101
+
102
+ export const compatChannelLocationDto = (parsedFeed: FeedObject) => {
103
+ if (!parsedFeed?.podcastLocation?.geo && !parsedFeed?.podcastLocation?.osm) {
104
+ return null;
105
+ }
106
+
107
+ return {
108
+ geo: parsedFeed.podcastLocation.geo || null,
109
+ osm: parsedFeed.podcastLocation.osm || null,
110
+ name: parsedFeed.podcastLocation.name || null
111
+ };
112
+ };
113
+
114
+ export const compatChannelPersonDtos = (parsedFeed: FeedObject) => {
115
+ const dtos = [];
116
+
117
+ if (Array.isArray(parsedFeed.podcastPeople)) {
118
+ for (const p of parsedFeed.podcastPeople) {
119
+ if (p.name) {
120
+ dtos.push({
121
+ name: p.name,
122
+ role: p.role?.toLowerCase() || null,
123
+ person_group: p.group?.toLowerCase() || 'cast',
124
+ img: p.img || null,
125
+ href: p.href || null
126
+ });
127
+ }
128
+ }
129
+ }
130
+
131
+ return dtos;
132
+ };
133
+
134
+ export const compatChannelPodrollRemoteItemDtos = (parsedFeed: FeedObject) => {
135
+ const dtos = [];
136
+
137
+ if (Array.isArray(parsedFeed.podroll)) {
138
+ for (const ri of parsedFeed.podroll) {
139
+ if (ri.feedGuid) {
140
+ dtos.push({
141
+ feed_guid: ri.feedGuid,
142
+ feed_url: ri.feedUrl || null,
143
+ item_guid: null,
144
+ title: /* PTDO: ri.title || */ null
145
+ });
146
+ }
147
+ }
148
+ }
149
+
150
+ return dtos;
151
+ };
152
+
153
+ export const compatChannelPublisherRemoteItemDtos = (parsedFeed: FeedObject) => {
154
+ const dtos = [];
155
+
156
+ if (Array.isArray(parsedFeed.podroll)) {
157
+ for (const ri of parsedFeed.podroll) {
158
+ if (ri.feedGuid) {
159
+ dtos.push({
160
+ feed_guid: ri.feedGuid,
161
+ feed_url: ri.feedUrl || null,
162
+ item_guid: null,
163
+ title: /* PTDO: ri.title || */ null
164
+ });
165
+ }
166
+ }
167
+ }
168
+
169
+ return dtos;
170
+ };
171
+
172
+ export const compatChannelRemoteItemDtos = (parsedFeed: FeedObject) => {
173
+ const dtos = [];
174
+
175
+ if (Array.isArray(parsedFeed.podcastRemoteItems)) {
176
+ for (const ri of parsedFeed.podcastRemoteItems) {
177
+ if (ri.feedGuid) {
178
+ dtos.push({
179
+ feed_guid: ri.feedGuid,
180
+ feed_url: ri.feedUrl || null,
181
+ item_guid: null,
182
+ title: /* PTDO: ri.title || */ null
183
+ });
184
+ }
185
+ }
186
+ }
187
+
188
+ return dtos;
189
+ };
190
+
191
+ export const compatChannelSocialInteractDtos = (parsedFeed: FeedObject) => {
192
+ const dtos = [];
193
+
194
+ if (parsedFeed?.podcastSocial?.length) {
195
+ for (const ps of parsedFeed.podcastSocial) {
196
+ dtos.push({
197
+ // PTDO: fix keys mismatch between partytime and podverse
198
+ protocol: ps.platform,
199
+ uri: ps.url,
200
+ account_id: ps.id || null,
201
+ account_url: ps.name || null,
202
+ priority: ps.priority || null
203
+ });
204
+ }
205
+ }
206
+
207
+ return dtos;
208
+ };
209
+
210
+ export const compatChannelSeasonDtos = (parsedFeed: FeedObject) => {
211
+ const dtos = [];
212
+
213
+ const parsedItems = parsedFeed?.items || [];
214
+
215
+ const seasonsIndex: { [key: number]: { name: string | null } } = {};
216
+
217
+ for (const parsedItem of parsedItems) {
218
+ const seasonNumber = parsedItem?.podcastSeason?.number || parsedItem?.itunesSeason;
219
+ const seasonName = parsedItem?.podcastSeason?.name || null;
220
+ if (Number.isInteger(seasonNumber)) {
221
+ const seasonNumberAsNumber = seasonNumber as number;
222
+ seasonsIndex[seasonNumberAsNumber] = {
223
+ name: seasonName
224
+ };
225
+ }
226
+ }
227
+
228
+ for (const [number, { name }] of Object.entries(seasonsIndex)) {
229
+ dtos.push({
230
+ number: parseInt(number),
231
+ name: name || null
232
+ });
233
+ }
234
+
235
+ return dtos;
236
+ };
237
+
238
+ export const compatChannelTrailerDtos = (parsedFeed: FeedObject) => {
239
+ const dtos = [];
240
+ if (parsedFeed?.trailers?.length) {
241
+ for (const pt of parsedFeed.trailers) {
242
+ dtos.push({
243
+ url: pt.url,
244
+ title: /* PTDO: add pt.title || */ null,
245
+ pubdate: pt.pubdate,
246
+ length: pt.length || null,
247
+ type: pt.type || null,
248
+ season: pt.season || null
249
+ });
250
+ }
251
+ }
252
+
253
+ return dtos;
254
+ };
255
+
256
+ export const compatChannelTxtDtos = (parsedFeed: FeedObject) => {
257
+ const dtos = [];
258
+ if (parsedFeed?.podcastTxt?.length) {
259
+ for (const pt of parsedFeed.podcastTxt) {
260
+ dtos.push({
261
+ purpose: pt.purpose || null,
262
+ value: pt.value
263
+ });
264
+ }
265
+ }
266
+
267
+ return dtos;
268
+ };
269
+
270
+ export const compatChannelValueDtos = (parsedFeed: FeedObject) => {
271
+ let dtos = [];
272
+ if (parsedFeed.value) {
273
+ const dto = compatChannelValue(parsedFeed.value);
274
+
275
+ const formattedDto = {
276
+ channel_value: {
277
+ type: dto.type,
278
+ method: dto.method,
279
+ suggested: dto.suggested || null
280
+ },
281
+ channel_value_recipients: dto.channel_value_recipients
282
+ };
283
+
284
+ dtos.push(formattedDto);
285
+ }
286
+ return dtos;
287
+ };
@@ -0,0 +1,23 @@
1
+ import { ItemChapterDto } from 'podverse-orm';
2
+
3
+ export type PIChapter = {
4
+ startTime: string
5
+ endTime: string | null
6
+ title: string | null
7
+ img: string | null
8
+ url: string | null
9
+ toc: boolean
10
+ }
11
+
12
+ export const compatParsedChapters = (chapters: PIChapter[]): ItemChapterDto[] => {
13
+ return chapters.map(chapter => {
14
+ return {
15
+ start_time: chapter.startTime,
16
+ end_time: chapter.endTime || null,
17
+ title: chapter.title || null,
18
+ img: chapter.img || null,
19
+ web_url: chapter.url || null,
20
+ table_of_contents: chapter.toc || true
21
+ };
22
+ });
23
+ };