Repository navigation
Expand file tree
/
Copy pathbook-bootstrap.js
More file actions
104 lines (98 loc) · 5.1 KB
/
Copy pathbook-bootstrap.js
File metadata and controls
104 lines (98 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
'use strict';
/**
* Pure planning for the book bootstrap (own-episode-IDs plan P1, section 8 "Bootstrap"): maps one
* show's existing Qdrant points to (show, catalog key) through Podcast Index's episode list, and
* says which book rows to write and which payload stamps to set. No I/O here.
*
* - A point's payload `id` is the PI episode ID it was embedded with, and stays its served ID.
* - A point whose PI ID isn't in the list, or whose item has no GUID and no enclosure, is
* `eid_missing`: it keeps its identity and is retried later. A key is never guessed.
* - Two points with the same key (PI re-listed the episode under a new ID) share one book row;
* the extra PI IDs become aliases. Stage 4's served-ID naming removes the duplicate later.
* - A point whose PI ID is already another key's served ID gets no row, so the show's transaction
* never fails on it. The point belongs to the owner (it is named by that served ID):
* - owner in this show (a changed GUID under the same PI ID): stamped with the owner's `eid` and
* `catalog_key`, like any recorded point;
* - owner in another show (a duplicate feed: PI lists one podcast under two show IDs that share
* episode IDs): stamped `eid_shared` with the owning show.
*/
const { catalogKey, recipeId } = require('./episode-recipe-id');
const num = (v) => {
const n = Number(v);
return Number.isSafeInteger(n) ? n : null;
};
/**
* @param {number} sid show ID
* @param {{ id: string, payload: object }[]} points the show's Qdrant points
* @param {object[]} piItems PI `episodes/byfeedid` items
* @param {Map<number, string>} [ownerByServed] who already holds these PI IDs as served IDs ("sid\u0000key")
* @returns {{ items: object[], stamps: { pointId: string, eid: number, key: string }[],
* missing: string[], duplicates: number, shared: { pointId: string, ownerSid: number }[] }}
*/
function planShow(sid, points, piItems, ownerByServed = new Map()) {
const byPiId = new Map();
for (const e of piItems || []) {
const id = num(e?.id);
if (id != null && !byPiId.has(id)) byPiId.set(id, e);
}
const items = new Map();
const stamps = [];
const missing = [];
const shared = [];
let duplicates = 0;
for (const p of points) {
const piId = num(p.payload?.id);
const e = piId == null ? null : byPiId.get(piId);
const key = e ? catalogKey(e.guid, e.enclosureUrl) : null;
if (!key) {
missing.push(p.id);
continue;
}
const owner = ownerByServed.get(piId);
if (owner != null && owner !== `${sid}\u0000${key}`) {
const cut = owner.indexOf('\u0000');
const ownerSid = Number(owner.slice(0, cut));
const ownerKey = owner.slice(cut + 1);
if (ownerSid === sid) stamps.push({ pointId: p.id, eid: recipeId(sid, ownerKey), key: ownerKey });
else shared.push({ pointId: p.id, ownerSid });
continue;
}
const eid = recipeId(sid, key);
const existing = items.get(key);
if (existing) {
duplicates++;
if (piId !== existing.piId) existing.extraPiIds.push(piId);
} else {
items.set(key, {
key,
eid,
piId,
servedId: piId,
extraPiIds: [],
title: typeof e.title === 'string' ? e.title : null,
audioUrl: typeof e.enclosureUrl === 'string' ? e.enclosureUrl : null,
imageUrl: (typeof e.image === 'string' && e.image) || (typeof e.feedImage === 'string' && e.feedImage) || null,
duration: num(e.duration),
publishedAt: num(e.datePublished),
chaptersUrl: typeof e.chaptersUrl === 'string' ? e.chaptersUrl : null,
transcriptUrl: typeof e.transcriptUrl === 'string' ? e.transcriptUrl : null,
});
}
stamps.push({ pointId: p.id, eid, key });
}
return { items: [...items.values()], stamps, missing, duplicates, shared };
}
/** Qdrant batch operations that stamp a show's points (one value set per point). */
function stampOperations(plan) {
const ops = plan.stamps.map((s) => ({ set_payload: { payload: { eid: s.eid, catalog_key: s.key }, points: [s.pointId] } }));
// A point placed now (for example by the monthly --retry-missing pass), whether stamped or found to
// belong to a duplicate feed, drops its earlier mark, so the next retry doesn't take it again.
const placed = [...plan.stamps.map((s) => s.pointId), ...(plan.shared || []).map((s) => s.pointId)];
if (placed.length) ops.push({ delete_payload: { keys: ['eid_missing'], points: placed } });
if (plan.missing.length) ops.push({ set_payload: { payload: { eid_missing: true }, points: plan.missing } });
const byOwner = new Map();
for (const { pointId, ownerSid } of plan.shared || []) byOwner.set(ownerSid, [...(byOwner.get(ownerSid) || []), pointId]);
for (const [ownerSid, ids] of byOwner) ops.push({ set_payload: { payload: { eid_shared: ownerSid }, points: ids } });
return ops;
}
module.exports = { planShow, stampOperations };