@chainlesschain/personal-data-hub 0.4.7 → 0.4.23
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/biz-tianyancha.test.js +159 -0
- package/__tests__/adapters/doc-baidu-netdisk.test.js +102 -0
- package/__tests__/adapters/doc-camscanner.test.js +147 -0
- package/__tests__/adapters/doc-platforms.test.js +177 -0
- package/__tests__/adapters/gov-ixiamen.test.js +150 -0
- package/__tests__/adapters/gov-tax.test.js +135 -0
- package/__tests__/adapters/health-meiyou.test.js +125 -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-dongchedi.test.js +165 -0
- package/__tests__/adapters/social-zhihu.test.js +246 -0
- 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/__tests__/adapters/video-xigua.test.js +106 -0
- package/__tests__/adapters/wework-pc.test.js +124 -0
- package/lib/adapter-guide.js +25 -3
- package/lib/adapters/_document-base.js +370 -0
- package/lib/adapters/_video-base.js +331 -0
- package/lib/adapters/biz-tianyancha/index.js +348 -0
- package/lib/adapters/doc-baidu-netdisk/index.js +91 -0
- package/lib/adapters/doc-camscanner/index.js +102 -0
- package/lib/adapters/doc-tencent-docs/index.js +94 -0
- package/lib/adapters/doc-wps/index.js +77 -0
- package/lib/adapters/gov-ixiamen/index.js +380 -0
- package/lib/adapters/gov-tax/index.js +451 -0
- package/lib/adapters/health-meiyou/index.js +393 -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-dongchedi/index.js +360 -0
- package/lib/adapters/social-zhihu/index.js +488 -0
- 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/adapters/video-xigua/index.js +68 -0
- package/lib/adapters/wework-pc/index.js +31 -0
- package/lib/index.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §A12 — Kugou 酷狗音乐 (com.kugou.android) adapter, dual-mode (snapshot +
|
|
3
|
+
* cookie-api). Phase 13+ §12.1 line-781 ROI ⭐⭐ "听歌历史".
|
|
4
|
+
*
|
|
5
|
+
* Mirrors netease-music's three-kind shape (play / favorite / playlist) so the
|
|
6
|
+
* vault treats music listening uniformly across providers. Unlike netease (which
|
|
7
|
+
* needs weapi signing via a dedicated api-client), Kugou's web endpoints are
|
|
8
|
+
* fetched through a generic injected `fetchFn` + optional signProvider seam,
|
|
9
|
+
* keeping this module a pure-Node parser/orchestrator.
|
|
10
|
+
*
|
|
11
|
+
* 1. snapshot mode (opts.inputPath): JSON schemaVersion 1, stateless.
|
|
12
|
+
* 2. cookie-api mode (opts.account.cookies): fetch play history / favourites /
|
|
13
|
+
* playlists from kugou web via the injected fetchFn, paginate; sign seam
|
|
14
|
+
* (opts.signProvider) for any anti-bot token; endpoints overridable via
|
|
15
|
+
* opts.*Url (best-effort, not field-verified — FAMILY-23 playbook).
|
|
16
|
+
*
|
|
17
|
+
* Snapshot schema (schemaVersion 1, mirrors netease-music):
|
|
18
|
+
* {
|
|
19
|
+
* "schemaVersion": 1, "snapshottedAt": <ms>,
|
|
20
|
+
* "account": { "userId": "...", "name": "..." },
|
|
21
|
+
* "events": [
|
|
22
|
+
* { "kind": "play", "id": "...", "songId": "...", "song": "...",
|
|
23
|
+
* "artist": "...", "album": "...", "playCount": N, "capturedAt": <ms> },
|
|
24
|
+
* { "kind": "favorite", "id": "...", "songId": "...", "song": "...", "artist": "..." },
|
|
25
|
+
* { "kind": "playlist", "id": "...", "playlistId": "...", "name": "...",
|
|
26
|
+
* "trackCount": N, "creator": "..." }
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
"use strict";
|
|
32
|
+
|
|
33
|
+
const fs = require("node:fs");
|
|
34
|
+
const { newId } = require("../../ids");
|
|
35
|
+
const {
|
|
36
|
+
ENTITY_TYPES,
|
|
37
|
+
EVENT_SUBTYPES,
|
|
38
|
+
ITEM_SUBTYPES,
|
|
39
|
+
CAPTURED_BY,
|
|
40
|
+
} = require("../../constants");
|
|
41
|
+
const { CookieAuth } = require("../shopping-base");
|
|
42
|
+
|
|
43
|
+
const NAME = "music-kugou";
|
|
44
|
+
const VERSION = "0.1.0";
|
|
45
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
46
|
+
|
|
47
|
+
const KIND_PLAY = "play";
|
|
48
|
+
const KIND_FAVORITE = "favorite";
|
|
49
|
+
const KIND_PLAYLIST = "playlist";
|
|
50
|
+
const VALID_KINDS = Object.freeze([KIND_PLAY, KIND_FAVORITE, KIND_PLAYLIST]);
|
|
51
|
+
|
|
52
|
+
// Best-effort Kugou web endpoints. Overridable via opts.*Url.
|
|
53
|
+
const PLAYS_URL = "https://www.kugou.com/api/v3/user/listen/list";
|
|
54
|
+
const FAVORITES_URL = "https://www.kugou.com/api/v3/user/favorite/list";
|
|
55
|
+
const PLAYLISTS_URL = "https://www.kugou.com/api/v3/user/playlist/list";
|
|
56
|
+
const PAGE_SIZE = 30;
|
|
57
|
+
|
|
58
|
+
function parseTime(v) {
|
|
59
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v >= 1e9 ? v * 1000 : v;
|
|
60
|
+
if (typeof v === "string") {
|
|
61
|
+
if (/^\d+$/.test(v)) {
|
|
62
|
+
const n = parseInt(v, 10);
|
|
63
|
+
return n > 1e12 ? n : n >= 1e9 ? n * 1000 : n;
|
|
64
|
+
}
|
|
65
|
+
const t = Date.parse(v);
|
|
66
|
+
return Number.isFinite(t) ? t : null;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stableOriginalId(kind, id) {
|
|
72
|
+
const safe =
|
|
73
|
+
(typeof id === "string" && id.length > 0 && id) ||
|
|
74
|
+
(typeof id === "number" && Number.isFinite(id) && String(id)) ||
|
|
75
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
76
|
+
return `kugou:${kind}:${safe}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Split Kugou's "歌手 - 歌名" filename into {song, artist} when discrete
|
|
80
|
+
* fields are absent. */
|
|
81
|
+
function splitFilename(name) {
|
|
82
|
+
if (typeof name !== "string" || !name.includes(" - ")) return null;
|
|
83
|
+
const idx = name.indexOf(" - ");
|
|
84
|
+
return { artist: name.slice(0, idx).trim(), song: name.slice(idx + 3).trim() };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class KugouMusicAdapter {
|
|
88
|
+
constructor(opts = {}) {
|
|
89
|
+
this.account = opts.account || null;
|
|
90
|
+
this._cookieAuth =
|
|
91
|
+
opts.account && opts.account.cookies
|
|
92
|
+
? new CookieAuth({ platform: "kugou", cookies: opts.account.cookies })
|
|
93
|
+
: null;
|
|
94
|
+
this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
|
|
95
|
+
this._signProvider =
|
|
96
|
+
typeof opts.signProvider === "function" ? opts.signProvider : null;
|
|
97
|
+
this._urls = {
|
|
98
|
+
play: opts.playsUrl || PLAYS_URL,
|
|
99
|
+
favorite: opts.favoritesUrl || FAVORITES_URL,
|
|
100
|
+
playlist: opts.playlistsUrl || PLAYLISTS_URL,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
this.name = NAME;
|
|
104
|
+
this.version = VERSION;
|
|
105
|
+
this.capabilities = [
|
|
106
|
+
"sync:snapshot",
|
|
107
|
+
"sync:cookie-api",
|
|
108
|
+
"parse:kugou-play",
|
|
109
|
+
"parse:kugou-favorite",
|
|
110
|
+
"parse:kugou-playlist",
|
|
111
|
+
];
|
|
112
|
+
this.extractMode = "web-api";
|
|
113
|
+
this.rateLimits = {};
|
|
114
|
+
this.dataDisclosure = {
|
|
115
|
+
fields: [
|
|
116
|
+
"kugou:play (歌名 / 歌手 / 专辑)",
|
|
117
|
+
"kugou:favorite (收藏的歌)",
|
|
118
|
+
"kugou:playlist (歌单名 / 曲目数)",
|
|
119
|
+
],
|
|
120
|
+
sensitivity: "low",
|
|
121
|
+
legalGate: false,
|
|
122
|
+
defaultInclude: { play: true, favorite: true, playlist: true },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
this._deps = { fs };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async authenticate(ctx = {}) {
|
|
129
|
+
if (ctx && typeof ctx.inputPath === "string" && ctx.inputPath.length > 0) {
|
|
130
|
+
try {
|
|
131
|
+
this._deps.fs.accessSync(ctx.inputPath, this._deps.fs.constants.R_OK);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
reason: "INPUT_PATH_UNREADABLE",
|
|
136
|
+
message: `snapshot not readable at ${ctx.inputPath}: ${err.message}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, mode: "snapshot-file" };
|
|
140
|
+
}
|
|
141
|
+
if (this._cookieAuth) {
|
|
142
|
+
const ok = await this._cookieAuth.validate();
|
|
143
|
+
if (!ok) return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
account: (this.account && this.account.userId) || null,
|
|
147
|
+
mode: "cookie",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
reason: "NO_INPUT",
|
|
153
|
+
message: "music-kugou.authenticate: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async healthCheck() {
|
|
158
|
+
if (this._cookieAuth) {
|
|
159
|
+
const r = await this.authenticate();
|
|
160
|
+
return r.ok ? { ok: true, lastChecked: Date.now() } : { ok: false, reason: r.reason, error: r.error };
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, lastChecked: Date.now() };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async *sync(opts = {}) {
|
|
166
|
+
if (typeof opts.inputPath === "string" && opts.inputPath.length > 0) {
|
|
167
|
+
yield* this._syncViaSnapshot(opts);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (this._cookieAuth) {
|
|
171
|
+
yield* this._syncViaCookie(opts);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
throw new Error(
|
|
175
|
+
"music-kugou.sync: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async *_syncViaSnapshot(opts) {
|
|
180
|
+
const raw = this._deps.fs.readFileSync(opts.inputPath, "utf-8");
|
|
181
|
+
const snapshot = JSON.parse(raw);
|
|
182
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`music-kugou.sync: snapshot schemaVersion mismatch (got ${snapshot && snapshot.schemaVersion}, expected ${SNAPSHOT_SCHEMA_VERSION})`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const fallback =
|
|
188
|
+
Number.isFinite(snapshot.snapshottedAt) && snapshot.snapshottedAt > 0
|
|
189
|
+
? Math.floor(snapshot.snapshottedAt)
|
|
190
|
+
: Date.now();
|
|
191
|
+
const account = snapshot.account && typeof snapshot.account === "object" ? snapshot.account : null;
|
|
192
|
+
const include = opts.include || {};
|
|
193
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
194
|
+
const events = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
195
|
+
let emitted = 0;
|
|
196
|
+
for (const ev of events) {
|
|
197
|
+
if (emitted >= limit) return;
|
|
198
|
+
if (!ev || typeof ev !== "object" || !VALID_KINDS.includes(ev.kind)) continue;
|
|
199
|
+
if (include[ev.kind] === false) continue;
|
|
200
|
+
const id = (typeof ev.id === "string" && ev.id) || ev.songId || ev.playlistId || null;
|
|
201
|
+
yield {
|
|
202
|
+
adapter: NAME,
|
|
203
|
+
kind: ev.kind,
|
|
204
|
+
originalId: stableOriginalId(ev.kind, id),
|
|
205
|
+
capturedAt: parseTime(ev.capturedAt) || fallback,
|
|
206
|
+
payload: { ...ev, account },
|
|
207
|
+
};
|
|
208
|
+
emitted += 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async *_syncViaCookie(opts = {}) {
|
|
213
|
+
if (!(await this._cookieAuth.validate())) return;
|
|
214
|
+
const cookies = this._cookieAuth.toHeader();
|
|
215
|
+
const include = opts.include || {};
|
|
216
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
217
|
+
const maxPages = Number.isInteger(opts.maxPages) && opts.maxPages > 0 ? opts.maxPages : 10;
|
|
218
|
+
|
|
219
|
+
const plan = [
|
|
220
|
+
{ kind: KIND_PLAY, url: this._urls.play, map: songItemToRecord },
|
|
221
|
+
{ kind: KIND_FAVORITE, url: this._urls.favorite, map: songItemToRecord },
|
|
222
|
+
{ kind: KIND_PLAYLIST, url: this._urls.playlist, map: playlistItemToRecord },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
let emitted = 0;
|
|
226
|
+
for (const step of plan) {
|
|
227
|
+
if (include[step.kind] === false) continue;
|
|
228
|
+
let page = 1;
|
|
229
|
+
while (page <= maxPages) {
|
|
230
|
+
const query = { page, pagesize: PAGE_SIZE };
|
|
231
|
+
let sign = null;
|
|
232
|
+
if (this._signProvider) {
|
|
233
|
+
sign = await this._signProvider({ url: step.url, query, cookies });
|
|
234
|
+
}
|
|
235
|
+
const resp = await this._fetchFn({ url: step.url, cookies, query, sign });
|
|
236
|
+
const items = extractList(resp);
|
|
237
|
+
if (!items.length) break;
|
|
238
|
+
for (const it of items) {
|
|
239
|
+
const rec = step.map(it);
|
|
240
|
+
if (!rec) continue;
|
|
241
|
+
if (emitted >= limit) return;
|
|
242
|
+
yield {
|
|
243
|
+
adapter: NAME,
|
|
244
|
+
kind: step.kind,
|
|
245
|
+
originalId: stableOriginalId(step.kind, rec.id),
|
|
246
|
+
capturedAt: rec.occurredAt || Date.now(),
|
|
247
|
+
payload: { ...rec, kind: step.kind, cookie: true },
|
|
248
|
+
};
|
|
249
|
+
emitted += 1;
|
|
250
|
+
}
|
|
251
|
+
if (items.length < PAGE_SIZE) break;
|
|
252
|
+
page += 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
normalize(raw) {
|
|
258
|
+
if (!raw || !raw.payload) throw new Error("KugouMusicAdapter.normalize: payload missing");
|
|
259
|
+
const kind = raw.kind || raw.payload.kind;
|
|
260
|
+
const ingestedAt = Date.now();
|
|
261
|
+
if (kind === KIND_PLAY) return normalizeSong(raw.payload, raw, ingestedAt, EVENT_SUBTYPES.MEDIA, "听了");
|
|
262
|
+
if (kind === KIND_FAVORITE) return normalizeSong(raw.payload, raw, ingestedAt, EVENT_SUBTYPES.LIKE, "收藏");
|
|
263
|
+
if (kind === KIND_PLAYLIST) return normalizePlaylist(raw.payload, raw, ingestedAt);
|
|
264
|
+
throw new Error(`KugouMusicAdapter.normalize: unknown kind ${kind}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── cookie response → intermediate record ───────────────────────────────────
|
|
269
|
+
|
|
270
|
+
function extractList(resp) {
|
|
271
|
+
if (!resp || typeof resp !== "object") return [];
|
|
272
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
273
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
274
|
+
const d = resp.data;
|
|
275
|
+
if (d && typeof d === "object") {
|
|
276
|
+
if (Array.isArray(d.list)) return d.list;
|
|
277
|
+
if (Array.isArray(d.info)) return d.info;
|
|
278
|
+
if (Array.isArray(d.songs)) return d.songs;
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function songItemToRecord(it) {
|
|
284
|
+
if (!it || typeof it !== "object") return null;
|
|
285
|
+
const id = it.hash || it.mixsongid || it.songid || it.audio_id || it.id;
|
|
286
|
+
if (!id) return null;
|
|
287
|
+
let song = it.songname || it.song_name || it.name || null;
|
|
288
|
+
let artist = it.singername || it.singer_name || it.author_name || it.singer || null;
|
|
289
|
+
if ((!song || !artist) && it.filename) {
|
|
290
|
+
const split = splitFilename(it.filename);
|
|
291
|
+
if (split) {
|
|
292
|
+
song = song || split.song;
|
|
293
|
+
artist = artist || split.artist;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
id: String(id),
|
|
298
|
+
songId: String(id),
|
|
299
|
+
song: song || "(未知歌曲)",
|
|
300
|
+
artist: artist || "",
|
|
301
|
+
album: it.album_name || it.albumname || it.album || null,
|
|
302
|
+
occurredAt: parseTime(it.addtime || it.playtime || it.update_time || it.timestamp),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function playlistItemToRecord(it) {
|
|
307
|
+
if (!it || typeof it !== "object") return null;
|
|
308
|
+
const id = it.listid || it.global_collection_id || it.specialid || it.id;
|
|
309
|
+
if (!id) return null;
|
|
310
|
+
return {
|
|
311
|
+
id: String(id),
|
|
312
|
+
playlistId: String(id),
|
|
313
|
+
name: it.name || it.list_name || it.specialname || "(未命名歌单)",
|
|
314
|
+
trackCount: it.count != null ? it.count : it.song_count != null ? it.song_count : null,
|
|
315
|
+
creator: it.nickname || it.list_create_username || it.creator || null,
|
|
316
|
+
occurredAt: parseTime(it.create_time || it.addtime),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── normalizers (mirror netease-music) ──────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
function buildSource(raw, occurredAt) {
|
|
323
|
+
return {
|
|
324
|
+
adapter: NAME,
|
|
325
|
+
adapterVersion: VERSION,
|
|
326
|
+
originalId: raw.originalId,
|
|
327
|
+
capturedAt: raw.capturedAt || occurredAt,
|
|
328
|
+
capturedBy: CAPTURED_BY.API,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeSong(p, raw, ingestedAt, subtype, verb) {
|
|
333
|
+
const occurredAt = parseTime(p.occurredAt || p.capturedAt) || raw.capturedAt || ingestedAt;
|
|
334
|
+
const source = buildSource(raw, occurredAt);
|
|
335
|
+
const song = p.song || "(未知歌曲)";
|
|
336
|
+
const artist = p.artist || "";
|
|
337
|
+
const songId = p.songId != null ? String(p.songId) : null;
|
|
338
|
+
const itemId = songId ? `item-kugou-song-${songId}` : `item-kugou-song-${newId()}`;
|
|
339
|
+
return {
|
|
340
|
+
events: [
|
|
341
|
+
{
|
|
342
|
+
id: newId(),
|
|
343
|
+
type: ENTITY_TYPES.EVENT,
|
|
344
|
+
subtype,
|
|
345
|
+
occurredAt,
|
|
346
|
+
actor: "person-self",
|
|
347
|
+
content: { title: `${verb}: ${song}${artist ? " - " + artist : ""}`, text: `${song} ${artist}`.trim() },
|
|
348
|
+
ingestedAt,
|
|
349
|
+
source,
|
|
350
|
+
extra: {
|
|
351
|
+
platform: "kugou",
|
|
352
|
+
song,
|
|
353
|
+
artist,
|
|
354
|
+
album: p.album || null,
|
|
355
|
+
songId,
|
|
356
|
+
playCount: p.playCount != null ? p.playCount : null,
|
|
357
|
+
itemRef: itemId,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
items: [
|
|
362
|
+
{
|
|
363
|
+
id: itemId,
|
|
364
|
+
type: ENTITY_TYPES.ITEM,
|
|
365
|
+
subtype: ITEM_SUBTYPES.MEDIA,
|
|
366
|
+
name: artist ? `${song} - ${artist}` : song,
|
|
367
|
+
ingestedAt,
|
|
368
|
+
source,
|
|
369
|
+
extra: { platform: "kugou", kind: "song", song, artist, album: p.album || null, songId },
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
persons: [],
|
|
373
|
+
places: [],
|
|
374
|
+
topics: [],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function normalizePlaylist(p, raw, ingestedAt) {
|
|
379
|
+
const occurredAt = parseTime(p.occurredAt || p.capturedAt) || raw.capturedAt || ingestedAt;
|
|
380
|
+
const source = buildSource(raw, occurredAt);
|
|
381
|
+
const pid = p.playlistId != null ? String(p.playlistId) : null;
|
|
382
|
+
return {
|
|
383
|
+
events: [],
|
|
384
|
+
persons: [],
|
|
385
|
+
places: [],
|
|
386
|
+
items: [],
|
|
387
|
+
topics: [
|
|
388
|
+
{
|
|
389
|
+
id: pid ? `topic-kugou-playlist-${pid}` : `topic-kugou-playlist-${newId()}`,
|
|
390
|
+
type: ENTITY_TYPES.TOPIC,
|
|
391
|
+
name: p.name || "(未命名歌单)",
|
|
392
|
+
ingestedAt,
|
|
393
|
+
source,
|
|
394
|
+
extra: {
|
|
395
|
+
platform: "kugou",
|
|
396
|
+
playlistId: pid,
|
|
397
|
+
trackCount: p.trackCount != null ? p.trackCount : null,
|
|
398
|
+
creator: p.creator || null,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function defaultFetch(_opts) {
|
|
406
|
+
throw new Error("music-kugou: no fetchFn configured for cookie-api mode");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
KugouMusicAdapter,
|
|
411
|
+
extractList,
|
|
412
|
+
songItemToRecord,
|
|
413
|
+
playlistItemToRecord,
|
|
414
|
+
NAME,
|
|
415
|
+
VERSION,
|
|
416
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
417
|
+
VALID_KINDS,
|
|
418
|
+
};
|