Skip to content

Commit 01df666

Browse files
committed
Add video task list and delete support
1 parent 7ba4460 commit 01df666

10 files changed

Lines changed: 401 additions & 0 deletions

File tree

‎src/client/endpoints.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ export function videoTaskV2Endpoint(baseUrl: string, taskId: string): string {
3030
return `${baseUrl}/v2/query/video_generation/${taskId}`;
3131
}
3232

33+
export function videoTaskV2ListEndpoint(baseUrl: string): string {
34+
return `${baseUrl}/v2/query/video_generation`;
35+
}
36+
37+
export function videoTaskV2DeleteEndpoint(baseUrl: string, taskId: string): string {
38+
return `${baseUrl}/v2/video_generation/${taskId}`;
39+
}
40+
3341
export function fileRetrieveEndpoint(baseUrl: string, fileId: string): string {
3442
return `${baseUrl}/v1/files/retrieve?file_id=${fileId}`;
3543
}

‎src/commands/video/task-delete.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { defineCommand } from '../../command';
2+
import { CLIError } from '../../errors/base';
3+
import { ExitCode } from '../../errors/codes';
4+
import { requestJson } from '../../client/http';
5+
import { videoTaskV2DeleteEndpoint } from '../../client/endpoints';
6+
import { formatOutput, detectOutputFormat } from '../../output/formatter';
7+
import type { Config } from '../../config/schema';
8+
import type { GlobalFlags } from '../../types/flags';
9+
import type { VideoV2TaskDeleteResponse } from '../../types/api';
10+
11+
export default defineCommand({
12+
name: 'video task delete',
13+
description: 'Delete a Video Generation V2 task',
14+
apiDocs: '/docs/api-reference/video-generation-v2-delete',
15+
usage: 'mmx video task delete --task-id <id>',
16+
options: [
17+
{ flag: '--task-id <id>', description: 'Video Generation V2 task ID', required: true },
18+
],
19+
examples: [
20+
'mmx video task delete --task-id 424010985738629',
21+
],
22+
async run(config: Config, flags: GlobalFlags) {
23+
const taskId = flags.taskId as string | undefined;
24+
if (!taskId) {
25+
throw new CLIError(
26+
'--task-id is required.',
27+
ExitCode.USAGE,
28+
'mmx video task delete --task-id <id>',
29+
);
30+
}
31+
32+
const format = detectOutputFormat(config.output);
33+
34+
if (config.dryRun) {
35+
process.stdout.write(formatOutput({ request: { delete_video_task: taskId } }, format) + '\n');
36+
return;
37+
}
38+
39+
const url = videoTaskV2DeleteEndpoint(config.baseUrl, taskId);
40+
const response = await requestJson<VideoV2TaskDeleteResponse>(config, {
41+
url,
42+
method: 'DELETE',
43+
});
44+
45+
if (config.quiet) {
46+
process.stdout.write(`${response.status}\n`);
47+
return;
48+
}
49+
50+
process.stdout.write(formatOutput(response, format) + '\n');
51+
},
52+
});

