@recordtimelabel/core 0.4.7 → 0.5.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/README.md +7 -3
- package/package.json +3 -2
- package/src/domain/channel-folder.js +377 -0
- package/src/domain/group-identity.js +456 -0
- package/src/domain/hash.js +147 -0
- package/src/domain/legacy.js +359 -0
- package/src/domain/shared.js +132 -0
- package/src/domain/time.js +43 -0
- package/src/domain/twitch-vod.js +303 -0
- package/src/domain.js +45 -0
- package/src/index.js +59 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import {fingerprintCanonicalJson} from './hash.js';
|
|
2
|
+
import {buildRecordTimeLabelGroupIndex} from './group-identity.js';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_UNKNOWN_TITLE,
|
|
5
|
+
collectNonVirtualRecords,
|
|
6
|
+
extractTwitchVodIdFromUrl,
|
|
7
|
+
getFirstMatchingUrlValue,
|
|
8
|
+
getRecordUrls,
|
|
9
|
+
normalizeId,
|
|
10
|
+
normalizeRecordPlatform,
|
|
11
|
+
normalizeText,
|
|
12
|
+
parseElapsedSeconds
|
|
13
|
+
} from './shared.js';
|
|
14
|
+
import {parseUtcMillis} from './time.js';
|
|
15
|
+
|
|
16
|
+
const TITLE_BRACKET_PATTERN = /\[[^\]]*\]|\([^)]*\)|\{[^}]*\}/g;
|
|
17
|
+
const NON_WORD_PATTERN = /[^\p{L}\p{N}\s]/gu;
|
|
18
|
+
const MINUTE_MS = 60 * 1000;
|
|
19
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
20
|
+
const RECORD_AFTER_START_GRACE_MS = 10 * MINUTE_MS;
|
|
21
|
+
const STREAM_START_GRACE_MS = 45 * MINUTE_MS;
|
|
22
|
+
const ELAPSED_START_TOLERANCE_MS = 45 * MINUTE_MS;
|
|
23
|
+
const MAX_REASONABLE_STREAM_MS = 48 * HOUR_MS;
|
|
24
|
+
|
|
25
|
+
const getVodStartMs = (vod = {}) => parseUtcMillis(vod.publishedAt || vod.createdAt);
|
|
26
|
+
|
|
27
|
+
const getNextVodStartMs = (value) => (
|
|
28
|
+
typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
29
|
+
? value
|
|
30
|
+
: parseUtcMillis(value)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const normalizeVodTitleForMatch = (value) => String(value || '')
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.normalize('NFKC')
|
|
36
|
+
.replace(TITLE_BRACKET_PATTERN, ' ')
|
|
37
|
+
.replace(NON_WORD_PATTERN, ' ')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
40
|
+
|
|
41
|
+
export const calculateVodTitleSimilarity = (expectedTitle, candidateTitle) => {
|
|
42
|
+
const expected = normalizeVodTitleForMatch(expectedTitle);
|
|
43
|
+
const candidate = normalizeVodTitleForMatch(candidateTitle);
|
|
44
|
+
if (!expected || !candidate) return 0;
|
|
45
|
+
if (expected === candidate) return 1;
|
|
46
|
+
if (expected.includes(candidate) || candidate.includes(expected)) return 0.9;
|
|
47
|
+
|
|
48
|
+
const expectedTokens = new Set(expected.split(' ').filter(Boolean));
|
|
49
|
+
const candidateTokens = new Set(candidate.split(' ').filter(Boolean));
|
|
50
|
+
if (expectedTokens.size === 0 || candidateTokens.size === 0) return 0;
|
|
51
|
+
|
|
52
|
+
let intersection = 0;
|
|
53
|
+
expectedTokens.forEach((token) => {
|
|
54
|
+
if (candidateTokens.has(token)) intersection += 1;
|
|
55
|
+
});
|
|
56
|
+
const union = new Set([...expectedTokens, ...candidateTokens]).size;
|
|
57
|
+
return union > 0 ? intersection / union : 0;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const scoreTwitchVodCandidate = (vod = {}, context = {}) => {
|
|
61
|
+
const recordMs = parseUtcMillis(context.recordTimestamp);
|
|
62
|
+
const streamStartMs = parseUtcMillis(context.streamCreatedAt || context.streamStartedAt);
|
|
63
|
+
const elapsedSeconds = parseElapsedSeconds(context.recordLiveTimeSeconds);
|
|
64
|
+
const targetSeconds = parseElapsedSeconds(context.recordTargetTimeSeconds) ?? elapsedSeconds;
|
|
65
|
+
const vodStartMs = getVodStartMs(vod);
|
|
66
|
+
const nextVodStartMs = getNextVodStartMs(context.nextVodStartMs || context.nextVodStartAt);
|
|
67
|
+
|
|
68
|
+
if (!vod?.id || vodStartMs === null) return null;
|
|
69
|
+
|
|
70
|
+
const expectedStartMs = recordMs !== null && elapsedSeconds !== null
|
|
71
|
+
? recordMs - (elapsedSeconds * 1000)
|
|
72
|
+
: null;
|
|
73
|
+
const lengthSeconds = parseElapsedSeconds(vod.lengthSeconds);
|
|
74
|
+
const hasStableAnchor = Boolean(streamStartMs !== null || expectedStartMs !== null);
|
|
75
|
+
const matchedBy = [];
|
|
76
|
+
let score = 0;
|
|
77
|
+
const streamAnchorMs = expectedStartMs !== null ? expectedStartMs : streamStartMs;
|
|
78
|
+
const targetAbsoluteMs = targetSeconds !== null && streamAnchorMs !== null
|
|
79
|
+
? streamAnchorMs + (targetSeconds * 1000)
|
|
80
|
+
: recordMs;
|
|
81
|
+
const lengthEndMs = lengthSeconds !== null ? vodStartMs + (lengthSeconds * 1000) : null;
|
|
82
|
+
const inferredEndMs = nextVodStartMs !== null &&
|
|
83
|
+
nextVodStartMs > vodStartMs &&
|
|
84
|
+
nextVodStartMs - vodStartMs <= MAX_REASONABLE_STREAM_MS
|
|
85
|
+
? nextVodStartMs
|
|
86
|
+
: null;
|
|
87
|
+
const vodEndMs = lengthEndMs || inferredEndMs;
|
|
88
|
+
|
|
89
|
+
if (targetAbsoluteMs !== null) {
|
|
90
|
+
if (targetAbsoluteMs < vodStartMs - RECORD_AFTER_START_GRACE_MS) return null;
|
|
91
|
+
if (vodEndMs !== null && targetAbsoluteMs >= vodEndMs + RECORD_AFTER_START_GRACE_MS) return null;
|
|
92
|
+
if (vodEndMs !== null) {
|
|
93
|
+
score += 80;
|
|
94
|
+
matchedBy.push('target-window');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (expectedStartMs !== null) {
|
|
99
|
+
const startDelta = Math.abs(vodStartMs - expectedStartMs);
|
|
100
|
+
if (startDelta <= ELAPSED_START_TOLERANCE_MS) {
|
|
101
|
+
const closeness = 1 - (startDelta / ELAPSED_START_TOLERANCE_MS);
|
|
102
|
+
score += 85 + (closeness * 15);
|
|
103
|
+
matchedBy.push('elapsed-start');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (streamStartMs !== null && recordMs !== null) {
|
|
108
|
+
const startsInsideStreamWindow =
|
|
109
|
+
vodStartMs >= streamStartMs - STREAM_START_GRACE_MS &&
|
|
110
|
+
vodStartMs <= recordMs + RECORD_AFTER_START_GRACE_MS;
|
|
111
|
+
if (startsInsideStreamWindow) {
|
|
112
|
+
const streamSpan = Math.max(recordMs - streamStartMs, 1);
|
|
113
|
+
const positionPenalty = Math.min(Math.max(vodStartMs - streamStartMs, 0) / streamSpan, 1) * 15;
|
|
114
|
+
score += 70 - positionPenalty;
|
|
115
|
+
matchedBy.push('stream-window');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (recordMs !== null && vodEndMs !== null) {
|
|
120
|
+
const containsRecord =
|
|
121
|
+
recordMs >= vodStartMs - RECORD_AFTER_START_GRACE_MS &&
|
|
122
|
+
recordMs <= vodEndMs + RECORD_AFTER_START_GRACE_MS;
|
|
123
|
+
if (containsRecord) {
|
|
124
|
+
score += 60;
|
|
125
|
+
matchedBy.push('duration-window');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!hasStableAnchor && recordMs !== null) {
|
|
130
|
+
const recordOffsetMs = recordMs - vodStartMs;
|
|
131
|
+
if (recordOffsetMs >= -RECORD_AFTER_START_GRACE_MS && recordOffsetMs <= MAX_REASONABLE_STREAM_MS) {
|
|
132
|
+
const closeness = 1 - Math.min(Math.max(recordOffsetMs, 0) / MAX_REASONABLE_STREAM_MS, 1);
|
|
133
|
+
score += 30 + (closeness * 20);
|
|
134
|
+
matchedBy.push('legacy-time-window');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (matchedBy.length === 0) return null;
|
|
139
|
+
|
|
140
|
+
const titleSimilarity = calculateVodTitleSimilarity(context.expectedTitle, vod.title || '');
|
|
141
|
+
score += titleSimilarity * 10;
|
|
142
|
+
if (score < (hasStableAnchor ? 55 : 25)) return null;
|
|
143
|
+
|
|
144
|
+
const vodTimeOffsetSeconds = targetSeconds !== null && matchedBy.includes('elapsed-start')
|
|
145
|
+
? targetSeconds
|
|
146
|
+
: targetSeconds !== null && streamAnchorMs !== null
|
|
147
|
+
? Math.max(0, Math.floor(targetSeconds - ((vodStartMs - streamAnchorMs) / 1000)))
|
|
148
|
+
: (recordMs !== null
|
|
149
|
+
? Math.max(0, Math.floor((recordMs - vodStartMs) / 1000))
|
|
150
|
+
: targetSeconds);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
vod,
|
|
154
|
+
score,
|
|
155
|
+
matchedBy,
|
|
156
|
+
titleSimilarity,
|
|
157
|
+
vodStartAt: new Date(vodStartMs).toISOString(),
|
|
158
|
+
vodTimeOffsetSeconds
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const selectBestMatchingTwitchVod = (items = [], context = {}) => {
|
|
163
|
+
const normalizedItems = (Array.isArray(items) ? items : [])
|
|
164
|
+
.map((item) => item?.node || item)
|
|
165
|
+
.map((vod) => ({vod, startMs: getVodStartMs(vod)}))
|
|
166
|
+
.filter((entry) => entry.vod?.id && entry.startMs !== null)
|
|
167
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
168
|
+
|
|
169
|
+
return normalizedItems
|
|
170
|
+
.map((entry, index) => scoreTwitchVodCandidate(entry.vod, {
|
|
171
|
+
...context,
|
|
172
|
+
nextVodStartMs: normalizedItems[index + 1]?.startMs ?? null
|
|
173
|
+
}))
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.sort((a, b) => b.score - a.score)[0] || null;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const valuesEqual = (left, right) => {
|
|
179
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
180
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
181
|
+
return left.every((value, index) => valuesEqual(value, right[index]));
|
|
182
|
+
}
|
|
183
|
+
return left === right;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const recordHasConflictingIdentity = (record, source) => {
|
|
187
|
+
const recordStreamId = normalizeText(record?.streamId);
|
|
188
|
+
const sourceStreamId = normalizeText(source?.streamId);
|
|
189
|
+
if (recordStreamId && sourceStreamId && recordStreamId !== sourceStreamId) return true;
|
|
190
|
+
const recordVodId = normalizeText(record?.vodId) || getFirstMatchingUrlValue(record, extractTwitchVodIdFromUrl);
|
|
191
|
+
const sourceVodId = normalizeText(source?.vodId) || getFirstMatchingUrlValue(source, extractTwitchVodIdFromUrl);
|
|
192
|
+
if (recordVodId && sourceVodId && recordVodId !== sourceVodId) return true;
|
|
193
|
+
return false;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const buildRecordTimeLabelVodPatchOperationId = (recordId, patch = {}) => (
|
|
197
|
+
`record.update:${normalizeId(recordId)}:vod-patch:${fingerprintCanonicalJson(patch || {})}`
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
export const buildKnownTwitchVodRecordPatches = ({
|
|
201
|
+
records = [],
|
|
202
|
+
groupIndex = null,
|
|
203
|
+
fallbackPlatform,
|
|
204
|
+
unknownTitle = DEFAULT_UNKNOWN_TITLE
|
|
205
|
+
} = {}) => {
|
|
206
|
+
const safeRecords = collectNonVirtualRecords(records);
|
|
207
|
+
const index = groupIndex || buildRecordTimeLabelGroupIndex({
|
|
208
|
+
records: safeRecords,
|
|
209
|
+
fallbackPlatform,
|
|
210
|
+
unknownTitle
|
|
211
|
+
});
|
|
212
|
+
const conflictGroupIds = new Set(index.diagnostics?.conflictGroupIds || []);
|
|
213
|
+
const sourceByGroupId = {};
|
|
214
|
+
|
|
215
|
+
safeRecords.forEach((record) => {
|
|
216
|
+
if (normalizeRecordPlatform(record, fallbackPlatform) !== 'twitch' || record?.hasVod !== true) return;
|
|
217
|
+
const recordId = normalizeId(record?.id);
|
|
218
|
+
const groupId = index.groupIdByRecordId[recordId];
|
|
219
|
+
if (!groupId || conflictGroupIds.has(groupId)) return;
|
|
220
|
+
const vodId = normalizeText(record?.vodId) || getFirstMatchingUrlValue(record, extractTwitchVodIdFromUrl);
|
|
221
|
+
if (!vodId) return;
|
|
222
|
+
const current = sourceByGroupId[groupId];
|
|
223
|
+
const currentScore = current
|
|
224
|
+
? Number(Boolean(current.vodStartAt || current.vodPublishedAt)) +
|
|
225
|
+
Number(Number.isFinite(Number(current.vodTimeOffsetSeconds)))
|
|
226
|
+
: -1;
|
|
227
|
+
const nextScore = Number(Boolean(record.vodStartAt || record.vodPublishedAt)) +
|
|
228
|
+
Number(Number.isFinite(Number(record.vodTimeOffsetSeconds)));
|
|
229
|
+
if (!current || nextScore > currentScore) sourceByGroupId[groupId] = record;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const patches = [];
|
|
233
|
+
safeRecords.forEach((record) => {
|
|
234
|
+
const recordId = normalizeId(record?.id);
|
|
235
|
+
if (!recordId || normalizeRecordPlatform(record, fallbackPlatform) !== 'twitch' || record.hasVod === true) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const groupId = index.groupIdByRecordId[recordId];
|
|
239
|
+
if (!groupId || conflictGroupIds.has(groupId)) return;
|
|
240
|
+
const source = sourceByGroupId[groupId];
|
|
241
|
+
if (!source || recordHasConflictingIdentity(record, source)) return;
|
|
242
|
+
const vodId = normalizeText(source.vodId) || getFirstMatchingUrlValue(source, extractTwitchVodIdFromUrl);
|
|
243
|
+
if (!vodId) return;
|
|
244
|
+
|
|
245
|
+
const vodStartAt = source.vodStartAt || source.vodPublishedAt || null;
|
|
246
|
+
const vodStartMs = parseUtcMillis(vodStartAt);
|
|
247
|
+
const recordCreatedAtMs = parseUtcMillis(record.createdAt);
|
|
248
|
+
const configuredOffsetSeconds = Number.isFinite(Number(record.timeOffsetSeconds))
|
|
249
|
+
? Math.max(0, Number(record.timeOffsetSeconds))
|
|
250
|
+
: 0;
|
|
251
|
+
const timestampOffsetSeconds = vodStartMs !== null && recordCreatedAtMs !== null &&
|
|
252
|
+
recordCreatedAtMs >= vodStartMs
|
|
253
|
+
? Math.max(0, Math.floor((recordCreatedAtMs - vodStartMs) / 1000) - configuredOffsetSeconds)
|
|
254
|
+
: null;
|
|
255
|
+
const fallbackOffsetSeconds = parseElapsedSeconds(record.liveTime || record.originalLiveTime);
|
|
256
|
+
const vodTimeOffsetSeconds = timestampOffsetSeconds ?? fallbackOffsetSeconds;
|
|
257
|
+
const sourceVideoUrl = getRecordUrls(source).find((url) => extractTwitchVodIdFromUrl(url) === vodId) ||
|
|
258
|
+
`https://www.twitch.tv/videos/${vodId}`;
|
|
259
|
+
const vodMatchedBy = Array.from(new Set([
|
|
260
|
+
...(Array.isArray(source.vodMatchedBy) ? source.vodMatchedBy : []),
|
|
261
|
+
'sibling-record'
|
|
262
|
+
]));
|
|
263
|
+
const candidate = {
|
|
264
|
+
hasVod: true,
|
|
265
|
+
videoUrl: sourceVideoUrl,
|
|
266
|
+
vodId,
|
|
267
|
+
vodPublishedAt: source.vodPublishedAt || vodStartAt,
|
|
268
|
+
vodStartAt,
|
|
269
|
+
vodTitle: source.vodTitle || '',
|
|
270
|
+
vodLookupVersion: 2,
|
|
271
|
+
vodMatchedBy
|
|
272
|
+
};
|
|
273
|
+
if (Number.isFinite(vodTimeOffsetSeconds)) candidate.vodTimeOffsetSeconds = vodTimeOffsetSeconds;
|
|
274
|
+
if (Number.isFinite(Number(source.vodMatchConfidence))) {
|
|
275
|
+
candidate.vodMatchConfidence = Number(source.vodMatchConfidence);
|
|
276
|
+
}
|
|
277
|
+
if (source.downloadUrl) {
|
|
278
|
+
candidate.downloadUrl = source.downloadUrl;
|
|
279
|
+
candidate.isDirectM3u8 = source.isDirectM3u8 || false;
|
|
280
|
+
candidate.m3u8Type = source.m3u8Type || null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const patch = {};
|
|
284
|
+
Object.entries(candidate).forEach(([field, value]) => {
|
|
285
|
+
if (!valuesEqual(record[field], value)) patch[field] = value;
|
|
286
|
+
});
|
|
287
|
+
if (Object.keys(patch).length === 0) return;
|
|
288
|
+
patches.push({
|
|
289
|
+
recordId,
|
|
290
|
+
patch,
|
|
291
|
+
operationId: buildRecordTimeLabelVodPatchOperationId(recordId, patch)
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
patches,
|
|
297
|
+
diagnostics: {
|
|
298
|
+
sourceGroupCount: Object.keys(sourceByGroupId).length,
|
|
299
|
+
patchCount: patches.length,
|
|
300
|
+
skippedConflictGroupCount: conflictGroupIds.size
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
};
|
package/src/domain.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const RECORD_TIMELABEL_DOMAIN_CAPABILITIES = Object.freeze([
|
|
2
|
+
'group-identity-v1',
|
|
3
|
+
'twitch-vod-matching-v1',
|
|
4
|
+
'legacy-import-v1',
|
|
5
|
+
'channel-folder-planning-v1'
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
RECORD_GROUP_IDENTITY_VERSION,
|
|
10
|
+
buildLegacyRecordTimeLabelGroupId,
|
|
11
|
+
buildRecordTimeLabelGroupIndex,
|
|
12
|
+
buildRecordTimeLabelGroupMetadata,
|
|
13
|
+
buildRecordTimeLabelGroupReorderOperationId,
|
|
14
|
+
buildRecordTimeLabelStandaloneGroupId,
|
|
15
|
+
canonicalizeRecordTimeLabelGroupView
|
|
16
|
+
} from './domain/group-identity.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
extractTwitchVodIdFromUrl,
|
|
20
|
+
extractYouTubeVideoIdFromUrl
|
|
21
|
+
} from './domain/shared.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
buildKnownTwitchVodRecordPatches,
|
|
25
|
+
buildRecordTimeLabelVodPatchOperationId,
|
|
26
|
+
calculateVodTitleSimilarity,
|
|
27
|
+
normalizeVodTitleForMatch,
|
|
28
|
+
scoreTwitchVodCandidate,
|
|
29
|
+
selectBestMatchingTwitchVod
|
|
30
|
+
} from './domain/twitch-vod.js';
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
normalizeLegacyRecordTimeLabelImport,
|
|
34
|
+
remapLegacyRecordTimeLabelFolderAliases
|
|
35
|
+
} from './domain/legacy.js';
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
buildRecordTimeLabelChannelFolderPlan,
|
|
39
|
+
collectRecordTimeLabelChannelCandidates,
|
|
40
|
+
extractChannelHandleFromUrl,
|
|
41
|
+
findBestRecordTimeLabelChannelFolder,
|
|
42
|
+
getChannelMatchKey,
|
|
43
|
+
isPendingChannelFolderId,
|
|
44
|
+
isValidChannelName
|
|
45
|
+
} from './domain/channel-folder.js';
|
package/src/index.js
CHANGED
|
@@ -31,7 +31,63 @@ const REQUIRED_FOLDERS = [
|
|
|
31
31
|
{ id: DEFAULT_FOLDER_ID, name: 'Uncategorized' }
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
import {
|
|
35
|
+
RECORD_GROUP_IDENTITY_VERSION,
|
|
36
|
+
RECORD_TIMELABEL_DOMAIN_CAPABILITIES,
|
|
37
|
+
buildKnownTwitchVodRecordPatches,
|
|
38
|
+
buildLegacyRecordTimeLabelGroupId,
|
|
39
|
+
buildRecordTimeLabelChannelFolderPlan,
|
|
40
|
+
buildRecordTimeLabelGroupIndex,
|
|
41
|
+
buildRecordTimeLabelGroupMetadata,
|
|
42
|
+
buildRecordTimeLabelGroupReorderOperationId,
|
|
43
|
+
buildRecordTimeLabelStandaloneGroupId,
|
|
44
|
+
buildRecordTimeLabelVodPatchOperationId,
|
|
45
|
+
calculateVodTitleSimilarity,
|
|
46
|
+
canonicalizeRecordTimeLabelGroupView,
|
|
47
|
+
collectRecordTimeLabelChannelCandidates,
|
|
48
|
+
extractChannelHandleFromUrl,
|
|
49
|
+
extractTwitchVodIdFromUrl,
|
|
50
|
+
extractYouTubeVideoIdFromUrl,
|
|
51
|
+
findBestRecordTimeLabelChannelFolder,
|
|
52
|
+
getChannelMatchKey,
|
|
53
|
+
isPendingChannelFolderId,
|
|
54
|
+
isValidChannelName,
|
|
55
|
+
normalizeLegacyRecordTimeLabelImport,
|
|
56
|
+
normalizeVodTitleForMatch,
|
|
57
|
+
remapLegacyRecordTimeLabelFolderAliases,
|
|
58
|
+
scoreTwitchVodCandidate,
|
|
59
|
+
selectBestMatchingTwitchVod
|
|
60
|
+
} from './domain.js';
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
RECORD_GROUP_IDENTITY_VERSION,
|
|
64
|
+
RECORD_TIMELABEL_DOMAIN_CAPABILITIES,
|
|
65
|
+
buildKnownTwitchVodRecordPatches,
|
|
66
|
+
buildLegacyRecordTimeLabelGroupId,
|
|
67
|
+
buildRecordTimeLabelChannelFolderPlan,
|
|
68
|
+
buildRecordTimeLabelGroupIndex,
|
|
69
|
+
buildRecordTimeLabelGroupMetadata,
|
|
70
|
+
buildRecordTimeLabelGroupReorderOperationId,
|
|
71
|
+
buildRecordTimeLabelStandaloneGroupId,
|
|
72
|
+
buildRecordTimeLabelVodPatchOperationId,
|
|
73
|
+
calculateVodTitleSimilarity,
|
|
74
|
+
canonicalizeRecordTimeLabelGroupView,
|
|
75
|
+
collectRecordTimeLabelChannelCandidates,
|
|
76
|
+
extractChannelHandleFromUrl,
|
|
77
|
+
extractTwitchVodIdFromUrl,
|
|
78
|
+
extractYouTubeVideoIdFromUrl,
|
|
79
|
+
findBestRecordTimeLabelChannelFolder,
|
|
80
|
+
getChannelMatchKey,
|
|
81
|
+
isPendingChannelFolderId,
|
|
82
|
+
isValidChannelName,
|
|
83
|
+
normalizeLegacyRecordTimeLabelImport,
|
|
84
|
+
normalizeVodTitleForMatch,
|
|
85
|
+
remapLegacyRecordTimeLabelFolderAliases,
|
|
86
|
+
scoreTwitchVodCandidate,
|
|
87
|
+
selectBestMatchingTwitchVod
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.5.0';
|
|
35
91
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
36
92
|
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
37
93
|
'fifo-retry-fence',
|
|
@@ -5289,6 +5345,8 @@ export const buildOperationsFromSnapshotDiff = ({
|
|
|
5289
5345
|
|
|
5290
5346
|
export default {
|
|
5291
5347
|
RECORD_TIMELABEL_CORE_VERSION,
|
|
5348
|
+
RECORD_GROUP_IDENTITY_VERSION,
|
|
5349
|
+
RECORD_TIMELABEL_DOMAIN_CAPABILITIES,
|
|
5292
5350
|
RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES,
|
|
5293
5351
|
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE,
|
|
5294
5352
|
RECORD_TIMELABEL_CAPABILITY_OPERATION_CONFLICT_QUARANTINE,
|