Compare commits
No commits in common. "286b73a28730a344af4bcc6d2e0dc84f7615955e" and "6cc703ada28ad227b6048bb98f95bf9ffc4c87d0" have entirely different histories.
286b73a287
...
6cc703ada2
@ -4,8 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch --env-file=.env src/index.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
"start": "tsx --env-file=.env src/index.ts",
|
"start": "tsx src/index.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage"
|
"test:coverage": "vitest run --coverage"
|
||||||
|
|||||||
@ -1,13 +1,6 @@
|
|||||||
import type { ContentItem, PlatformAdapter } from "@muse/shared";
|
import type { ContentItem, PlatformAdapter } from "@muse/shared";
|
||||||
import { tikhubFetch } from "../tikhub";
|
import { tikhubFetch } from "../tikhub";
|
||||||
|
|
||||||
/** Ensure Bilibili URLs use https:// (API may return protocol-relative "//..." or http) */
|
|
||||||
function normalizeUrl(url: string): string {
|
|
||||||
if (url.startsWith("//")) return `https:${url}`;
|
|
||||||
if (url.startsWith("http://")) return url.replace("http://", "https://");
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BilibiliAdapter implements PlatformAdapter {
|
export class BilibiliAdapter implements PlatformAdapter {
|
||||||
async fetchTrending(count: number): Promise<ContentItem[]> {
|
async fetchTrending(count: number): Promise<ContentItem[]> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -51,10 +44,10 @@ export class BilibiliAdapter implements PlatformAdapter {
|
|||||||
return {
|
return {
|
||||||
id: String(aid || bvid),
|
id: String(aid || bvid),
|
||||||
title: raw?.title || "无标题",
|
title: raw?.title || "无标题",
|
||||||
cover_url: raw?.pic ? normalizeUrl(raw.pic) : undefined,
|
cover_url: raw?.pic || undefined,
|
||||||
video_url: undefined,
|
video_url: undefined,
|
||||||
author_name: owner?.name || raw?.author || "未知作者",
|
author_name: owner?.name || raw?.author || "未知作者",
|
||||||
author_avatar: owner?.face ? normalizeUrl(owner.face) : undefined,
|
author_avatar: owner?.face || undefined,
|
||||||
play_count: stat?.view ?? undefined,
|
play_count: stat?.view ?? undefined,
|
||||||
like_count: stat?.like ?? undefined,
|
like_count: stat?.like ?? undefined,
|
||||||
collect_count: stat?.favorite ?? undefined,
|
collect_count: stat?.favorite ?? undefined,
|
||||||
|
|||||||
@ -58,7 +58,7 @@ describe("InstagramAdapter", () => {
|
|||||||
code: "SEC001",
|
code: "SEC001",
|
||||||
caption: "Section post",
|
caption: "Section post",
|
||||||
user: { username: "user1" },
|
user: { username: "user1" },
|
||||||
like_count: 5000,
|
like_count: 1000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -87,7 +87,6 @@ describe("InstagramAdapter", () => {
|
|||||||
code: "cap-obj",
|
code: "cap-obj",
|
||||||
caption: { text: "Caption from object" },
|
caption: { text: "Caption from object" },
|
||||||
user: { username: "test" },
|
user: { username: "test" },
|
||||||
like_count: 500,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -103,7 +102,6 @@ describe("InstagramAdapter", () => {
|
|||||||
code: "cap-str",
|
code: "cap-str",
|
||||||
caption: "String caption",
|
caption: "String caption",
|
||||||
user: { username: "test" },
|
user: { username: "test" },
|
||||||
like_count: 500,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -119,7 +117,6 @@ describe("InstagramAdapter", () => {
|
|||||||
code: "cap-null",
|
code: "cap-null",
|
||||||
caption: null,
|
caption: null,
|
||||||
user: { username: "test" },
|
user: { username: "test" },
|
||||||
like_count: 500,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -135,7 +132,6 @@ describe("InstagramAdapter", () => {
|
|||||||
code: "thumb-test",
|
code: "thumb-test",
|
||||||
thumbnail_url: "https://ig.com/thumb.jpg",
|
thumbnail_url: "https://ig.com/thumb.jpg",
|
||||||
user: { username: "test" },
|
user: { username: "test" },
|
||||||
like_count: 500,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -143,22 +139,6 @@ describe("InstagramAdapter", () => {
|
|||||||
const items = await adapter.fetchTrending(20);
|
const items = await adapter.fetchTrending(20);
|
||||||
expect(items[0].cover_url).toBe("https://ig.com/thumb.jpg");
|
expect(items[0].cover_url).toBe("https://ig.com/thumb.jpg");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters out low-engagement items and sorts by likes", async () => {
|
|
||||||
mockFetch.mockResolvedValueOnce({
|
|
||||||
items: [
|
|
||||||
{ code: "low", caption: "Low", user: { username: "a" }, like_count: 5 },
|
|
||||||
{ code: "high", caption: "High", user: { username: "b" }, like_count: 10000 },
|
|
||||||
{ code: "mid", caption: "Mid", user: { username: "c" }, like_count: 500 },
|
|
||||||
{ code: "none", caption: "None", user: { username: "d" } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const items = await adapter.fetchTrending(20);
|
|
||||||
expect(items).toHaveLength(2);
|
|
||||||
expect(items[0].id).toBe("high");
|
|
||||||
expect(items[1].id).toBe("mid");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fetchDetail", () => {
|
describe("fetchDetail", () => {
|
||||||
|
|||||||
@ -34,12 +34,10 @@ export class InstagramAdapter implements PlatformAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
.slice(0, count)
|
||||||
.map((item: unknown, index: number) =>
|
.map((item: unknown, index: number) =>
|
||||||
this.mapToContentItem(item as Record<string, unknown>, index)
|
this.mapToContentItem(item as Record<string, unknown>, index)
|
||||||
)
|
);
|
||||||
.filter((item) => item.like_count != null && item.like_count >= 100)
|
|
||||||
.sort((a, b) => (b.like_count ?? 0) - (a.like_count ?? 0))
|
|
||||||
.slice(0, count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchDetail(id: string): Promise<ContentItem> {
|
async fetchDetail(id: string): Promise<ContentItem> {
|
||||||
|
|||||||
@ -35,7 +35,6 @@ export function ContentCard({ item }: ContentCardProps) {
|
|||||||
alt={item.title}
|
alt={item.title}
|
||||||
fill
|
fill
|
||||||
unoptimized
|
unoptimized
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 960px) 50vw, (max-width: 1240px) 33vw, 25vw"
|
sizes="(max-width: 640px) 100vw, (max-width: 960px) 50vw, (max-width: 1240px) 33vw, 25vw"
|
||||||
@ -85,7 +84,6 @@ export function ContentCard({ item }: ContentCardProps) {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
unoptimized
|
unoptimized
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
className="rounded-full object-cover"
|
className="rounded-full object-cover"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = "none";
|
(e.target as HTMLImageElement).style.display = "none";
|
||||||
|
|||||||
@ -46,7 +46,6 @@ export function DetailPanel({ item }: DetailPanelProps) {
|
|||||||
alt={item.title}
|
alt={item.title}
|
||||||
fill
|
fill
|
||||||
unoptimized
|
unoptimized
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
priority
|
priority
|
||||||
sizes="(max-width: 768px) 100vw, 768px"
|
sizes="(max-width: 768px) 100vw, 768px"
|
||||||
@ -88,7 +87,6 @@ export function DetailPanel({ item }: DetailPanelProps) {
|
|||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
unoptimized
|
unoptimized
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
className="rounded-full object-cover"
|
className="rounded-full object-cover"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = "none";
|
(e.target as HTMLImageElement).style.display = "none";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user