‎src/commands/video/task-list.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { defineCommand } from '../../command';
2+
import { requestJson } from '../../client/http';
3+
import { videoTaskV2ListEndpoint } from '../../client/endpoints';
4+
import { formatOutput, detectOutputFormat } from '../../output/formatter';
5+
import type { Config } from '../../config/schema';
6+
import type { GlobalFlags } from '../../types/flags';
7+
import type { VideoV2TaskListResponse } from '../../types/api';
8+
import { VIDEO_V2_MODEL } from '../../video/v2';
9+
10+
export default defineCommand({
11+
name: 'video task list',
12+
description: 'List Video Generation V2 tasks',
13+
apiDocs: '/docs/api-reference/video-generation-v2-list',
14+
usage: 'mmx video task list [flags]',
15+
options: [
16+
{ flag: '--page-num <number>', description: 'Page number to request', type: 'number' },
17+
{ flag: '--page-size <number>', description: 'Page size to request', type: 'number' },
18+
{ flag: '--status <status>', description: 'Filter by task status' },
19+
{ flag: '--task-id <id>', description: 'Filter by task ID; repeatable', type: 'array' },
20+
{ flag: '--task-type <type>', description: 'Filter by task type' },
21+
],
22+
examples: [
23+
'mmx video task list --output json',
24+
'mmx video task list --status succeeded --page-size 20',
25+
],
26+
async run(config: Config, flags: GlobalFlags) {
27+
const format = detectOutputFormat(config.output);
28+
const url = new URL(videoTaskV2ListEndpoint(config.baseUrl));
29+
30+
if (flags.pageNum !== undefined) url.searchParams.set('page_num', String(flags.pageNum));
31+
if (flags.pageSize !== undefined) url.searchParams.set('page_size', String(flags.pageSize));
32+
if (flags.status) url.searchParams.set('filter.status', String(flags.status));
33+
const taskIds = flags.taskId as string[] | undefined;
34+
if (taskIds?.length) url.searchParams.set('filter.task_ids', taskIds.join(','));
35+
url.searchParams.set('filter.model', VIDEO_V2_MODEL);
36+
if (flags.taskType) url.searchParams.set('filter.task_type', String(flags.taskType));
37+
38+
if (config.dryRun) {
39+
process.stdout.write(formatOutput({ request: { url: url.toString() } }, format) + '\n');
40+
return;
41+
}
42+
43+
const response = await requestJson<VideoV2TaskListResponse>(config, { url: url.toString() });
44+
45+
if (format !== 'text') {
46+
process.stdout.write(formatOutput(response, format) + '\n');
47+
return;
48+
}
49+
50+
if (!response.items || response.items.length === 0) {
51+
process.stdout.write('No video tasks found.\n');
52+
return;
53+
}
54+
55+
const tableData = response.items.map(task => ({
56+
ID: task.id,
57+
MODEL: task.model,
58+
STATUS: task.status,
59+
RESOLUTION: task.resolution ?? '',
60+
DURATION: task.duration ?? '',
61+
}));
62+
63+
const { formatTable } = await import('../../output/text');
64+
process.stdout.write(formatTable(tableData) + '\n');
65+
},
66+
});

