diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 49a6a39805..a803db4508 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -45,6 +45,8 @@ jobs: with: version: 8 run_install: false + - name: Install FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.0.2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index bf9badb3eb..55561e65f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440) - Fix: エンドポイント`notes/translate`のエラーを改善 - Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632) +- Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正 ## 2024.3.1 diff --git a/locales/index.d.ts b/locales/index.d.ts index c60395952b..67b76ba3d5 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8842,6 +8842,14 @@ export interface Locale extends ILocale { * ボタン */ "button": string; + /** + * 動的ブロック + */ + "dynamic": string; + /** + * このブロックは廃止されています。今後は{play}を利用してください。 + */ + "dynamicDescription": ParameterizedString<"play">; /** * ノート埋め込み */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e089e3b194..528367f303 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2334,6 +2334,8 @@ _pages: section: "セクション" image: "画像" button: "ボタン" + dynamic: "動的ブロック" + dynamicDescription: "このブロックは廃止されています。今後は{play}を利用してください。" note: "ノート埋め込み" _note: @@ -2633,4 +2635,3 @@ _externalNavigationWarning: title: "外部サイトに移動します" description: "{host}を離れて外部サイトに移動します" trustThisDomain: "このデバイスで今後このドメインを信頼する" - \ No newline at end of file diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index b8babcb3a7..169285f033 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -14,11 +14,12 @@ import FFmpeg from 'fluent-ffmpeg'; import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; import { type predictionType } from 'nsfwjs'; -import sharp from 'sharp'; import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import { encode } from 'blurhash'; import { createTempDir } from '@/misc/create-temp.js'; import { AiService } from '@/core/AiService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; export type FileInfo = { @@ -49,9 +50,13 @@ const TYPE_SVG = { @Injectable() export class FileInfoService { + private logger: Logger; + constructor( private aiService: AiService, + private loggerService: LoggerService, ) { + this.logger = this.loggerService.getLogger('file-info'); } /** @@ -317,6 +322,34 @@ export class FileInfoService { return mime; } + /** + * ビデオファイルにビデオトラックがあるかどうかチェック + * (ない場合:m4a, webmなど) + * + * @param path ファイルパス + * @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す) + */ + @bindThis + private hasVideoTrackOnVideoFile(path: string): Promise { + const sublogger = this.logger.createSubLogger('ffprobe'); + sublogger.info(`Checking the video file. File path: ${path}`); + return new Promise((resolve) => { + try { + FFmpeg.ffprobe(path, (err, metadata) => { + if (err) { + sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err); + resolve(true); + return; + } + resolve(metadata.streams.some((stream) => stream.codec_type === 'video')); + }); + } catch (err) { + sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error); + resolve(true); + } + }); + } + /** * Detect MIME Type and extension */ @@ -339,6 +372,20 @@ export class FileInfoService { return TYPE_SVG; } + if ((type.mime.startsWith('video') || type.mime === 'application/ogg') && !(await this.hasVideoTrackOnVideoFile(path))) { + const newMime = `audio/${type.mime.split('/')[1]}`; + if (newMime === 'audio/mp4') { + return { + mime: 'audio/mp4', + ext: 'm4a', + }; + } + return { + mime: newMime, + ext: type.ext, + }; + } + return { mime: this.fixMime(type.mime), ext: type.ext, diff --git a/packages/backend/test/resources/kick_gaba7.m4a b/packages/backend/test/resources/kick_gaba7.m4a new file mode 100644 index 0000000000..321df6349f Binary files /dev/null and b/packages/backend/test/resources/kick_gaba7.m4a differ diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index 2eec80d763..40d187f5a8 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -15,6 +15,7 @@ import { GlobalModule } from '@/GlobalModule.js'; import { FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; import { AiService } from '@/core/AiService.js'; +import { LoggerService } from '@/core/LoggerService.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -35,6 +36,7 @@ describe('FileInfoService', () => { ], providers: [ AiService, + LoggerService, FileInfoService, ], }) @@ -323,8 +325,26 @@ describe('FileInfoService', () => { }); }); - /* - * video/webmとして検出されてしまう + test('MPEG-4 AUDIO (M4A)', async () => { + const path = `${resources}/kick_gaba7.m4a`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + delete info.width; + delete info.height; + delete info.orientation; + assert.deepStrictEqual(info, { + size: 9817, + md5: '74c9279a4abe98789565f1dc1a541a42', + type: { + mime: 'audio/mp4', + ext: 'm4a', + }, + }); + }); + test('WEBM AUDIO', async () => { const path = `${resources}/kick_gaba7.webm`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -337,13 +357,12 @@ describe('FileInfoService', () => { delete info.orientation; assert.deepStrictEqual(info, { size: 8879, - md5: '3350083dec312419cfdc06c16413aca7', + md5: '53bc1adcb6acbbda67ff9bd484896438', type: { mime: 'audio/webm', ext: 'webm', }, }); }); - */ }); }); diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue index 164720ac6b..c7f72dce8c 100644 --- a/packages/frontend/src/components/page/page.block.vue +++ b/packages/frontend/src/components/page/page.block.vue @@ -14,6 +14,7 @@ import XText from './page.text.vue'; import XSection from './page.section.vue'; import XImage from './page.image.vue'; import XNote from './page.note.vue'; +import XDynamic from './page.dynamic.vue'; function getComponent(type: string) { switch (type) { @@ -21,6 +22,20 @@ function getComponent(type: string) { case 'section': return XSection; case 'image': return XImage; case 'note': return XNote; + + // 動的ページの代替用ブロック + case 'button': + case 'if': + case 'textarea': + case 'post': + case 'canvas': + case 'numberInput': + case 'textInput': + case 'switch': + case 'radioButton': + case 'counter': + return XDynamic; + default: return null; } } diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue new file mode 100644 index 0000000000..8c511a690d --- /dev/null +++ b/packages/frontend/src/components/page/page.dynamic.vue @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue index 4e501bd699..e0c7956f6e 100644 --- a/packages/frontend/src/components/page/page.text.vue +++ b/packages/frontend/src/components/page/page.text.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only