@xiaohhhh1/canvas-agent 0.4.25 → 0.4.27
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Response } from "playwright-core";
|
|
1
2
|
export type FastMossRankingSource = "new" | "hot" | "video" | "sales";
|
|
2
3
|
export type FastMossPhase = "idle" | "saved" | "launching" | "login_required" | "ready" | "verification_required" | "membership_expired" | "error";
|
|
3
4
|
export type FastMossStatus = {
|
|
@@ -30,6 +31,15 @@ type VisibleVideoItem = {
|
|
|
30
31
|
images: string[];
|
|
31
32
|
headers: string[];
|
|
32
33
|
cells: string[];
|
|
34
|
+
previewIndex?: number;
|
|
35
|
+
};
|
|
36
|
+
export type ObservedVideoRecord = {
|
|
37
|
+
videoId: string;
|
|
38
|
+
authorId?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
sourceUrl?: string;
|
|
41
|
+
thumbnailUrl?: string;
|
|
42
|
+
metrics?: Record<string, number>;
|
|
33
43
|
};
|
|
34
44
|
export declare class FastMossIntegration {
|
|
35
45
|
private context;
|
|
@@ -46,6 +56,9 @@ export declare class FastMossIntegration {
|
|
|
46
56
|
private readonly authenticatedMarker;
|
|
47
57
|
private readonly dataDir;
|
|
48
58
|
private savedAuthenticated;
|
|
59
|
+
private observedVideoRecords;
|
|
60
|
+
private videoResponseTasks;
|
|
61
|
+
private observedVideoPages;
|
|
49
62
|
constructor();
|
|
50
63
|
status(): FastMossStatus;
|
|
51
64
|
start(): Promise<FastMossStatus>;
|
|
@@ -113,6 +126,15 @@ export declare class FastMossIntegration {
|
|
|
113
126
|
private openProductRanking;
|
|
114
127
|
private openVideoLearningPage;
|
|
115
128
|
private readVideoLearningItems;
|
|
129
|
+
/**
|
|
130
|
+
* AI ranking cards expose their TikTok video id only through the visible
|
|
131
|
+
* preview action. Open only the cards needed for this batch, capture the
|
|
132
|
+
* public FastMoss video-detail URL, and close the temporary tab again.
|
|
133
|
+
*/
|
|
134
|
+
private resolveVideoLearningCardLinks;
|
|
135
|
+
private resetObservedVideoRecords;
|
|
136
|
+
private observeVideoRankingResponses;
|
|
137
|
+
private waitForVideoResponseTasks;
|
|
116
138
|
private clickVideoLearningTab;
|
|
117
139
|
private clickVideoLearningNextPage;
|
|
118
140
|
private waitForVideoLearningNavigation;
|
|
@@ -124,7 +146,29 @@ export declare function fastMossAiVideoLearningRankUrl(market?: string): string;
|
|
|
124
146
|
export declare function mergeRankingRecord(target: Map<string, Record<string, unknown>>, row: Record<string, unknown>, source: FastMossRankingSource, position: number): void;
|
|
125
147
|
export declare function selectDetailCandidates(records: Array<Record<string, unknown>>, limit?: number): Record<string, unknown>[];
|
|
126
148
|
export declare function isTransientFastMossNavigation(error: unknown): boolean;
|
|
149
|
+
export declare function isVideoRankingResponse(response: Response | string): boolean;
|
|
150
|
+
export declare function publicVideoRecordsFromRankPayload(payload: unknown): ObservedVideoRecord[];
|
|
151
|
+
export declare function videoLearningSourcesFromObservedRecords(records: ObservedVideoRecord[], context: {
|
|
152
|
+
market: string;
|
|
153
|
+
dimension: VideoLearningDimension;
|
|
154
|
+
observedAt: string;
|
|
155
|
+
}): {
|
|
156
|
+
source: string;
|
|
157
|
+
platform: string;
|
|
158
|
+
platformVideoId: string;
|
|
159
|
+
sourceUrl: string;
|
|
160
|
+
market: string;
|
|
161
|
+
rankingDimension: VideoLearningDimension;
|
|
162
|
+
rank: number;
|
|
163
|
+
title: string;
|
|
164
|
+
productTitle: string;
|
|
165
|
+
thumbnailUrl: string | null;
|
|
166
|
+
metrics: Record<string, number>;
|
|
167
|
+
observedAt: string;
|
|
168
|
+
}[];
|
|
127
169
|
export declare function isAnalyzableVideoUrl(value: string): boolean;
|
|
170
|
+
export declare function isFastMossVideoDetailUrl(value: string): boolean;
|
|
171
|
+
export declare function publicTikTokVideoUrlFromFastMoss(value: string): string;
|
|
128
172
|
export declare function videoLearningSourcesFromItems(items: VisibleVideoItem[], context: {
|
|
129
173
|
market: string;
|
|
130
174
|
dimension: VideoLearningDimension;
|
|
@@ -37,6 +37,9 @@ export class FastMossIntegration {
|
|
|
37
37
|
authenticatedMarker = path.join(this.profileDir, "authenticated.json");
|
|
38
38
|
dataDir = path.join(CONFIG_DIR, "fastmoss-selection");
|
|
39
39
|
savedAuthenticated = existsSync(this.authenticatedMarker);
|
|
40
|
+
observedVideoRecords = [];
|
|
41
|
+
videoResponseTasks = new Set();
|
|
42
|
+
observedVideoPages = new WeakSet();
|
|
40
43
|
constructor() {
|
|
41
44
|
if (!this.savedAuthenticated)
|
|
42
45
|
return;
|
|
@@ -90,8 +93,9 @@ export class FastMossIntegration {
|
|
|
90
93
|
throw error;
|
|
91
94
|
}
|
|
92
95
|
this.page = this.context.pages()[0] || await this.context.newPage();
|
|
96
|
+
this.observeVideoRankingResponses(this.page);
|
|
93
97
|
this.context.on("page", (page) => {
|
|
94
|
-
this.page
|
|
98
|
+
this.observeVideoRankingResponses(page);
|
|
95
99
|
});
|
|
96
100
|
this.context.on("close", () => {
|
|
97
101
|
this.context = null;
|
|
@@ -285,12 +289,14 @@ export class FastMossIntegration {
|
|
|
285
289
|
return;
|
|
286
290
|
let previousSignature = "";
|
|
287
291
|
for (let pageNumber = 1; pageNumber <= 5 && sources.filter((item) => item.rankingDimension === dimension).length < limit; pageNumber += 1) {
|
|
288
|
-
const snapshot = await this.readVideoLearningItems();
|
|
292
|
+
const snapshot = await this.resolveVideoLearningCardLinks(await this.readVideoLearningItems(), limit);
|
|
289
293
|
const signature = JSON.stringify(snapshot.map((item) => [item.text, item.media, item.links]).slice(0, 20));
|
|
290
294
|
if (!snapshot.length || (pageNumber > 1 && signature === previousSignature))
|
|
291
295
|
break;
|
|
292
296
|
previousSignature = signature;
|
|
293
|
-
|
|
297
|
+
await this.waitForVideoResponseTasks();
|
|
298
|
+
const observed = videoLearningSourcesFromObservedRecords(this.observedVideoRecords, { market, dimension, observedAt: new Date().toISOString() });
|
|
299
|
+
const normalized = [...observed, ...videoLearningSourcesFromItems(snapshot, { market, dimension, observedAt: new Date().toISOString() })];
|
|
294
300
|
for (const item of normalized) {
|
|
295
301
|
const key = String(item.platformVideoId || item.sourceUrl || "");
|
|
296
302
|
if (!key || seen.has(key))
|
|
@@ -334,6 +340,7 @@ export class FastMossIntegration {
|
|
|
334
340
|
for (const entry of aiDimensions) {
|
|
335
341
|
if (sources.length >= requestedTarget)
|
|
336
342
|
break;
|
|
343
|
+
this.resetObservedVideoRecords();
|
|
337
344
|
const clicked = await this.clickVideoLearningTab(entry.labels);
|
|
338
345
|
if (!clicked) {
|
|
339
346
|
rejected.push({ dimension: entry.dimension, reason: `没有找到${entry.labels[0]}标签` });
|
|
@@ -405,7 +412,10 @@ export class FastMossIntegration {
|
|
|
405
412
|
}
|
|
406
413
|
useLatestPage() {
|
|
407
414
|
const pages = this.context?.pages().filter((page) => !page.isClosed()) || [];
|
|
408
|
-
|
|
415
|
+
const rankingPages = pages.filter((page) => !isFastMossVideoDetailUrl(page.url()));
|
|
416
|
+
if (rankingPages.length)
|
|
417
|
+
this.page = rankingPages[rankingPages.length - 1];
|
|
418
|
+
else if (pages.length)
|
|
409
419
|
this.page = pages[pages.length - 1];
|
|
410
420
|
}
|
|
411
421
|
async openProductRanking(input, ranking = RANKING_DEFINITIONS[0]) {
|
|
@@ -436,6 +446,8 @@ export class FastMossIntegration {
|
|
|
436
446
|
async openVideoLearningPage(url, label) {
|
|
437
447
|
if (!this.page || this.page.isClosed())
|
|
438
448
|
throw new Error("FastMoss 专用浏览器未打开");
|
|
449
|
+
this.resetObservedVideoRecords();
|
|
450
|
+
this.observeVideoRankingResponses(this.page);
|
|
439
451
|
this.message = `正在自动进入 ${label}`;
|
|
440
452
|
try {
|
|
441
453
|
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
@@ -480,6 +492,77 @@ export class FastMossIntegration {
|
|
|
480
492
|
}
|
|
481
493
|
throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜页面跳转后没有恢复");
|
|
482
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* AI ranking cards expose their TikTok video id only through the visible
|
|
497
|
+
* preview action. Open only the cards needed for this batch, capture the
|
|
498
|
+
* public FastMoss video-detail URL, and close the temporary tab again.
|
|
499
|
+
*/
|
|
500
|
+
async resolveVideoLearningCardLinks(items, limit) {
|
|
501
|
+
if (!this.page || this.page.isClosed())
|
|
502
|
+
return items;
|
|
503
|
+
const rankingPage = this.page;
|
|
504
|
+
const candidates = items
|
|
505
|
+
.filter((item) => !item.links.some(isFastMossVideoDetailUrl) && Number.isInteger(item.previewIndex))
|
|
506
|
+
.slice(0, Math.max(0, limit));
|
|
507
|
+
if (!candidates.length)
|
|
508
|
+
return items;
|
|
509
|
+
const covers = rankingPage.locator('div.cursor-pointer > img[class*="aspect-"]');
|
|
510
|
+
const coverCount = await covers.count();
|
|
511
|
+
for (const item of candidates) {
|
|
512
|
+
const index = Number(item.previewIndex);
|
|
513
|
+
if (!Number.isInteger(index) || index < 0 || index >= coverCount)
|
|
514
|
+
continue;
|
|
515
|
+
let popup = null;
|
|
516
|
+
try {
|
|
517
|
+
[popup] = await Promise.all([
|
|
518
|
+
rankingPage.waitForEvent("popup", { timeout: 6_000 }),
|
|
519
|
+
covers.nth(index).click({ timeout: 6_000 }),
|
|
520
|
+
]);
|
|
521
|
+
await popup.waitForURL(/\/media-source\/video\/\d+/i, { timeout: 10_000 });
|
|
522
|
+
const detailUrl = popup.url();
|
|
523
|
+
if (isFastMossVideoDetailUrl(detailUrl))
|
|
524
|
+
item.links.unshift(detailUrl);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
// Keep the item unresolved. The caller records a truthful rejection
|
|
528
|
+
// instead of counting a cover image or ranking page as a video.
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
if (popup && !popup.isClosed())
|
|
532
|
+
await popup.close().catch(() => undefined);
|
|
533
|
+
this.page = rankingPage;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return items;
|
|
537
|
+
}
|
|
538
|
+
resetObservedVideoRecords() {
|
|
539
|
+
this.observedVideoRecords = [];
|
|
540
|
+
this.videoResponseTasks.clear();
|
|
541
|
+
}
|
|
542
|
+
observeVideoRankingResponses(page) {
|
|
543
|
+
if (this.observedVideoPages.has(page))
|
|
544
|
+
return;
|
|
545
|
+
this.observedVideoPages.add(page);
|
|
546
|
+
page.on("response", (response) => {
|
|
547
|
+
if (!isVideoRankingResponse(response))
|
|
548
|
+
return;
|
|
549
|
+
let task;
|
|
550
|
+
task = response.json()
|
|
551
|
+
.then((payload) => {
|
|
552
|
+
const merged = new Map(this.observedVideoRecords.map((item) => [item.videoId, item]));
|
|
553
|
+
for (const item of publicVideoRecordsFromRankPayload(payload))
|
|
554
|
+
merged.set(item.videoId, { ...merged.get(item.videoId), ...item });
|
|
555
|
+
this.observedVideoRecords = [...merged.values()];
|
|
556
|
+
})
|
|
557
|
+
.catch(() => undefined)
|
|
558
|
+
.finally(() => this.videoResponseTasks.delete(task));
|
|
559
|
+
this.videoResponseTasks.add(task);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
async waitForVideoResponseTasks() {
|
|
563
|
+
if (this.videoResponseTasks.size)
|
|
564
|
+
await Promise.allSettled([...this.videoResponseTasks]);
|
|
565
|
+
}
|
|
483
566
|
async clickVideoLearningTab(labels) {
|
|
484
567
|
let lastError;
|
|
485
568
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -692,13 +775,14 @@ async function readVisibleVideoLearningItems(page) {
|
|
|
692
775
|
]);
|
|
693
776
|
return [...new Set([...direct, ...data].filter(Boolean).map(absoluteUrl).filter(Boolean))];
|
|
694
777
|
};
|
|
695
|
-
const serialize = (root, headers = [], cells = []) => ({
|
|
778
|
+
const serialize = (root, headers = [], cells = [], previewIndex) => ({
|
|
696
779
|
text: root.innerText.replace(/\s+/g, " ").trim().slice(0, 8000),
|
|
697
780
|
links: [...new Set([...root.querySelectorAll("a[href]")].map((link) => link.href).filter(Boolean))],
|
|
698
781
|
media: mediaUrls(root),
|
|
699
782
|
images: [...new Set([...root.querySelectorAll("img")].map((image) => image.currentSrc || image.src || image.dataset.src || "").filter(Boolean).map(absoluteUrl).filter(Boolean))],
|
|
700
783
|
headers,
|
|
701
784
|
cells,
|
|
785
|
+
...(previewIndex == null ? {} : { previewIndex }),
|
|
702
786
|
});
|
|
703
787
|
const tableItems = [...document.querySelectorAll("table tbody tr")]
|
|
704
788
|
.filter(shown)
|
|
@@ -709,8 +793,15 @@ async function readVisibleVideoLearningItems(page) {
|
|
|
709
793
|
return serialize(row, headers, cells);
|
|
710
794
|
})
|
|
711
795
|
.filter((item) => item.text && (item.links.length || item.media.length || item.images.length));
|
|
796
|
+
const previewSelector = 'div.cursor-pointer > img[class*="aspect-"]';
|
|
797
|
+
const previewImages = [...document.querySelectorAll(previewSelector)];
|
|
798
|
+
const previewItems = previewImages.map((preview, previewIndex) => {
|
|
799
|
+
const root = preview.parentElement?.parentElement || preview.parentElement || preview;
|
|
800
|
+
return serialize(root, [], [], previewIndex);
|
|
801
|
+
}).filter((item) => item.text && item.images.length);
|
|
712
802
|
const cardCandidates = [...document.querySelectorAll("article,li,[class*='card'],[class*='item']")]
|
|
713
803
|
.filter(shown)
|
|
804
|
+
.filter((element) => !element.querySelector(previewSelector))
|
|
714
805
|
.filter((element) => {
|
|
715
806
|
const rect = element.getBoundingClientRect();
|
|
716
807
|
const text = element.innerText.replace(/\s+/g, " ").trim();
|
|
@@ -731,7 +822,7 @@ async function readVisibleVideoLearningItems(page) {
|
|
|
731
822
|
if (cards.length >= 80)
|
|
732
823
|
break;
|
|
733
824
|
}
|
|
734
|
-
const combined = [...tableItems, ...cards];
|
|
825
|
+
const combined = [...tableItems, ...previewItems, ...cards];
|
|
735
826
|
const seen = new Set();
|
|
736
827
|
return combined.filter((item) => {
|
|
737
828
|
const key = JSON.stringify([item.text.slice(0, 300), item.media[0], item.links[0], item.images[0]]);
|
|
@@ -745,6 +836,87 @@ async function readVisibleVideoLearningItems(page) {
|
|
|
745
836
|
function firstHttp(values, expected) {
|
|
746
837
|
return values.find((value) => /^https?:\/\//i.test(value) && (!expected || expected.test(value))) || "";
|
|
747
838
|
}
|
|
839
|
+
export function isVideoRankingResponse(response) {
|
|
840
|
+
const value = typeof response === "string" ? response : response.url();
|
|
841
|
+
try {
|
|
842
|
+
const url = new URL(value);
|
|
843
|
+
return /(?:^|\.)fastmoss\.com$/i.test(url.hostname)
|
|
844
|
+
&& /\/api\/.*(?:video|aweme|media)/i.test(url.pathname);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
export function publicVideoRecordsFromRankPayload(payload) {
|
|
851
|
+
const output = new Map();
|
|
852
|
+
const visited = new Set();
|
|
853
|
+
let inspected = 0;
|
|
854
|
+
const scalar = (record, keys) => {
|
|
855
|
+
for (const key of keys) {
|
|
856
|
+
const value = record[key];
|
|
857
|
+
if (typeof value === "string" || typeof value === "number")
|
|
858
|
+
return String(value).trim();
|
|
859
|
+
}
|
|
860
|
+
return "";
|
|
861
|
+
};
|
|
862
|
+
const numeric = (record, keys) => {
|
|
863
|
+
const raw = scalar(record, keys);
|
|
864
|
+
const value = numberValue(raw);
|
|
865
|
+
return Number.isFinite(value) ? value : undefined;
|
|
866
|
+
};
|
|
867
|
+
const visit = (value) => {
|
|
868
|
+
if (!value || typeof value !== "object" || visited.has(value) || inspected >= 20_000)
|
|
869
|
+
return;
|
|
870
|
+
visited.add(value);
|
|
871
|
+
inspected += 1;
|
|
872
|
+
if (Array.isArray(value)) {
|
|
873
|
+
value.forEach(visit);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const record = value;
|
|
877
|
+
const videoId = scalar(record, ["video_id", "videoId", "aweme_id", "awemeId", "item_id", "itemId"]);
|
|
878
|
+
if (/^\d{8,}$/.test(videoId)) {
|
|
879
|
+
const sourceCandidate = scalar(record, ["video_url", "videoUrl", "play_url", "playUrl", "download_url", "downloadUrl", "share_url", "shareUrl"]);
|
|
880
|
+
const thumbnailCandidate = scalar(record, ["cover_url", "coverUrl", "cover", "poster", "origin_cover", "originCover"]);
|
|
881
|
+
const metrics = Object.fromEntries(Object.entries({
|
|
882
|
+
sales: numeric(record, ["sales", "sold_count", "soldCount", "sale_count", "saleCount"]),
|
|
883
|
+
gmv: numeric(record, ["gmv", "sale_amount", "saleAmount", "sales_amount", "salesAmount"]),
|
|
884
|
+
views: numeric(record, ["views", "view_count", "viewCount", "play_count", "playCount"]),
|
|
885
|
+
likes: numeric(record, ["likes", "like_count", "likeCount", "digg_count", "diggCount"]),
|
|
886
|
+
comments: numeric(record, ["comments", "comment_count", "commentCount"]),
|
|
887
|
+
roas: numeric(record, ["roas", "ROAS"]),
|
|
888
|
+
}).filter((entry) => entry[1] != null));
|
|
889
|
+
const next = {
|
|
890
|
+
videoId,
|
|
891
|
+
authorId: scalar(record, ["author_unique_id", "authorUniqueId", "unique_id", "uniqueId", "author_id", "authorId"]) || undefined,
|
|
892
|
+
title: scalar(record, ["title", "video_title", "videoTitle", "desc", "description", "content", "text"]) || undefined,
|
|
893
|
+
sourceUrl: isAnalyzableVideoUrl(sourceCandidate) ? sourceCandidate : undefined,
|
|
894
|
+
thumbnailUrl: /^https?:\/\//i.test(thumbnailCandidate) ? thumbnailCandidate : undefined,
|
|
895
|
+
metrics,
|
|
896
|
+
};
|
|
897
|
+
output.set(videoId, { ...output.get(videoId), ...next });
|
|
898
|
+
}
|
|
899
|
+
Object.values(record).forEach(visit);
|
|
900
|
+
};
|
|
901
|
+
visit(payload);
|
|
902
|
+
return [...output.values()];
|
|
903
|
+
}
|
|
904
|
+
export function videoLearningSourcesFromObservedRecords(records, context) {
|
|
905
|
+
return records.map((record, index) => ({
|
|
906
|
+
source: "fastmoss-agent",
|
|
907
|
+
platform: "tiktok",
|
|
908
|
+
platformVideoId: record.videoId,
|
|
909
|
+
sourceUrl: record.sourceUrl || `https://www.tiktok.com/@_/video/${record.videoId}`,
|
|
910
|
+
market: context.market,
|
|
911
|
+
rankingDimension: context.dimension,
|
|
912
|
+
rank: index + 1,
|
|
913
|
+
title: record.title || `TikTok video ${record.videoId}`,
|
|
914
|
+
productTitle: record.title || "",
|
|
915
|
+
thumbnailUrl: record.thumbnailUrl || null,
|
|
916
|
+
metrics: record.metrics || {},
|
|
917
|
+
observedAt: context.observedAt,
|
|
918
|
+
}));
|
|
919
|
+
}
|
|
748
920
|
function videoIdFromValues(values) {
|
|
749
921
|
for (const value of values) {
|
|
750
922
|
const direct = value.match(/(?:video|aweme|item|material)[/=_-](\d{8,})/i)?.[1];
|
|
@@ -808,6 +980,21 @@ export function isAnalyzableVideoUrl(value) {
|
|
|
808
980
|
return false;
|
|
809
981
|
}
|
|
810
982
|
}
|
|
983
|
+
export function isFastMossVideoDetailUrl(value) {
|
|
984
|
+
try {
|
|
985
|
+
const url = new URL(value);
|
|
986
|
+
return /(?:^|\.)fastmoss\.com$/i.test(url.hostname) && /\/media-source\/video\/\d+(?:\/|$)/i.test(url.pathname);
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
export function publicTikTokVideoUrlFromFastMoss(value) {
|
|
993
|
+
if (!isFastMossVideoDetailUrl(value))
|
|
994
|
+
return "";
|
|
995
|
+
const id = new URL(value).pathname.match(/\/media-source\/video\/(\d+)(?:\/|$)/i)?.[1];
|
|
996
|
+
return id ? `https://www.tiktok.com/@_/video/${id}` : "";
|
|
997
|
+
}
|
|
811
998
|
export function videoLearningSourcesFromItems(items, context) {
|
|
812
999
|
const output = [];
|
|
813
1000
|
const seen = new Set();
|
|
@@ -815,7 +1002,8 @@ export function videoLearningSourcesFromItems(items, context) {
|
|
|
815
1002
|
const mediaUrl = item.media.find((value) => isAnalyzableVideoUrl(value)) || "";
|
|
816
1003
|
const publicVideoUrl = firstHttp(item.links, /tiktok\.com\/.*(?:video|v)\//i);
|
|
817
1004
|
const traceUrl = firstHttp(item.links, /video|aweme|creative|material/i);
|
|
818
|
-
const
|
|
1005
|
+
const publicTraceUrl = publicTikTokVideoUrlFromFastMoss(traceUrl);
|
|
1006
|
+
const sourceUrl = mediaUrl || publicVideoUrl || publicTraceUrl;
|
|
819
1007
|
const platformVideoId = videoIdFromValues([sourceUrl, ...item.links, ...item.media]) || stableId(`${context.dimension}|${item.text}|${item.images[0] || ""}`);
|
|
820
1008
|
if (seen.has(platformVideoId))
|
|
821
1009
|
continue;
|