@chainlesschain/personal-data-hub 0.4.6 → 0.4.18
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/__tests__/adapters/doc-baidu-netdisk.test.js +102 -0
- package/__tests__/adapters/doc-platforms.test.js +177 -0
- package/__tests__/adapters/music-kugou.test.js +187 -0
- package/__tests__/adapters/recruit-boss.test.js +180 -0
- package/__tests__/adapters/shopping-dianping.test.js +239 -0
- package/__tests__/adapters/social-csdn.test.js +175 -0
- package/__tests__/adapters/social-zhihu.test.js +246 -0
- package/__tests__/adapters/travel-12306.test.js +234 -1
- package/__tests__/adapters/travel-ctrip.test.js +175 -1
- package/__tests__/adapters/travel-didi.test.js +204 -0
- package/__tests__/adapters/travel-tongcheng.test.js +289 -0
- package/__tests__/adapters/video-platforms.test.js +152 -0
- package/lib/adapter-guide.js +13 -1
- package/lib/adapters/_document-base.js +370 -0
- package/lib/adapters/_video-base.js +331 -0
- package/lib/adapters/doc-baidu-netdisk/index.js +91 -0
- package/lib/adapters/doc-tencent-docs/index.js +94 -0
- package/lib/adapters/doc-wps/index.js +77 -0
- package/lib/adapters/music-kugou/index.js +418 -0
- package/lib/adapters/recruit-boss/index.js +442 -0
- package/lib/adapters/shopping-dianping/index.js +473 -0
- package/lib/adapters/social-csdn/index.js +444 -0
- package/lib/adapters/social-zhihu/index.js +488 -0
- package/lib/adapters/travel-12306/index.js +279 -5
- package/lib/adapters/travel-ctrip/index.js +255 -40
- package/lib/adapters/travel-didi/index.js +327 -0
- package/lib/adapters/travel-tongcheng/index.js +393 -0
- package/lib/adapters/video-iqiyi/index.js +75 -0
- package/lib/adapters/video-tencent/index.js +78 -0
- package/lib/index.js +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §9.3d — Tongcheng (同程旅行) order adapter, dual-mode (snapshot + cookie-api).
|
|
3
|
+
*
|
|
4
|
+
* 同程 (com.tongcheng.android) is a Phase 9 travel platform (ROI ⭐⭐⭐ in
|
|
5
|
+
* docs/design/Personal_Data_Hub_Architecture.md §12.1, "与携程互补") that was
|
|
6
|
+
* skipped when the travel 四件套 (amap/baidu/ctrip/12306) shipped. It is an OTA
|
|
7
|
+
* like 携程, so this adapter mirrors travel-ctrip: orders span flight / hotel /
|
|
8
|
+
* train / bus / scenery (门票) / cruise / car, each mapped to the appropriate
|
|
9
|
+
* `vehicleType` in the vendor-neutral TravelRecord.
|
|
10
|
+
*
|
|
11
|
+
* 1. snapshot / file-import mode (opts.inputPath | opts.dataPath): ingest a
|
|
12
|
+
* JSON dump from an Android in-APK collector / browser extension / curated
|
|
13
|
+
* file. account OPTIONAL (file-import is stateless).
|
|
14
|
+
*
|
|
15
|
+
* 2. cookie-api mode (opts.account.cookies, v0.1): fetch the Tongcheng order
|
|
16
|
+
* centre directly from the hub. After login on the ly.com domain the order
|
|
17
|
+
* list is reachable under the `.ly.com` cookie. As with the other travel /
|
|
18
|
+
* shopping adapters the actual HTTP call is delegated to an injected
|
|
19
|
+
* `fetchFn` (Android in-APK cc → OkHttp; desktop hub → Electron WebView net
|
|
20
|
+
* request) so this module stays a pure-Node parser + orchestrator. account
|
|
21
|
+
* OPTIONAL — the cookie carries identity.
|
|
22
|
+
*
|
|
23
|
+
* ── sign seam ──────────────────────────────────────────────────────────
|
|
24
|
+
* Tongcheng's H5 / m-API requests carry an anti-bot signature computed by
|
|
25
|
+
* client-side JS (analogous to 携程 mtgsig / 拼多多 anti_token). No pure-Node
|
|
26
|
+
* implementation survives the rotation, so signing is injected via
|
|
27
|
+
* `opts.signProvider` (or constructor `signProvider`). When absent the
|
|
28
|
+
* request is still issued unsigned — best-effort, the endpoint may reject
|
|
29
|
+
* it, which surfaces as zero events rather than a crash. The endpoint
|
|
30
|
+
* constant is best-effort and overridable via `opts.ordersUrl`; Tongcheng
|
|
31
|
+
* rotates H5 paths, so adjust / pass opts.ordersUrl if it drifts
|
|
32
|
+
* (FAMILY-23 playbook — endpoints are not field-verified here).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
"use strict";
|
|
36
|
+
|
|
37
|
+
const fs = require("node:fs");
|
|
38
|
+
const { normalizeTravelRecord, parseChineseDateTime } = require("../travel-base");
|
|
39
|
+
const { CookieAuth } = require("../shopping-base");
|
|
40
|
+
|
|
41
|
+
const NAME = "travel-tongcheng";
|
|
42
|
+
const VERSION = "0.1.0";
|
|
43
|
+
|
|
44
|
+
// Best-effort Tongcheng order-centre list endpoint. Overridable via
|
|
45
|
+
// opts.ordersUrl (Tongcheng rotates H5 paths; the injected fetchFn host may
|
|
46
|
+
// also point at whichever order API the captured cookie is currently scoped to).
|
|
47
|
+
const TONGCHENG_ORDERS_URL = "https://m.ly.com/order/orderList";
|
|
48
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
49
|
+
const DEFAULT_MAX_PAGES = 10;
|
|
50
|
+
|
|
51
|
+
const TYPE_MAP = {
|
|
52
|
+
flight: "flight",
|
|
53
|
+
airline: "flight",
|
|
54
|
+
jipiao: "flight",
|
|
55
|
+
机票: "flight",
|
|
56
|
+
hotel: "hotel",
|
|
57
|
+
jiudian: "hotel",
|
|
58
|
+
酒店: "hotel",
|
|
59
|
+
train: "train",
|
|
60
|
+
huoche: "train",
|
|
61
|
+
火车: "train",
|
|
62
|
+
火车票: "train",
|
|
63
|
+
bus: "bus",
|
|
64
|
+
qiche: "bus",
|
|
65
|
+
汽车: "bus",
|
|
66
|
+
汽车票: "bus",
|
|
67
|
+
coach: "bus",
|
|
68
|
+
scenery: "attraction",
|
|
69
|
+
jingdian: "attraction",
|
|
70
|
+
景点: "attraction",
|
|
71
|
+
门票: "attraction",
|
|
72
|
+
ticket: "attraction",
|
|
73
|
+
cruise: "cruise",
|
|
74
|
+
youlun: "cruise",
|
|
75
|
+
邮轮: "cruise",
|
|
76
|
+
car: "car",
|
|
77
|
+
yongche: "car",
|
|
78
|
+
用车: "car",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
class TongchengAdapter {
|
|
82
|
+
constructor(opts = {}) {
|
|
83
|
+
this.account = opts.account || null;
|
|
84
|
+
this._dataPath = opts.dataPath || null;
|
|
85
|
+
|
|
86
|
+
// cookie-api mode — activates when account.cookies is supplied.
|
|
87
|
+
this._cookieAuth =
|
|
88
|
+
opts.account && opts.account.cookies
|
|
89
|
+
? new CookieAuth({ platform: "tongcheng", cookies: opts.account.cookies })
|
|
90
|
+
: null;
|
|
91
|
+
this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
|
|
92
|
+
this._signProvider =
|
|
93
|
+
typeof opts.signProvider === "function" ? opts.signProvider : null;
|
|
94
|
+
this._ordersUrl =
|
|
95
|
+
typeof opts.ordersUrl === "string" && opts.ordersUrl.length > 0
|
|
96
|
+
? opts.ordersUrl
|
|
97
|
+
: TONGCHENG_ORDERS_URL;
|
|
98
|
+
|
|
99
|
+
this.name = NAME;
|
|
100
|
+
this.version = VERSION;
|
|
101
|
+
this.capabilities = [
|
|
102
|
+
"import:json",
|
|
103
|
+
"sync:snapshot",
|
|
104
|
+
"sync:cookie-api",
|
|
105
|
+
"parse:tongcheng-orders",
|
|
106
|
+
];
|
|
107
|
+
this.extractMode = "file-import";
|
|
108
|
+
this.rateLimits = {};
|
|
109
|
+
this.dataDisclosure = {
|
|
110
|
+
fields: [
|
|
111
|
+
"tongcheng:orderId / type / fromCity / toCity / dates / passengerName / price / carrier",
|
|
112
|
+
],
|
|
113
|
+
sensitivity: "medium",
|
|
114
|
+
legalGate: false,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// _deps injection seam — vi.mock fs doesn't intercept inlined CJS require.
|
|
118
|
+
this._deps = { fs };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async authenticate(ctx = {}) {
|
|
122
|
+
const filePath = (ctx && ctx.inputPath) || ctx.dataPath || this._dataPath;
|
|
123
|
+
if (filePath) {
|
|
124
|
+
try {
|
|
125
|
+
this._deps.fs.accessSync(filePath, this._deps.fs.constants.R_OK);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: "INPUT_PATH_UNREADABLE",
|
|
130
|
+
message: `not readable at ${filePath}: ${err.message}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, mode: "snapshot-file" };
|
|
134
|
+
}
|
|
135
|
+
if (this._cookieAuth) {
|
|
136
|
+
const ok = await this._cookieAuth.validate();
|
|
137
|
+
if (!ok) {
|
|
138
|
+
return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
|
|
139
|
+
}
|
|
140
|
+
// account is OPTIONAL in cookie mode — the .ly.com cookie carries identity.
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
account: (this.account && this.account.email) || null,
|
|
144
|
+
mode: "cookie",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
account: this.account ? this.account.email : null,
|
|
150
|
+
mode: "ready",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async healthCheck() {
|
|
155
|
+
if (this._cookieAuth) {
|
|
156
|
+
const r = await this.authenticate();
|
|
157
|
+
return r.ok
|
|
158
|
+
? { ok: true, lastChecked: Date.now() }
|
|
159
|
+
: { ok: false, reason: r.reason, error: r.error };
|
|
160
|
+
}
|
|
161
|
+
return { ok: true, lastChecked: Date.now() };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async *sync(opts = {}) {
|
|
165
|
+
const dataPath = opts.inputPath || opts.dataPath || this._dataPath;
|
|
166
|
+
if (dataPath) {
|
|
167
|
+
if (!this._deps.fs.existsSync(dataPath)) return;
|
|
168
|
+
const text = this._deps.fs.readFileSync(dataPath, "utf-8");
|
|
169
|
+
let records;
|
|
170
|
+
try {
|
|
171
|
+
records = parseRecords(text);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
throw new Error(`TongchengAdapter: parse failed: ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
for (const r of records) {
|
|
176
|
+
yield {
|
|
177
|
+
adapter: NAME,
|
|
178
|
+
originalId: r.recordId,
|
|
179
|
+
capturedAt: r.bookedAt || r.departureMs || Date.now(),
|
|
180
|
+
payload: { record: r },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this._cookieAuth) {
|
|
186
|
+
yield* this._syncViaCookie(opts);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* §9.3d — cookie-api live fetch. Hits the Tongcheng order-centre list
|
|
192
|
+
* endpoint via the injected fetchFn, paginates with a pageIndex cursor, stops
|
|
193
|
+
* at the sinceWatermark / maxPages, maps each order through orderToRecord (so
|
|
194
|
+
* the existing normalize path applies unchanged) and yields it.
|
|
195
|
+
*/
|
|
196
|
+
async *_syncViaCookie(opts = {}) {
|
|
197
|
+
if (!(await this._cookieAuth.validate())) return;
|
|
198
|
+
const cookies = this._cookieAuth.toHeader();
|
|
199
|
+
const sinceMs =
|
|
200
|
+
opts.sinceWatermark != null
|
|
201
|
+
? parseInt(String(opts.sinceWatermark), 10) || 0
|
|
202
|
+
: Date.now() - 365 * 24 * 3600_000; // default last year
|
|
203
|
+
const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
|
|
204
|
+
const maxPages =
|
|
205
|
+
Number.isInteger(opts.maxPages) && opts.maxPages > 0
|
|
206
|
+
? opts.maxPages
|
|
207
|
+
: DEFAULT_MAX_PAGES;
|
|
208
|
+
const limit =
|
|
209
|
+
Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
210
|
+
|
|
211
|
+
let emitted = 0;
|
|
212
|
+
let pageIndex = 1;
|
|
213
|
+
while (pageIndex <= maxPages) {
|
|
214
|
+
const query = { pageIndex, pageSize, ts: Date.now() };
|
|
215
|
+
// sign seam — best-effort. null when no signProvider.
|
|
216
|
+
let sign = null;
|
|
217
|
+
if (this._signProvider) {
|
|
218
|
+
sign = await this._signProvider({ url: this._ordersUrl, query, cookies });
|
|
219
|
+
}
|
|
220
|
+
const resp = await this._fetchFn({ url: this._ordersUrl, cookies, query, sign });
|
|
221
|
+
const orders = extractOrders(resp);
|
|
222
|
+
if (!orders.length) break;
|
|
223
|
+
|
|
224
|
+
let pageHasNew = false;
|
|
225
|
+
let reachedWatermark = false;
|
|
226
|
+
for (const raw of orders) {
|
|
227
|
+
const rec = orderToRecord(raw, { capturedVia: "cookie-api" });
|
|
228
|
+
if (!rec) continue;
|
|
229
|
+
const ts = rec.bookedAt || rec.departureMs || null;
|
|
230
|
+
if (ts && ts < sinceMs) {
|
|
231
|
+
reachedWatermark = true; // remaining orders are older
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
pageHasNew = true;
|
|
235
|
+
if (emitted >= limit) return;
|
|
236
|
+
yield {
|
|
237
|
+
adapter: NAME,
|
|
238
|
+
originalId: rec.recordId,
|
|
239
|
+
capturedAt: ts || Date.now(),
|
|
240
|
+
payload: { record: rec },
|
|
241
|
+
};
|
|
242
|
+
emitted += 1;
|
|
243
|
+
}
|
|
244
|
+
if (reachedWatermark || !pageHasNew || orders.length < pageSize) break;
|
|
245
|
+
pageIndex += 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
normalize(raw) {
|
|
250
|
+
if (!raw || !raw.payload || !raw.payload.record) {
|
|
251
|
+
throw new Error("TongchengAdapter.normalize: raw.payload.record missing");
|
|
252
|
+
}
|
|
253
|
+
return normalizeTravelRecord(raw.payload.record, {
|
|
254
|
+
adapterName: NAME,
|
|
255
|
+
adapterVersion: VERSION,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseRecords(text) {
|
|
261
|
+
let raw;
|
|
262
|
+
try {
|
|
263
|
+
raw = JSON.parse(text);
|
|
264
|
+
} catch (_e) {
|
|
265
|
+
// Try JSONL
|
|
266
|
+
raw = text
|
|
267
|
+
.split(/\r?\n/)
|
|
268
|
+
.filter((l) => l.trim().startsWith("{"))
|
|
269
|
+
.map((l) => JSON.parse(l));
|
|
270
|
+
}
|
|
271
|
+
const orders = Array.isArray(raw) ? raw : raw.orders || [];
|
|
272
|
+
return orders.map((o) => orderToRecord(o)).filter(Boolean);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Map one Tongcheng order object → vendor-neutral TravelRecord. Field names are
|
|
277
|
+
* best-effort across H5 versions (camelCase + snake_case + Chinese fallbacks).
|
|
278
|
+
*/
|
|
279
|
+
function orderToRecord(o, opts = {}) {
|
|
280
|
+
if (!o || typeof o !== "object") return null;
|
|
281
|
+
const recordId =
|
|
282
|
+
o.orderId || o.orderSerialId || o.serialId || o.id || o.order_no || o.orderNo;
|
|
283
|
+
if (!recordId) return null;
|
|
284
|
+
const type = (
|
|
285
|
+
o.type ||
|
|
286
|
+
o.orderType ||
|
|
287
|
+
o.projectType ||
|
|
288
|
+
o.projectTag ||
|
|
289
|
+
o.bizType ||
|
|
290
|
+
o.businessType ||
|
|
291
|
+
""
|
|
292
|
+
)
|
|
293
|
+
.toString()
|
|
294
|
+
.toLowerCase();
|
|
295
|
+
const vehicleType = TYPE_MAP[type] || "trip";
|
|
296
|
+
|
|
297
|
+
const priceRaw =
|
|
298
|
+
o.price != null
|
|
299
|
+
? o.price
|
|
300
|
+
: o.amount != null
|
|
301
|
+
? o.amount
|
|
302
|
+
: o.orderAmount != null
|
|
303
|
+
? o.orderAmount
|
|
304
|
+
: o.totalAmount != null
|
|
305
|
+
? o.totalAmount
|
|
306
|
+
: o.payAmount != null
|
|
307
|
+
? o.payAmount
|
|
308
|
+
: o.totalPrice != null
|
|
309
|
+
? o.totalPrice
|
|
310
|
+
: null;
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
vendorId: "tongcheng",
|
|
314
|
+
recordId: String(recordId),
|
|
315
|
+
vehicleType,
|
|
316
|
+
from:
|
|
317
|
+
o.fromCity || o.from_city || o.depCity || o.departureCity
|
|
318
|
+
? { city: o.fromCity || o.from_city || o.depCity || o.departureCity }
|
|
319
|
+
: null,
|
|
320
|
+
to:
|
|
321
|
+
o.toCity || o.to_city || o.arrCity || o.arrivalCity || o.hotelCity || o.sceneryName
|
|
322
|
+
? { city: o.toCity || o.to_city || o.arrCity || o.arrivalCity || o.hotelCity || o.sceneryName }
|
|
323
|
+
: null,
|
|
324
|
+
departureMs: numberOrParse(
|
|
325
|
+
o.departureTime || o.dep_time || o.departureDate || o.useDate || o.checkIn || o.check_in || o.startDate,
|
|
326
|
+
),
|
|
327
|
+
arrivalMs: numberOrParse(
|
|
328
|
+
o.arrivalTime || o.arr_time || o.arrivalDate || o.checkOut || o.check_out || o.endDate,
|
|
329
|
+
),
|
|
330
|
+
carrier:
|
|
331
|
+
o.carrier || o.airline || o.airlineName || o.hotelName || o.hotel_name || o.sceneryName || o.title || "同程",
|
|
332
|
+
vehicleNumber: o.flightNumber || o.flight_no || o.trainNumber || o.train_no || o.trainNo,
|
|
333
|
+
totalCost:
|
|
334
|
+
priceRaw != null
|
|
335
|
+
? { value: parseFloat(priceRaw), currency: o.currency || "CNY" }
|
|
336
|
+
: null,
|
|
337
|
+
traveler:
|
|
338
|
+
o.passengerName || o.passenger || o.guestName || o.guest_name || o.linkName || o.contactName,
|
|
339
|
+
confirmationCode: o.confirmationCode || o.pnr || o.confirmation_no || o.serialId,
|
|
340
|
+
bookedAt: numberOrParse(o.bookedAt || o.order_time || o.orderDate || o.createDate || o.createTime),
|
|
341
|
+
extras: {
|
|
342
|
+
type,
|
|
343
|
+
...(o.nights != null ? { nights: o.nights } : {}),
|
|
344
|
+
...(opts.capturedVia ? { capturedVia: opts.capturedVia } : {}),
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Pull the order array out of a Tongcheng order-centre response. Tongcheng
|
|
351
|
+
* nests the list under different keys across H5 versions; the injected fetchFn
|
|
352
|
+
* may also pre-flatten to `{ orders }`. Tolerant of all common shapes.
|
|
353
|
+
*/
|
|
354
|
+
function extractOrders(resp) {
|
|
355
|
+
if (!resp || typeof resp !== "object") return [];
|
|
356
|
+
if (Array.isArray(resp.orders)) return resp.orders;
|
|
357
|
+
if (Array.isArray(resp.orderList)) return resp.orderList;
|
|
358
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
359
|
+
const data = resp.data && typeof resp.data === "object" ? resp.data : null;
|
|
360
|
+
if (data) {
|
|
361
|
+
if (Array.isArray(data.orders)) return data.orders;
|
|
362
|
+
if (Array.isArray(data.orderList)) return data.orderList;
|
|
363
|
+
if (Array.isArray(data.list)) return data.list;
|
|
364
|
+
if (Array.isArray(data.records)) return data.records;
|
|
365
|
+
}
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function numberOrParse(v) {
|
|
370
|
+
if (Number.isFinite(v)) return v;
|
|
371
|
+
if (typeof v === "string") {
|
|
372
|
+
if (/^\d+$/.test(v) && v.length >= 10) return parseInt(v, 10);
|
|
373
|
+
return parseChineseDateTime(v);
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function defaultFetch(_opts) {
|
|
379
|
+
// Pure-Node has no HTTP layer; the host (Android cc → OkHttp; desktop hub →
|
|
380
|
+
// Electron WebView net) injects a real fetchFn. A missing fetchFn is a wiring
|
|
381
|
+
// bug, not a runtime data condition, so it throws loudly (mirrors travel-ctrip).
|
|
382
|
+
throw new Error("travel-tongcheng: no fetchFn configured for cookie-api mode");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
TongchengAdapter,
|
|
387
|
+
parseRecords,
|
|
388
|
+
orderToRecord,
|
|
389
|
+
extractOrders,
|
|
390
|
+
TYPE_MAP,
|
|
391
|
+
NAME,
|
|
392
|
+
VERSION,
|
|
393
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §13+ — 爱奇艺 (iQiyi, com.qiyi.video) adapter. §12.1 Phase 13+ ROI ⭐⭐
|
|
3
|
+
* "观看历史". Thin wrapper over _video-base.
|
|
4
|
+
*
|
|
5
|
+
* iQiyi exposes the user's watch history + favourites (追剧) via iqiyi.com APIs;
|
|
6
|
+
* this adapter supplies the endpoints + field mapping, the base handles snapshot
|
|
7
|
+
* + cookie-api orchestration + normalize (MEDIA / LIKE event + MEDIA item).
|
|
8
|
+
* Endpoints best-effort + overridable (not field-verified — FAMILY-23 playbook).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { createVideoAdapter, parseTime, SNAPSHOT_SCHEMA_VERSION } = require("../_video-base");
|
|
14
|
+
|
|
15
|
+
const NAME = "video-iqiyi";
|
|
16
|
+
const VERSION = "0.1.0";
|
|
17
|
+
|
|
18
|
+
const WATCH_URL = "https://l.rcd.iqiyi.com/apis/rcd/myRC";
|
|
19
|
+
const FAVOURITE_URL = "https://collect.if.iqiyi.com/japi/collect/list";
|
|
20
|
+
|
|
21
|
+
// iQiyi channel/category code → label (best-effort).
|
|
22
|
+
const CHANNEL_MAP = {
|
|
23
|
+
1: "movie",
|
|
24
|
+
2: "tv",
|
|
25
|
+
3: "variety",
|
|
26
|
+
4: "anime",
|
|
27
|
+
6: "documentary",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function mapCategory(it) {
|
|
31
|
+
const c = it.channelId != null ? it.channelId : it.chnId;
|
|
32
|
+
if (c != null && CHANNEL_MAP[c]) return CHANNEL_MAP[c];
|
|
33
|
+
return it.channelName || it.categoryName || it.category || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractItems(resp) {
|
|
37
|
+
if (!resp || typeof resp !== "object") return [];
|
|
38
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
39
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
40
|
+
const d = resp.data;
|
|
41
|
+
if (d && typeof d === "object") {
|
|
42
|
+
if (Array.isArray(d.list)) return d.list;
|
|
43
|
+
if (Array.isArray(d.records)) return d.records;
|
|
44
|
+
if (Array.isArray(d.rc)) return d.rc;
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mapItem(it) {
|
|
50
|
+
if (!it || typeof it !== "object") return null;
|
|
51
|
+
const videoId = it.tvId || it.tvid || it.albumId || it.qipuId || it.id;
|
|
52
|
+
if (!videoId) return null;
|
|
53
|
+
return {
|
|
54
|
+
videoId: String(videoId),
|
|
55
|
+
title: it.albumName || it.videoName || it.title || it.name || "(未知视频)",
|
|
56
|
+
category: mapCategory(it),
|
|
57
|
+
episode: it.videoName && it.albumName && it.videoName !== it.albumName ? it.videoName : it.order ? `第${it.order}集` : null,
|
|
58
|
+
channel: it.channelName || null,
|
|
59
|
+
durationSec: Number.isFinite(it.videoDuration) ? it.videoDuration : Number.isFinite(it.duration) ? it.duration : null,
|
|
60
|
+
url: it.pageUrl || it.url || (it.tvId ? `https://www.iqiyi.com/v_${it.tvId}.html` : null),
|
|
61
|
+
occurredAt: parseTime(it.addtime || it.playTime || it.updateTime || it.timestamp),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const IqiyiVideoAdapter = createVideoAdapter({
|
|
66
|
+
NAME,
|
|
67
|
+
VERSION,
|
|
68
|
+
platform: "iqiyi",
|
|
69
|
+
watchUrl: WATCH_URL,
|
|
70
|
+
favouriteUrl: FAVOURITE_URL,
|
|
71
|
+
extractItems,
|
|
72
|
+
mapItem,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
module.exports = { IqiyiVideoAdapter, extractItems, mapItem, CHANNEL_MAP, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §13+ — 腾讯视频 (Tencent Video, com.tencent.qqlive) adapter. §12.1 Phase 13+
|
|
3
|
+
* ROI ⭐⭐ "观看历史". Thin wrapper over _video-base.
|
|
4
|
+
*
|
|
5
|
+
* Tencent Video exposes the user's watch history + 追剧/收藏 via v.qq.com APIs;
|
|
6
|
+
* this adapter supplies the endpoints + field mapping, the base handles snapshot
|
|
7
|
+
* + cookie-api orchestration + normalize. Endpoints best-effort + overridable
|
|
8
|
+
* (not field-verified — FAMILY-23 playbook).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { createVideoAdapter, parseTime, SNAPSHOT_SCHEMA_VERSION } = require("../_video-base");
|
|
14
|
+
|
|
15
|
+
const NAME = "video-tencent";
|
|
16
|
+
const VERSION = "0.1.0";
|
|
17
|
+
|
|
18
|
+
const WATCH_URL = "https://pbaccess.video.qq.com/trpc.v...history.HistoryServer/GetHistory";
|
|
19
|
+
const FAVOURITE_URL = "https://pbaccess.video.qq.com/trpc.v...favorite.FavoriteServer/GetFavorite";
|
|
20
|
+
|
|
21
|
+
const TYPE_MAP = {
|
|
22
|
+
1: "tv",
|
|
23
|
+
2: "movie",
|
|
24
|
+
3: "variety",
|
|
25
|
+
4: "anime",
|
|
26
|
+
10: "documentary",
|
|
27
|
+
movie: "movie",
|
|
28
|
+
tv: "tv",
|
|
29
|
+
variety: "variety",
|
|
30
|
+
anime: "anime",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function mapCategory(it) {
|
|
34
|
+
const raw = it.cTypeId != null ? it.cTypeId : it.typeId != null ? it.typeId : it.category;
|
|
35
|
+
const key = String(raw == null ? "" : raw).toLowerCase();
|
|
36
|
+
return TYPE_MAP[key] || TYPE_MAP[raw] || it.categoryName || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractItems(resp) {
|
|
40
|
+
if (!resp || typeof resp !== "object") return [];
|
|
41
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
42
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
43
|
+
const d = resp.data;
|
|
44
|
+
if (d && typeof d === "object") {
|
|
45
|
+
if (Array.isArray(d.list)) return d.list;
|
|
46
|
+
if (Array.isArray(d.records)) return d.records;
|
|
47
|
+
if (Array.isArray(d.videoList)) return d.videoList;
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mapItem(it) {
|
|
53
|
+
if (!it || typeof it !== "object") return null;
|
|
54
|
+
const videoId = it.cid || it.vid || it.lid || it.id;
|
|
55
|
+
if (!videoId) return null;
|
|
56
|
+
return {
|
|
57
|
+
videoId: String(videoId),
|
|
58
|
+
title: it.cTitle || it.title || it.videoTitle || it.name || "(未知视频)",
|
|
59
|
+
category: mapCategory(it),
|
|
60
|
+
episode: it.episode || it.vTitle || (it.episodeNum ? `第${it.episodeNum}集` : null),
|
|
61
|
+
channel: it.channelName || null,
|
|
62
|
+
durationSec: Number.isFinite(it.duration) ? it.duration : Number.isFinite(it.totalTime) ? it.totalTime : null,
|
|
63
|
+
url: it.url || (it.cid ? `https://v.qq.com/x/cover/${it.cid}.html` : null),
|
|
64
|
+
occurredAt: parseTime(it.viewTime || it.updateTime || it.markTime || it.time),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const TencentVideoAdapter = createVideoAdapter({
|
|
69
|
+
NAME,
|
|
70
|
+
VERSION,
|
|
71
|
+
platform: "tencent-video",
|
|
72
|
+
watchUrl: WATCH_URL,
|
|
73
|
+
favouriteUrl: FAVOURITE_URL,
|
|
74
|
+
extractItems,
|
|
75
|
+
mapItem,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
module.exports = { TencentVideoAdapter, extractItems, mapItem, TYPE_MAP, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
|
package/lib/index.js
CHANGED
|
@@ -39,6 +39,8 @@ const wechatAdapter = require("./adapters/wechat");
|
|
|
39
39
|
const travelBase = require("./adapters/travel-base");
|
|
40
40
|
const { Train12306Adapter } = require("./adapters/travel-12306");
|
|
41
41
|
const { CtripAdapter } = require("./adapters/travel-ctrip");
|
|
42
|
+
const { TongchengAdapter } = require("./adapters/travel-tongcheng");
|
|
43
|
+
const { DidiAdapter } = require("./adapters/travel-didi");
|
|
42
44
|
const { AmapAdapter } = require("./adapters/travel-amap");
|
|
43
45
|
const { BaiduMapAdapter } = require("./adapters/travel-baidu-map");
|
|
44
46
|
const { TencentMapAdapter } = require("./adapters/travel-tencent-map");
|
|
@@ -47,8 +49,12 @@ const { TaobaoAdapter } = require("./adapters/shopping-taobao");
|
|
|
47
49
|
const { JdAdapter } = require("./adapters/shopping-jd");
|
|
48
50
|
const { MeituanAdapter } = require("./adapters/shopping-meituan");
|
|
49
51
|
const { PinduoduoAdapter } = require("./adapters/shopping-pinduoduo");
|
|
52
|
+
const { DianpingAdapter } = require("./adapters/shopping-dianping");
|
|
50
53
|
const { BilibiliAdapter } = require("./adapters/social-bilibili");
|
|
51
54
|
const { WeiboAdapter } = require("./adapters/social-weibo");
|
|
55
|
+
const { ZhihuAdapter } = require("./adapters/social-zhihu");
|
|
56
|
+
const { BossZhipinAdapter } = require("./adapters/recruit-boss");
|
|
57
|
+
const { CsdnAdapter } = require("./adapters/social-csdn");
|
|
52
58
|
const { DouyinAdapter } = require("./adapters/social-douyin");
|
|
53
59
|
const { XiaohongshuAdapter } = require("./adapters/social-xiaohongshu");
|
|
54
60
|
const { ToutiaoAdapter } = require("./adapters/social-toutiao");
|
|
@@ -64,7 +70,13 @@ const { WeChatPcAdapter } = require("./adapters/wechat-pc");
|
|
|
64
70
|
const { QQPcAdapter } = require("./adapters/qq-pc");
|
|
65
71
|
const { AppleHealthAdapter } = require("./adapters/apple-health");
|
|
66
72
|
const { NeteaseMusicAdapter } = require("./adapters/netease-music");
|
|
73
|
+
const { KugouMusicAdapter } = require("./adapters/music-kugou");
|
|
74
|
+
const { IqiyiVideoAdapter } = require("./adapters/video-iqiyi");
|
|
75
|
+
const { TencentVideoAdapter } = require("./adapters/video-tencent");
|
|
67
76
|
const { WeReadAdapter } = require("./adapters/weread");
|
|
77
|
+
const { WpsDocAdapter } = require("./adapters/doc-wps");
|
|
78
|
+
const { TencentDocsAdapter } = require("./adapters/doc-tencent-docs");
|
|
79
|
+
const { BaiduNetdiskAdapter } = require("./adapters/doc-baidu-netdisk");
|
|
68
80
|
const { DingTalkPcAdapter } = require("./adapters/dingtalk-pc");
|
|
69
81
|
const { FeishuPcAdapter } = require("./adapters/feishu-pc");
|
|
70
82
|
const { TelegramAdapter } = require("./adapters/messaging-telegram");
|
|
@@ -264,6 +276,8 @@ module.exports = {
|
|
|
264
276
|
parseChineseDateTime: travelBase.parseChineseDateTime,
|
|
265
277
|
Train12306Adapter,
|
|
266
278
|
CtripAdapter,
|
|
279
|
+
TongchengAdapter,
|
|
280
|
+
DidiAdapter,
|
|
267
281
|
AmapAdapter,
|
|
268
282
|
BaiduMapAdapter,
|
|
269
283
|
TencentMapAdapter,
|
|
@@ -275,10 +289,14 @@ module.exports = {
|
|
|
275
289
|
JdAdapter,
|
|
276
290
|
MeituanAdapter,
|
|
277
291
|
PinduoduoAdapter,
|
|
292
|
+
DianpingAdapter,
|
|
278
293
|
|
|
279
294
|
// Phase 13+ — long-tail social + messaging (借 sjqz parsers)
|
|
280
295
|
BilibiliAdapter,
|
|
281
296
|
WeiboAdapter,
|
|
297
|
+
ZhihuAdapter,
|
|
298
|
+
BossZhipinAdapter,
|
|
299
|
+
CsdnAdapter,
|
|
282
300
|
DouyinAdapter,
|
|
283
301
|
XiaohongshuAdapter,
|
|
284
302
|
ToutiaoAdapter,
|
|
@@ -293,7 +311,13 @@ module.exports = {
|
|
|
293
311
|
QQPcAdapter,
|
|
294
312
|
AppleHealthAdapter,
|
|
295
313
|
NeteaseMusicAdapter,
|
|
314
|
+
KugouMusicAdapter,
|
|
315
|
+
IqiyiVideoAdapter,
|
|
316
|
+
TencentVideoAdapter,
|
|
296
317
|
WeReadAdapter,
|
|
318
|
+
WpsDocAdapter,
|
|
319
|
+
TencentDocsAdapter,
|
|
320
|
+
BaiduNetdiskAdapter,
|
|
297
321
|
DingTalkPcAdapter,
|
|
298
322
|
FeishuPcAdapter,
|
|
299
323
|
TelegramAdapter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainlesschain/personal-data-hub",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"description": "Personal Data Hub — UnifiedSchema + validators + KG ingest helpers for the data-back-to-the-individual middleware",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "lib/index.js",
|