Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/clis/douyin/collections.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const { browserFetchMock } = vi.hoisted(() => ({
browserFetchMock: vi.fn(),
}));

vi.mock('./_shared/browser-fetch.js', () => ({
browserFetch: browserFetchMock,
}));

import { getRegistry } from '../../registry.js';
import './collections.js';

describe('douyin collections registration', () => {
describe('douyin collections', () => {
beforeEach(() => {
browserFetchMock.mockReset();
});

it('registers the collections command', () => {
const registry = getRegistry();
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'collections');
Expand All @@ -23,4 +36,24 @@ describe('douyin collections registration', () => {
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'collections');
expect(cmd?.strategy).toBe('cookie');
});

it('uses the current mix list request shape', async () => {
const registry = getRegistry();
const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'collections');
expect(command?.func).toBeDefined();
if (!command?.func) throw new Error('douyin collections command not registered');

browserFetchMock.mockResolvedValueOnce({
mix_list: [],
});

const rows = await command.func({} as any, { limit: 12 });

expect(browserFetchMock).toHaveBeenCalledWith(
{},
'GET',
'https://creator.douyin.com/web/api/mix/list/?status=0,1,2,3,6&count=12&cursor=0&should_query_new_mix=1&device_platform=web&aid=1128',
);
expect(rows).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion src/clis/douyin/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cli({
],
columns: ['mix_id', 'name', 'item_count'],
func: async (page, kwargs) => {
const url = `https://creator.douyin.com/web/api/mix/list/?aid=1128&count=${kwargs.limit}`;
const url = `https://creator.douyin.com/web/api/mix/list/?status=0,1,2,3,6&count=${kwargs.limit}&cursor=0&should_query_new_mix=1&device_platform=web&aid=1128`;
const res = await browserFetch(page, 'GET', url) as {
mix_list: Array<{ mix_id: string; mix_name: string; item_count: number }>
};
Expand Down
44 changes: 42 additions & 2 deletions src/clis/douyin/hashtag.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const { browserFetchMock } = vi.hoisted(() => ({
browserFetchMock: vi.fn(),
}));

vi.mock('./_shared/browser-fetch.js', () => ({
browserFetch: browserFetchMock,
}));

import { getRegistry } from '../../registry.js';
import './hashtag.js';

describe('douyin hashtag registration', () => {
describe('douyin hashtag', () => {
beforeEach(() => {
browserFetchMock.mockReset();
});

it('registers the hashtag command', () => {
const registry = getRegistry();
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'hashtag');
Expand All @@ -25,4 +38,31 @@ describe('douyin hashtag registration', () => {
const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'hashtag');
expect(cmd?.strategy).toBe('cookie');
});

it('parses the current hotspot recommendation shape', async () => {
const registry = getRegistry();
const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'hashtag');
expect(command?.func).toBeDefined();
if (!command?.func) throw new Error('douyin hashtag command not registered');

browserFetchMock.mockResolvedValueOnce({
all_sentences: [
{
word: '在公园花海里大晒一场',
hot_value: 12141172,
sentence_id: '2448416',
},
],
});

const rows = await command.func({} as any, { action: 'hot', keyword: '', limit: 5 });

expect(rows).toEqual([
{
name: '在公园花海里大晒一场',
id: '2448416',
view_count: 12141172,
},
]);
});
});
14 changes: 11 additions & 3 deletions src/clis/douyin/hashtag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,19 @@ cli({
const kw = kwargs.keyword as string;
const url = `https://creator.douyin.com/aweme/v1/hotspot/recommend/?${kw ? `keyword=${encodeURIComponent(kw)}&` : ''}aid=1128`;
const res = await browserFetch(page, 'GET', url) as {
hotspot_list: Array<{ sentence: string; hot_value: number }>
hotspot_list?: Array<{ sentence: string; hot_value: number }>;
all_sentences?: Array<{ sentence_id?: string; word?: string; hot_value: number }>;
};
return (res.hotspot_list ?? []).slice(0, kwargs.limit as number).map(h => ({
const items = res.hotspot_list
?? res.all_sentences?.map(h => ({
sentence: h.word ?? '',
hot_value: h.hot_value,
sentence_id: h.sentence_id ?? '',
}))
?? [];
return items.slice(0, kwargs.limit as number).map(h => ({
name: h.sentence,
id: '',
id: 'sentence_id' in h ? h.sentence_id : '',
view_count: h.hot_value,
}));
}
Expand Down
54 changes: 52 additions & 2 deletions src/clis/douyin/videos.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,62 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const { browserFetchMock } = vi.hoisted(() => ({
browserFetchMock: vi.fn(),
}));

vi.mock('./_shared/browser-fetch.js', () => ({
browserFetch: browserFetchMock,
}));

import { getRegistry } from '../../registry.js';
import './videos.js';

describe('douyin videos registration', () => {
describe('douyin videos', () => {
beforeEach(() => {
browserFetchMock.mockReset();
});

it('registers the videos command', () => {
const registry = getRegistry();
const values = [...registry.values()];
const cmd = values.find(c => c.site === 'douyin' && c.name === 'videos');
expect(cmd).toBeDefined();
});

it('parses the current creator work_list api shape', async () => {
const registry = getRegistry();
const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'videos');
expect(command?.func).toBeDefined();
if (!command?.func) throw new Error('douyin videos command not registered');

browserFetchMock.mockResolvedValueOnce({
aweme_list: [
{
aweme_id: '7000000000000000001',
desc: '测试视频标题',
create_time: 1581571130,
statistics: {
play_count: 0,
digg_count: 12,
},
status: {
is_private: true,
},
},
],
});

const rows = await command.func({} as any, { limit: 5, page: 1, status: 'all' });

expect(rows).toEqual([
{
aweme_id: '7000000000000000001',
title: '测试视频标题',
status: 'private',
play_count: 0,
digg_count: 12,
create_time: new Date(1581571130 * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
},
]);
});
});
64 changes: 49 additions & 15 deletions src/clis/douyin/videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@ import { cli, Strategy } from '../../registry.js';
import { browserFetch } from './_shared/browser-fetch.js';
import type { IPage } from '../../types.js';

interface LegacyWorkItem {
aweme_id: string;
desc: string;
status: number;
public_time: number;
create_time: number;
statistics: { play_count: number; digg_count: number };
}

interface CurrentWorkItem {
aweme_id: string;
desc?: string;
status?: {
in_reviewing?: boolean;
is_private?: boolean;
is_delete?: boolean;
is_prohibited?: boolean;
};
public_time?: number;
create_time?: number;
statistics?: { play_count?: number; digg_count?: number };
}

function normalizeVideoStatus(
status: number | {
in_reviewing?: boolean;
is_private?: boolean;
is_delete?: boolean;
is_prohibited?: boolean;
} | undefined,
publicTime: number | undefined,
): number | string {
if (typeof status === 'number') return status;
if (!status) return publicTime && publicTime > Date.now() / 1000 ? 'scheduled' : 'published';
if (status.is_delete) return 'deleted';
if (status.is_prohibited) return 'prohibited';
if (status.in_reviewing) return 'reviewing';
if (status.is_private) return 'private';
if (publicTime && publicTime > Date.now() / 1000) return 'scheduled';
return 'published';
}

cli({
site: 'douyin',
name: 'videos',
Expand All @@ -19,31 +61,23 @@ cli({
const statusNum = statusMap[kwargs.status as string] ?? 0;
const url = `https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=${kwargs.limit}&page_num=${kwargs.page}&status=${statusNum}`;
const res = (await browserFetch(page, 'GET', url)) as {
data: {
work_list: Array<{
aweme_id: string;
desc: string;
status: number;
public_time: number;
create_time: number;
statistics: { play_count: number; digg_count: number };
}>;
};
data?: { work_list?: LegacyWorkItem[] };
aweme_list?: CurrentWorkItem[];
};
let items = res.data?.work_list ?? [];
let items: Array<LegacyWorkItem | CurrentWorkItem> = res.data?.work_list ?? res.aweme_list ?? [];

// The API has a bug with status=16 for scheduled, so filter client-side
if (kwargs.status === 'scheduled') {
items = items.filter((v) => v.public_time > Date.now() / 1000);
items = items.filter((v) => (v.public_time ?? 0) > Date.now() / 1000);
}

return items.map((v) => ({
aweme_id: v.aweme_id,
title: v.desc,
status: v.status,
title: v.desc ?? '',
status: normalizeVideoStatus(v.status, v.public_time),
play_count: v.statistics?.play_count ?? 0,
digg_count: v.statistics?.digg_count ?? 0,
create_time: new Date(v.create_time * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
create_time: new Date((v.create_time ?? v.public_time ?? 0) * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
}));
},
});