diff --git a/interface.d.ts b/interface.d.ts index 56f894c..9c430ff 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -2802,6 +2802,22 @@ export function voice_upload( name: string data: string | Buffer } + imgFile?: { + name: string + data: string | Buffer + } + voiceListId: string | number + coverImgId?: string | number + categoryId: string | number + secondCategoryId: string | number + description: string + songName?: string + privacy?: string | number + publishTime?: string | number + autoPublish?: string | number + autoPublishText?: string + orderNo?: string | number + composedSongs?: string } & RequestBaseConfig, ): Promise diff --git a/module/voice_upload.js b/module/voice_upload.js index 986fa71..0e4241b 100644 --- a/module/voice_upload.js +++ b/module/voice_upload.js @@ -2,6 +2,7 @@ const { default: axios } = require('axios') const fs = require('fs') var xml2js = require('xml2js') +const uploadPlugin = require('../plugins/upload') const createOption = require('../util/option.js') const { getFileExtension, readFileChunk } = require('../util/fileHelper') @@ -19,15 +20,7 @@ function createDupkey() { return s.join('') } -module.exports = async (query, request) => { - const ext = getFileExtension(query.songFile.name) - const filename = - query.songName || - query.songFile.name - .replace('.' + ext, '') - .replace(/\s/g, '') - .replace(/\./g, '_') - +module.exports = async (query, request, dependencies = {}) => { if (!query.songFile) { return Promise.reject({ status: 500, @@ -38,6 +31,19 @@ module.exports = async (query, request) => { }) } + const axiosRequest = dependencies.axios || axios + const uploadImage = dependencies.uploadPlugin || uploadPlugin + const ext = getFileExtension(query.songFile.name) + const filename = + query.songName || + query.songFile.name + .replace('.' + ext, '') + .replace(/\s/g, '') + .replace(/\./g, '_') + const coverImgId = query.imgFile + ? (await uploadImage(query, request)).imgId + : query.coverImgId + const tokenRes = await request( `/api/nos/token/alloc`, { @@ -53,7 +59,7 @@ module.exports = async (query, request) => { const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F') const docId = tokenRes.body.result.docId - const res = await axios({ + const res = await axiosRequest({ method: 'post', url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`, headers: { @@ -94,7 +100,7 @@ module.exports = async (query, request) => { ) } - const res3 = await axios({ + const res3 = await axiosRequest({ method: 'put', url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`, headers: { @@ -117,7 +123,7 @@ module.exports = async (query, request) => { } completeStr += '' - await axios({ + await axiosRequest({ method: 'post', url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`, headers: { @@ -128,29 +134,29 @@ module.exports = async (query, request) => { data: completeStr, }) + const voiceData = JSON.stringify([ + { + name: filename, + autoPublish: query.autoPublish == 1 ? true : false, + autoPublishText: query.autoPublishText || '', + description: query.description, + voiceListId: query.voiceListId, + coverImgId, + dfsId: docId, + categoryId: query.categoryId, + secondCategoryId: query.secondCategoryId, + composedSongs: query.composedSongs ? query.composedSongs.split(',') : [], + privacy: query.privacy == 1 ? true : false, + publishTime: query.publishTime || 0, + orderNo: query.orderNo || 1, + }, + ]) + await request( `/api/voice/workbench/voice/batch/upload/preCheck`, { dupkey: createDupkey(), - voiceData: JSON.stringify([ - { - name: filename, - autoPublish: query.autoPublish == 1 ? true : false, - autoPublishText: query.autoPublishText || '', - description: query.description, - voiceListId: query.voiceListId, - coverImgId: query.coverImgId, - dfsId: docId, - categoryId: query.categoryId, - secondCategoryId: query.secondCategoryId, - composedSongs: query.composedSongs - ? query.composedSongs.split(',') - : [], - privacy: query.privacy == 1 ? true : false, - publishTime: query.publishTime || 0, - orderNo: query.orderNo || 1, - }, - ]), + voiceData, }, { ...createOption(query), @@ -163,25 +169,7 @@ module.exports = async (query, request) => { `/api/voice/workbench/voice/batch/upload/v2`, { dupkey: createDupkey(), - voiceData: JSON.stringify([ - { - name: filename, - autoPublish: query.autoPublish == 1 ? true : false, - autoPublishText: query.autoPublishText || '', - description: query.description, - voiceListId: query.voiceListId, - coverImgId: query.coverImgId, - dfsId: docId, - categoryId: query.categoryId, - secondCategoryId: query.secondCategoryId, - composedSongs: query.composedSongs - ? query.composedSongs.split(',') - : [], - privacy: query.privacy == 1 ? true : false, - publishTime: query.publishTime || 0, - orderNo: query.orderNo || 1, - }, - ]), + voiceData, }, { ...createOption(query), diff --git a/public/docs/home.md b/public/docs/home.md index e4f172b..b01689c 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -4371,27 +4371,33 @@ ONLINE 已发布 ### 播客上传声音 -说明: 可以上传声音到播客,例子在 `/public/voice_upload.html` 访问地址: /voice_upload.html +说明: 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传声音文件 formData(name 为`songFile`),可通过 formData(name 为`imgFile`)同时上传声音封面。例子在 `/public/voice_upload.html` 访问地址: /voice_upload.html **接口地址:** `/voice/upload` **必选参数:** -`voiceListId`: 播客 id -`coverImgId`: 播客封面 +`songFile`: 声音文件 + +`voiceListId`: 播客 id `categoryId`: 分类 id -`secondCategoryId`:次级分类 id +`secondCategoryId`: 次级分类 id `description`: 声音介绍 **可选参数:** + +`imgFile`: 声音封面图片文件,上传后会自动生成图片 id。与`coverImgId`同时传入时,优先使用`imgFile` + +`coverImgId`: 已上传的声音封面图片 id,未传入`imgFile`时使用该值 + `songName`: 声音名称 -`privacy`: 设为隐私声音,播客如果是隐私博客,则必须设为 1 +`privacy`: 设为隐私声音,播客如果是隐私播客,则必须设为 1 -`publishTime`:默认立即发布,定时发布的话需传入时间戳 +`publishTime`: 默认立即发布,定时发布的话需传入时间戳 `autoPublish`: 是否发布动态,是则传入 1 diff --git a/public/voice_upload.html b/public/voice_upload.html index e8f9e40..5833422 100644 --- a/public/voice_upload.html +++ b/public/voice_upload.html @@ -217,8 +217,13 @@
- - + + +
+ +
+ +
@@ -251,12 +256,13 @@ methods: { submit() { console.info('submit') - const file = document.querySelector('input[type=file]').files[0] - if (!file) { - alert('请选择文件') + const songFile = document.querySelector('input[name=songFile]').files[0] + const imgFile = document.querySelector('input[name=imgFile]').files[0] + if (!songFile) { + alert('请选择声音文件') return } - this.upload(file) + this.upload(songFile, imgFile) }, async getData() { @@ -286,14 +292,17 @@ } }, - upload(file) { + upload(songFile, imgFile) { if (!this.currentVoice) { alert('请先选择播客列表') return } var formData = new FormData() - formData.append('songFile', file) + formData.append('songFile', songFile) + if (imgFile) { + formData.append('imgFile', imgFile) + } axios({ method: 'post', @@ -312,7 +321,7 @@ data: formData, }) .then((res) => { - alert(`${file.name} 上传成功`) + alert(`${songFile.name} 上传成功`) }) .catch((err) => { console.error('上传失败:', err) diff --git a/test/voice_upload.test.js b/test/voice_upload.test.js new file mode 100644 index 0000000..0e42a33 --- /dev/null +++ b/test/voice_upload.test.js @@ -0,0 +1,129 @@ +const assert = require('assert') +const voiceUpload = require('../module/voice_upload') + +function createVoiceUploadHarness(uploadPlugin) { + const requestCalls = [] + + return { + requestCalls, + dependencies: { + uploadPlugin, + axios: async (options) => { + if (options.method === 'post' && options.url.endsWith('?uploads')) { + return { + data: 'upload-id', + } + } + + if (options.method === 'put') { + return { headers: { etag: 'etag-1' } } + } + + return { data: {} } + }, + }, + request: async (uri, data, options) => { + requestCalls.push({ uri, data, options }) + + if (uri === '/api/nos/token/alloc') { + return { + body: { + result: { + objectKey: 'voice/audio.mp3', + docId: 'audio-doc-id', + token: 'nos-token', + }, + }, + } + } + + if (uri === '/api/voice/workbench/voice/batch/upload/v2') { + return { body: { data: { voiceId: 'voice-id' } } } + } + + return { body: { code: 200 } } + }, + } +} + +function createVoiceUploadQuery(overrides = {}) { + const data = Buffer.from('audio') + + return { + songFile: { + name: 'episode.mp3', + mimetype: 'audio/mpeg', + size: data.length, + data, + }, + voiceListId: 'voice-list-id', + categoryId: 'category-id', + secondCategoryId: 'second-category-id', + description: 'episode description', + ...overrides, + } +} + +describe('voice upload cover', () => { + it('uses the uploaded image as the cover for every voice submission', async () => { + let uploadedQuery + const harness = createVoiceUploadHarness(async (query) => { + uploadedQuery = query + return { imgId: 'uploaded-cover-id' } + }) + const query = createVoiceUploadQuery({ + imgFile: { + name: 'cover.jpg', + mimetype: 'image/jpeg', + data: Buffer.from('image'), + }, + coverImgId: 'fallback-cover-id', + }) + + const result = await voiceUpload( + query, + harness.request, + harness.dependencies, + ) + const voiceCalls = harness.requestCalls.filter((call) => + call.uri.startsWith('/api/voice/workbench/voice/batch/upload'), + ) + + assert.strictEqual(uploadedQuery, query) + assert.strictEqual(voiceCalls.length, 2) + voiceCalls.forEach((call) => { + const [voiceData] = JSON.parse(call.data.voiceData) + assert.strictEqual(voiceData.coverImgId, 'uploaded-cover-id') + }) + assert.deepStrictEqual(result, { + status: 200, + body: { + code: 200, + data: { voiceId: 'voice-id' }, + }, + }) + }) + + it('keeps using coverImgId when no image file is uploaded', async () => { + let uploaded = false + const harness = createVoiceUploadHarness(async () => { + uploaded = true + return { imgId: 'unexpected-cover-id' } + }) + + await voiceUpload( + createVoiceUploadQuery({ coverImgId: 'existing-cover-id' }), + harness.request, + harness.dependencies, + ) + + const voiceCalls = harness.requestCalls.filter((call) => + call.uri.startsWith('/api/voice/workbench/voice/batch/upload'), + ) + assert.strictEqual(uploaded, false) + voiceCalls.forEach((call) => { + const [voiceData] = JSON.parse(call.data.voiceData) + assert.strictEqual(voiceData.coverImgId, 'existing-cover-id') + }) + }) +})