9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
69 lines
3.4 KiB
JavaScript
69 lines
3.4 KiB
JavaScript
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { formatBytes } from '@jackwener/opencli/download/progress';
|
|
import { httpDownload, sanitizeFilename } from '@jackwener/opencli/download';
|
|
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import { getDoubanPhotoExtension, loadDoubanSubjectPhotos, normalizeDoubanSubjectId } from './utils.js';
|
|
function buildDoubanPhotoFilename(subjectId, photo) {
|
|
const index = String(photo.index).padStart(3, '0');
|
|
const suffix = sanitizeFilename(photo.title || photo.photoId || 'photo', 80) || 'photo';
|
|
return `${subjectId}_${index}_${photo.photoId || 'photo'}_${suffix}${getDoubanPhotoExtension(photo.imageUrl)}`;
|
|
}
|
|
cli({
|
|
site: 'douban',
|
|
name: 'download',
|
|
access: 'read',
|
|
description: '下载电影海报/剧照图片',
|
|
domain: 'movie.douban.com',
|
|
strategy: Strategy.COOKIE,
|
|
args: [
|
|
{ name: 'id', positional: true, required: true, help: '电影 subject ID' },
|
|
{ name: 'type', default: 'Rb', help: '豆瓣 photos 的 type 参数,默认 Rb(海报)' },
|
|
{ name: 'limit', type: 'int', default: 120, help: '最多下载多少张图片' },
|
|
{ name: 'photo-id', help: '只下载指定 photo_id 的图片' },
|
|
{ name: 'output', default: './douban-downloads', help: '输出目录' },
|
|
],
|
|
columns: ['index', 'title', 'status', 'size'],
|
|
func: async (page, kwargs) => {
|
|
const subjectId = normalizeDoubanSubjectId(String(kwargs.id || ''));
|
|
const output = String(kwargs.output || './douban-downloads');
|
|
const requestedPhotoId = String(kwargs['photo-id'] || '').trim();
|
|
const loadOptions = {
|
|
type: String(kwargs.type || 'Rb'),
|
|
};
|
|
if (requestedPhotoId)
|
|
loadOptions.targetPhotoId = requestedPhotoId;
|
|
else
|
|
loadOptions.limit = Number(kwargs.limit) || 120;
|
|
const data = await loadDoubanSubjectPhotos(page, subjectId, loadOptions);
|
|
const photos = requestedPhotoId
|
|
? data.photos.filter((photo) => photo.photoId === requestedPhotoId)
|
|
: data.photos;
|
|
if (requestedPhotoId && !photos.length) {
|
|
throw new EmptyResultError('douban download', `Photo ID ${requestedPhotoId} was not found under subject ${subjectId}. Try "douban photos ${subjectId} -f json" first.`);
|
|
}
|
|
const outputDir = path.join(output, subjectId);
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
const results = [];
|
|
for (const photo of photos) {
|
|
const filename = buildDoubanPhotoFilename(subjectId, photo);
|
|
const destPath = path.join(outputDir, filename);
|
|
const result = await httpDownload(photo.imageUrl, destPath, {
|
|
headers: { Referer: photo.detailUrl || `https://movie.douban.com/subject/${subjectId}/photos?type=${encodeURIComponent(String(kwargs.type || 'Rb'))}` },
|
|
timeout: 60000,
|
|
});
|
|
results.push({
|
|
index: photo.index,
|
|
title: photo.title,
|
|
photo_id: photo.photoId,
|
|
image_url: photo.imageUrl,
|
|
detail_url: photo.detailUrl,
|
|
status: result.success ? 'success' : 'failed',
|
|
size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
|
|
});
|
|
}
|
|
return results;
|
|
},
|
|
});
|