podverse-parser 5.1.6-alpha.3 → 5.1.7-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/rss/feed/feed.js +1 -1
- package/dist/lib/rss/parser.d.ts +12 -4
- package/dist/lib/rss/parser.d.ts.map +1 -1
- package/dist/lib/rss/parser.js +49 -3
- package/dist/lib/rss/remoteItemParser.d.ts +6 -2
- package/dist/lib/rss/remoteItemParser.d.ts.map +1 -1
- package/dist/lib/rss/remoteItemParser.js +33 -18
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import './module-alias-config';
|
|
2
2
|
export { parseChapters } from './lib/chapters/chapters';
|
|
3
|
-
export { parseRSSFeedAndSaveToDatabase
|
|
3
|
+
export { parseRSSFeedAndSaveToDatabase } from './lib/rss/parser';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,6BAA6B,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -90,7 +90,7 @@ const handleParsedFeed = (parsedFeed_2, feed_1, ...args_1) => __awaiter(void 0,
|
|
|
90
90
|
throw new errors_1.FeedNoChangesSinceLastParsedError(feed.id);
|
|
91
91
|
}
|
|
92
92
|
const feedService = new podverse_orm_1.FeedService();
|
|
93
|
-
return feedService.update(feed.id, { last_parsed_file_hash:
|
|
93
|
+
return feedService.update(feed.id, { last_parsed_file_hash: null });
|
|
94
94
|
});
|
|
95
95
|
exports.handleParsedFeed = handleParsedFeed;
|
|
96
96
|
const checkIfFeedIsParsing = (feed) => {
|
package/dist/lib/rss/parser.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { OnDemandParserEventType } from 'podverse-helpers';
|
|
2
|
+
import { FeedObject } from 'podcast-partytime';
|
|
3
|
+
export declare const getAndParseRSSFeed: (url: string) => Promise<FeedObject>;
|
|
4
|
+
export type ParseRSSOnDemandParserEvent = {
|
|
5
|
+
accountId: number | null;
|
|
6
|
+
remoteParentPodcastIndexId: number | null;
|
|
7
|
+
type: OnDemandParserEventType | null;
|
|
4
8
|
};
|
|
5
|
-
export
|
|
9
|
+
export type ParseRSSFeedAndSaveToDatabaseOptions = {
|
|
10
|
+
forceParse: boolean;
|
|
11
|
+
onDemandParserEvent: ParseRSSOnDemandParserEvent;
|
|
12
|
+
};
|
|
13
|
+
export declare const parseRSSFeedAndSaveToDatabase: (url: string, podcast_index_id: number, options: ParseRSSFeedAndSaveToDatabaseOptions) => Promise<void>;
|
|
6
14
|
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/lib/rss/parser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/lib/rss/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EAIxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAa,MAAM,mBAAmB,CAAC;AAgC1D,eAAO,MAAM,kBAAkB,GAAU,KAAK,MAAM,wBAUnD,CAAC;AAQF,MAAM,MAAM,2BAA2B,GAAG;IACxC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,IAAI,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACtC,CAAA;AAED,MAAM,MAAM,oCAAoC,GAAG;IACjD,UAAU,EAAE,OAAO,CAAC;IACpB,mBAAmB,EAAE,2BAA2B,CAAC;CAClD,CAAA;AAED,eAAO,MAAM,6BAA6B,GACxC,KAAK,MAAM,EACX,kBAAkB,MAAM,EACxB,SAAS,oCAAoC,kBAuI9C,CAAC"}
|
package/dist/lib/rss/parser.js
CHANGED
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.parseRSSFeedAndSaveToDatabase = exports.getAndParseRSSFeed = void 0;
|
|
13
|
+
const podverse_helpers_1 = require("podverse-helpers");
|
|
13
14
|
const podcast_partytime_1 = require("podcast-partytime");
|
|
14
15
|
const podverse_orm_1 = require("podverse-orm");
|
|
15
16
|
// import { handleNewItemsNotifications, handleNewLiveItemsNotifications } from '@parser/lib/notifications';
|
|
@@ -25,6 +26,7 @@ const loggerService_1 = require("@parser/factories/loggerService");
|
|
|
25
26
|
// import { firebaseAccessTokenServiceFactory } from '@parser/factories/firebaseAccessTokenService';
|
|
26
27
|
// import { NotificationsServiceFactory } from '@parser/factories/notificationsService';
|
|
27
28
|
const _request_1 = require("../_request");
|
|
29
|
+
const parsedFeed_1 = require("./hash/parsedFeed");
|
|
28
30
|
/*
|
|
29
31
|
NOTE: All RSS feeds that have a podcast_index_id will be saved to the database.
|
|
30
32
|
RSS feeds without podcast_index_id (Add By RSS feeds) will NOT be saved to the database.
|
|
@@ -40,11 +42,31 @@ const getAndParseRSSFeed = (url) => __awaiter(void 0, void 0, void 0, function*
|
|
|
40
42
|
});
|
|
41
43
|
exports.getAndParseRSSFeed = getAndParseRSSFeed;
|
|
42
44
|
const parseRSSFeedAndSaveToDatabase = (url, podcast_index_id, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
const { onDemandParserEvent } = options;
|
|
46
|
+
const onDemandParserEventService = new podverse_orm_1.OnDemandParserEventService();
|
|
47
|
+
if (onDemandParserEvent) {
|
|
48
|
+
const { accountId, type } = onDemandParserEvent;
|
|
49
|
+
if (accountId && type) {
|
|
50
|
+
if (type === podverse_helpers_1.OnDemandParserEventType.ADD) {
|
|
51
|
+
const count = yield onDemandParserEventService.getCountByAccountIdAndTypeSince(accountId, podverse_helpers_1.OnDemandParserEventType.ADD, (0, podverse_helpers_1.getOnDemandParserEventDateRange)());
|
|
52
|
+
if (count >= podverse_helpers_1.ON_DEMAND_ADD_PARSER_LIMIT) {
|
|
53
|
+
throw new Error('Monthly on-demand add feed parser limit reached');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (type === podverse_helpers_1.OnDemandParserEventType.REFRESH) {
|
|
57
|
+
const count = yield onDemandParserEventService.getCountByAccountIdAndTypeSince(accountId, podverse_helpers_1.OnDemandParserEventType.REFRESH, (0, podverse_helpers_1.getOnDemandParserEventDateRange)());
|
|
58
|
+
if (count >= podverse_helpers_1.ON_DEMAND_REFRESH_PARSER_LIMIT) {
|
|
59
|
+
throw new Error('Monthly on-demand refresh feed parser limit reached');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
43
64
|
const feedService = new podverse_orm_1.FeedService();
|
|
44
65
|
let feed = null;
|
|
45
66
|
let channel = null;
|
|
46
67
|
const timerFullRunLabel = `parseRSSFeedAndSaveToDatabase ${url} ${podcast_index_id}`;
|
|
47
68
|
timerManager_1.timerManager.start(timerFullRunLabel);
|
|
69
|
+
let parsedFeed = null;
|
|
48
70
|
try {
|
|
49
71
|
if (!url || !podcast_index_id) {
|
|
50
72
|
throw new Error(`parseRSSFeedAndSaveToDatabase: url or podcast_index_id is missing for ${url} ${podcast_index_id}`);
|
|
@@ -54,7 +76,7 @@ const parseRSSFeedAndSaveToDatabase = (url, podcast_index_id, options) => __awai
|
|
|
54
76
|
if (!(0, podverse_orm_1.checkIfFeedFlagStatusShouldParse)(feed.feed_flag_status.id)) {
|
|
55
77
|
throw new Error(`parseRSSFeedAndSaveToDatabase: feed_flag_status.status is not Active or AlwaysAllow for ${feed.id} ${feed.podcast_index_id} ${feed.url}`);
|
|
56
78
|
}
|
|
57
|
-
|
|
79
|
+
parsedFeed = yield (0, feed_1.handleRequestRSSFeed)(feed);
|
|
58
80
|
feed = yield (0, feed_1.handleParsedFeed)(parsedFeed, feed, options);
|
|
59
81
|
yield feedService.update(feed.id, { is_parsing: new Date() });
|
|
60
82
|
if ((0, podverse_orm_1.checkIfSpamFeed)(parsedFeed)) {
|
|
@@ -105,11 +127,35 @@ const parseRSSFeedAndSaveToDatabase = (url, podcast_index_id, options) => __awai
|
|
|
105
127
|
loggerService_1.loggerService.info(`Finished parsing channel: ${channel === null || channel === void 0 ? void 0 : channel.id} ${channel === null || channel === void 0 ? void 0 : channel.id_text} feed: ${feed === null || feed === void 0 ? void 0 : feed.id} url: ${url} podcast_index_id: ${podcast_index_id}`);
|
|
106
128
|
timerManager_1.timerManager.endAll();
|
|
107
129
|
if (feed) {
|
|
108
|
-
|
|
130
|
+
if (parsedFeed) {
|
|
131
|
+
const currentFeedFileHash = (0, parsedFeed_1.getParsedFeedMd5Hash)(parsedFeed);
|
|
132
|
+
yield feedService.update(feed.id, {
|
|
133
|
+
is_parsing: null,
|
|
134
|
+
last_parsed_file_hash: currentFeedFileHash
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (onDemandParserEvent) {
|
|
139
|
+
const { accountId, remoteParentPodcastIndexId, type } = onDemandParserEvent;
|
|
140
|
+
if (accountId && type) {
|
|
141
|
+
const accountService = new podverse_orm_1.AccountService();
|
|
142
|
+
const account = yield accountService.get(accountId);
|
|
143
|
+
if (account) {
|
|
144
|
+
yield onDemandParserEventService.create({
|
|
145
|
+
account,
|
|
146
|
+
podcastIndexId: podcast_index_id,
|
|
147
|
+
remoteParentPodcastIndexId: remoteParentPodcastIndexId,
|
|
148
|
+
type,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
109
152
|
}
|
|
110
153
|
}
|
|
111
154
|
if (channel) {
|
|
112
|
-
yield (0, remoteItemParser_1.handleAllRemoteItemsFeedParsing)(channel
|
|
155
|
+
yield (0, remoteItemParser_1.handleAllRemoteItemsFeedParsing)(channel, {
|
|
156
|
+
accountId: (onDemandParserEvent === null || onDemandParserEvent === void 0 ? void 0 : onDemandParserEvent.accountId) || null,
|
|
157
|
+
remoteParentPodcastIndexId: podcast_index_id,
|
|
158
|
+
});
|
|
113
159
|
}
|
|
114
160
|
return;
|
|
115
161
|
});
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import { Channel } from
|
|
2
|
-
export
|
|
1
|
+
import { Channel } from 'podverse-orm';
|
|
2
|
+
export type OnDemandParserRemoteItemParams = {
|
|
3
|
+
accountId: number | null;
|
|
4
|
+
remoteParentPodcastIndexId: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const handleAllRemoteItemsFeedParsing: (channel: Channel, params: OnDemandParserRemoteItemParams) => Promise<void>;
|
|
3
7
|
//# sourceMappingURL=remoteItemParser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remoteItemParser.d.ts","sourceRoot":"","sources":["../../../src/lib/rss/remoteItemParser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"remoteItemParser.d.ts","sourceRoot":"","sources":["../../../src/lib/rss/remoteItemParser.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,OAAO,EAUR,MAAM,cAAc,CAAC;AAUtB,MAAM,MAAM,8BAA8B,GAAG;IAC3C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,0BAA0B,EAAE,MAAM,CAAC;CACpC,CAAA;AAoED,eAAO,MAAM,+BAA+B,GAAU,SAAS,OAAO,EAAE,QAAQ,8BAA8B,kBAO7G,CAAC"}
|
|
@@ -28,8 +28,16 @@ function handleRequestDelay(url) {
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
const handleRemoteItemsFeedParsing = (feedGuidsToParse) => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
|
+
const handleRemoteItemsFeedParsing = (feedGuidsToParse, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
32
32
|
var _a, _b;
|
|
33
|
+
const { accountId, remoteParentPodcastIndexId } = params;
|
|
34
|
+
if (accountId) {
|
|
35
|
+
const onDemandParserEventService = new podverse_orm_1.OnDemandParserEventService();
|
|
36
|
+
const count = yield onDemandParserEventService.getCountByAccountIdAndTypeSince(accountId, podverse_helpers_1.OnDemandParserEventType.REMOTE_ITEM, (0, podverse_helpers_1.getOnDemandParserEventDateRange)());
|
|
37
|
+
if (count >= podverse_helpers_1.ON_DEMAND_REMOTE_ITEM_PARSER_LIMIT) {
|
|
38
|
+
throw new Error('Monthly on-demand remote item feed parser limit reached');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
33
41
|
const piFeedDatas = [];
|
|
34
42
|
for (const feedGuid of feedGuidsToParse) {
|
|
35
43
|
const feedService = new podverse_orm_1.FeedService();
|
|
@@ -54,46 +62,53 @@ const handleRemoteItemsFeedParsing = (feedGuidsToParse) => __awaiter(void 0, voi
|
|
|
54
62
|
if (!feed) {
|
|
55
63
|
yield handleRequestDelay(piFeedData.url);
|
|
56
64
|
loggerService_1.loggerService.info(`handleRemoteItemsFeedParsing: ${piFeedData.url} ${piFeedData.id}`);
|
|
57
|
-
yield (0, parser_1.parseRSSFeedAndSaveToDatabase)(piFeedData.url, piFeedData.id, {
|
|
65
|
+
yield (0, parser_1.parseRSSFeedAndSaveToDatabase)(piFeedData.url, piFeedData.id, {
|
|
66
|
+
forceParse: false,
|
|
67
|
+
onDemandParserEvent: {
|
|
68
|
+
accountId,
|
|
69
|
+
remoteParentPodcastIndexId,
|
|
70
|
+
type: podverse_helpers_1.OnDemandParserEventType.REMOTE_ITEM,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
});
|
|
61
|
-
const handleAllRemoteItemsFeedParsing = (channel) => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
const handleAllRemoteItemsFeedParsing = (channel, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
77
|
const channelService = new podverse_orm_1.ChannelService();
|
|
63
78
|
const latestChannel = yield channelService.get(channel.id);
|
|
64
|
-
yield handleRemoteItemsPodrollParsing(channel);
|
|
65
|
-
yield handleRemoteItemsPublisherParsing(channel);
|
|
66
|
-
yield handleRemoteItemsChannelParsing(channel);
|
|
67
|
-
yield handleRemoteItemsItemValueTimeSplitParsing(latestChannel);
|
|
79
|
+
yield handleRemoteItemsPodrollParsing(channel, params);
|
|
80
|
+
yield handleRemoteItemsPublisherParsing(channel, params);
|
|
81
|
+
yield handleRemoteItemsChannelParsing(channel, params);
|
|
82
|
+
yield handleRemoteItemsItemValueTimeSplitParsing(latestChannel, params);
|
|
68
83
|
});
|
|
69
84
|
exports.handleAllRemoteItemsFeedParsing = handleAllRemoteItemsFeedParsing;
|
|
70
|
-
const handleRemoteItemsPodrollParsing = (channel) => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
const handleRemoteItemsPodrollParsing = (channel, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
86
|
const channelPodrollService = new podverse_orm_1.ChannelPodrollService();
|
|
72
87
|
const channelPodroll = yield channelPodrollService.get(channel);
|
|
73
88
|
if (channelPodroll) {
|
|
74
89
|
const channelPodrollRemoteItemService = new podverse_orm_1.ChannelPodrollRemoteItemService();
|
|
75
90
|
const channelPodrollRemoteItems = yield channelPodrollRemoteItemService.getAll(channelPodroll);
|
|
76
|
-
const feedGuidsToParse = channelPodrollRemoteItems.map(
|
|
77
|
-
yield handleRemoteItemsFeedParsing(feedGuidsToParse);
|
|
91
|
+
const feedGuidsToParse = channelPodrollRemoteItems.map(remoteItem => remoteItem.feed_guid);
|
|
92
|
+
yield handleRemoteItemsFeedParsing(feedGuidsToParse, params);
|
|
78
93
|
}
|
|
79
94
|
});
|
|
80
|
-
const handleRemoteItemsPublisherParsing = (channel) => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
const handleRemoteItemsPublisherParsing = (channel, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
96
|
const channelPublisherService = new podverse_orm_1.ChannelPublisherService();
|
|
82
97
|
const channelPublisher = yield channelPublisherService.get(channel);
|
|
83
98
|
if (channelPublisher) {
|
|
84
99
|
const channelPublisherRemoteItemService = new podverse_orm_1.ChannelPublisherRemoteItemService();
|
|
85
100
|
const channelPublisherRemoteItems = yield channelPublisherRemoteItemService.getAll(channelPublisher);
|
|
86
|
-
const feedGuidsToParse = channelPublisherRemoteItems.map(
|
|
87
|
-
yield handleRemoteItemsFeedParsing(feedGuidsToParse);
|
|
101
|
+
const feedGuidsToParse = channelPublisherRemoteItems.map(remoteItem => remoteItem.feed_guid);
|
|
102
|
+
yield handleRemoteItemsFeedParsing(feedGuidsToParse, params);
|
|
88
103
|
}
|
|
89
104
|
});
|
|
90
|
-
const handleRemoteItemsChannelParsing = (channel) => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
const handleRemoteItemsChannelParsing = (channel, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
106
|
const channelRemoteItemService = new podverse_orm_1.ChannelRemoteItemService();
|
|
92
107
|
const channelRemoteItems = yield channelRemoteItemService.getAll(channel);
|
|
93
|
-
const feedGuidsToParse = channelRemoteItems.map(
|
|
94
|
-
yield handleRemoteItemsFeedParsing(feedGuidsToParse);
|
|
108
|
+
const feedGuidsToParse = channelRemoteItems.map(remoteItem => remoteItem.feed_guid);
|
|
109
|
+
yield handleRemoteItemsFeedParsing(feedGuidsToParse, params);
|
|
95
110
|
});
|
|
96
|
-
const handleRemoteItemsItemValueTimeSplitParsing = (channel) => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
const handleRemoteItemsItemValueTimeSplitParsing = (channel, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
112
|
var _a, _b;
|
|
98
113
|
if (channel.has_value_time_splits) {
|
|
99
114
|
const itemService = new podverse_orm_1.ItemService();
|
|
@@ -113,7 +128,7 @@ const handleRemoteItemsItemValueTimeSplitParsing = (channel) => __awaiter(void 0
|
|
|
113
128
|
for (const itemValueTimeSplit of itemValue.item_value_time_splits) {
|
|
114
129
|
if (itemValueTimeSplit.item_value_time_split_remote_item) {
|
|
115
130
|
const feedGuid = itemValueTimeSplit.item_value_time_split_remote_item.feed_guid;
|
|
116
|
-
yield handleRemoteItemsFeedParsing([feedGuid]);
|
|
131
|
+
yield handleRemoteItemsFeedParsing([feedGuid], params);
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "podverse-parser",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.7-alpha.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"module-alias": "^2.2.3",
|
|
20
20
|
"podcast-partytime": "^4.9.1",
|
|
21
|
-
"podverse-external-services": "^5.1.
|
|
22
|
-
"podverse-helpers": "^5.1.
|
|
23
|
-
"podverse-orm": "^5.1.
|
|
21
|
+
"podverse-external-services": "^5.1.7-alpha.0",
|
|
22
|
+
"podverse-helpers": "^5.1.7-alpha.0",
|
|
23
|
+
"podverse-orm": "^5.1.7-alpha.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@eslint/config-array": "^0.21.0",
|