‎src/registry.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import speechVoices from './commands/speech/voices';
1414
import imageGenerate from './commands/image/generate';
1515
import videoGenerate from './commands/video/generate';
1616
import videoTaskGet from './commands/video/task-get';
17+
import videoTaskList from './commands/video/task-list';
18+
import videoTaskDelete from './commands/video/task-delete';
1719
import videoDownload from './commands/video/download';
1820
import musicGenerate from './commands/music/generate';
1921
import musicCover from './commands/music/cover';
@@ -293,6 +295,8 @@ export const registry = new CommandRegistry({
293295
'image generate': imageGenerate,
294296
'video generate': videoGenerate,
295297
'video task get': videoTaskGet,
298+
'video task list': videoTaskList,
299+
'video task delete': videoTaskDelete,
296300
'video download': videoDownload,
297301
'music generate': musicGenerate,
298302
'music cover': musicCover,

‎src/sdk/video/index.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import {
44
videoGenerateEndpoint,
55
videoGenerateV2Endpoint,
66
videoTaskEndpoint,
7+
videoTaskV2DeleteEndpoint,
78
videoTaskV2Endpoint,
9+
videoTaskV2ListEndpoint,
810
} from "../../client/endpoints";
911
import {
1012
FileRetrieveResponse,
1113
VideoRequest,
1214
VideoTaskResponse,
15+
VideoV2TaskDeleteResponse,
16+
VideoV2TaskListResponse,
1317
VideoV2Request,
1418
VideoV2Response,
1519
VideoV2Task,
@@ -49,6 +53,14 @@ export interface VideoDownloadRequest {
4953
outPath: string;
5054
}
5155

56+
export interface VideoV2ListTasksRequest {
57+
pageNum?: number;
58+
pageSize?: number;
59+
status?: string;
60+
taskIds?: string[];
61+
taskType?: string;
62+
}
63+
5264
export class VideoSDK extends Client {
5365
async generate(request: VideoAsyncGenerateRequest & { async: true }): Promise<{taskId: string}>;
5466
async generate(request: VideoAsyncGenerateRequest): Promise<VideoTaskResponse | VideoV2Task>;
@@ -105,6 +117,23 @@ export class VideoSDK extends Client {
105117
return await this.requestJson<VideoTaskResponse>({ url });
106118
}
107119

120+
async listTasks(request: VideoV2ListTasksRequest = {}): Promise<VideoV2TaskListResponse> {
121+
const url = new URL(videoTaskV2ListEndpoint(this.config.baseUrl));
122+
if (request.pageNum !== undefined) url.searchParams.set('page_num', String(request.pageNum));
123+
if (request.pageSize !== undefined) url.searchParams.set('page_size', String(request.pageSize));
124+
if (request.status) url.searchParams.set('filter.status', request.status);
125+
if (request.taskIds?.length) url.searchParams.set('filter.task_ids', request.taskIds.join(','));
126+
url.searchParams.set('filter.model', VIDEO_V2_MODEL);
127+
if (request.taskType) url.searchParams.set('filter.task_type', request.taskType);
128+
129+
return await this.requestJson<VideoV2TaskListResponse>({ url: url.toString() });
130+
}
131+
132+
async deleteTask({taskId}: {taskId: string}): Promise<VideoV2TaskDeleteResponse> {
133+
const url = videoTaskV2DeleteEndpoint(this.config.baseUrl, taskId);
134+
return await this.requestJson<VideoV2TaskDeleteResponse>({ url, method: 'DELETE' });
135+
}
136+
108137
async download(request: VideoDownloadRequest) {
109138
const url = fileRetrieveEndpoint(this.config.baseUrl, request.fileId);
110139
const fileInfo = await this.requestJson<FileRetrieveResponse>({ url });

‎src/types/api.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,24 @@ export interface VideoV2Task {
271271
};
272272
ratio?: string;
273273
task_type?: string;
274+
modality?: string;
274275
}
275276

276277
export interface VideoV2TaskResponse {
277278
task: VideoV2Task;
278279
}
279280

281+
export interface VideoV2TaskListResponse {
282+
items: VideoV2Task[];
283+
total: number;
284+
}
285+
286+
export interface VideoV2TaskDeleteResponse {
287+
task_id: string;
288+
action: string;
289+
status: string;
290+
}
291+
280292
// ---- Music ----
281293

282294
export interface MusicRequest {

‎test/commands/aliases.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ describe('command aliases', () => {
2626
expect(registry.resolve(['file', 'list']).command.name).toBe('file list');
2727
expect(registry.resolve(['file', 'delete']).command.name).toBe('file delete');
2828
});
29+
30+
it('resolves video task management commands', () => {
31+
expect(registry.resolve(['video', 'task', 'get']).command.name).toBe('video task get');
32+
expect(registry.resolve(['video', 'task', 'list']).command.name).toBe('video task list');
33+
expect(registry.resolve(['video', 'task', 'delete']).command.name).toBe('video task delete');
34+
});
2935
});
3036

3137
describe('text chat --prompt alias', () => {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it, expect, afterEach } from 'bun:test';
2+
import { default as taskDeleteCommand } from '../../../src/commands/video/task-delete';
3+
import { createMockServer, jsonResponse, type MockServer } from '../../helpers/mock-server';
4+
import type { Config } from '../../../src/config/schema';
5+
import type { GlobalFlags } from '../../../src/types/flags';
6+
7+
const baseConfig: Config = {
8+
apiKey: 'test-key',
9+
region: 'global',
10+
baseUrl: 'https://api.mmx.io',
11+
output: 'json',
12+
timeout: 10,
13+
verbose: false,
14+
quiet: false,
15+
noColor: true,
16+
yes: false,
17+
dryRun: false,
18+
nonInteractive: true,
19+
async: false,
20+
};
21+
22+
const baseFlags: GlobalFlags = {
23+
quiet: false,
24+
verbose: false,
25+
noColor: true,
26+
yes: false,
27+
dryRun: false,
28+
help: false,
29+
nonInteractive: true,
30+
async: false,
31+
};
32+
33+
describe('video task delete command', () => {
34+
let server: MockServer;
35+
36+
afterEach(() => {
37+
server?.close();
38+
});
39+
40+
it('requires task-id', async () => {
41+
await expect(taskDeleteCommand.execute(baseConfig, baseFlags))
42+
.rejects.toThrow('--task-id is required');
43+
});
44+
45+
it('deletes a Video Generation V2 task', async () => {
46+
let method = '';
47+
server = createMockServer({
48+
routes: {
49+
'/v2/video_generation/h3-123': (req) => {
50+
method = req.method;
51+
return jsonResponse({ task_id: 'h3-123', action: 'delete', status: 'deleted' });
52+
},
53+
},
54+
});
55+
56+
let output = '';
57+
const originalWrite = process.stdout.write;
58+
process.stdout.write = ((chunk: string) => {
59+
output += chunk;
60+
return true;
61+
}) as typeof process.stdout.write;
62+
63+
try {
64+
await taskDeleteCommand.execute(
65+
{ ...baseConfig, baseUrl: server.url },
66+
{ ...baseFlags, taskId: 'h3-123' },
67+
);
68+
} finally {
69+
process.stdout.write = originalWrite;
70+
}
71+
72+
expect(method).toBe('DELETE');
73+
expect(JSON.parse(output)).toEqual({
74+
task_id: 'h3-123',
75+
action: 'delete',
76+
status: 'deleted',
77+
});
78+
});
79+
});

0 commit comments

Comments
 (0)