mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-08 07:21:31 -07:00
fix(subsync): decode file:// tracks mpv reports for dropped subtitles
- Sync a subtitle dropped onto mpv without hitting "Protocol file: not supported"; decode file:// external-filename/path back to a real path for target, reference, and video - Stream strip proxy: destroy the connection instead of retrying/502 once a response is handed off, and cap buffered playlist bodies - Bridge installer: cap downloaded bundle size to guard against a lying/missing content-length - Anime browser playback: bump generation on dispose so a stale in-flight playEpisode cleans up its own subtitle cache - Fix a flaky sidecar-process test by binding to an OS-assigned port instead of pre-allocating one
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
codecToExtension,
|
||||
fileExists,
|
||||
MpvTrack,
|
||||
resolveLocalMediaPath,
|
||||
runCommand,
|
||||
summarizeCommandFailure,
|
||||
} from '../../subsync/utils';
|
||||
@@ -148,10 +149,12 @@ export async function extractSubtitleTrackToFile(
|
||||
input: SubtitleExtractionInput,
|
||||
): Promise<FileExtractionResult> {
|
||||
if (input.track.external) {
|
||||
const externalPath = input.track['external-filename'];
|
||||
if (typeof externalPath !== 'string' || externalPath.length === 0) {
|
||||
const externalFilename = input.track['external-filename'];
|
||||
if (typeof externalFilename !== 'string' || externalFilename.length === 0) {
|
||||
throw new Error('External subtitle track has no file path');
|
||||
}
|
||||
// A dropped subtitle arrives as a `file://` URL, which is local, not remote.
|
||||
const externalPath = resolveLocalMediaPath(externalFilename);
|
||||
if (isRemoteMediaPath(externalPath)) {
|
||||
return downloadRemoteSubtitleTrack(externalPath, input.httpHeaders);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { runSubsyncManual } from './subsync';
|
||||
import type { TriggerSubsyncFromConfigDeps } from './subsync';
|
||||
|
||||
@@ -122,3 +123,97 @@ test('runSubsyncManual syncs stream subtitle tracks served over http', async (t)
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Dropping a subtitle onto mpv hands it a `file://` URL, and mpv reports that
|
||||
* URL back as the track's `external-filename`. Reading it as a stream made the
|
||||
* run die with `Protocol "file:" not supported. Expected "http:"`.
|
||||
*/
|
||||
test('runSubsyncManual retimes a dropped file:// track against a stream reference', async (t) => {
|
||||
if (process.platform === 'win32') {
|
||||
t.skip('stub shell scripts are not executable on Windows');
|
||||
return;
|
||||
}
|
||||
|
||||
const host = await startStubHost();
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subsync-file-url-'));
|
||||
const alassLogPath = path.join(tmpDir, 'alass-args.log');
|
||||
const alassPath = path.join(tmpDir, 'alass.sh');
|
||||
fs.writeFileSync(
|
||||
alassPath,
|
||||
`#!/bin/sh\n: > "${alassLogPath}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${alassLogPath}"; done\ncp "$2" "${tmpDir}/target.copy"\nprintf '%s' "retimed" > "$3"\nexit 0\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
// Percent-encoded on purpose: the space and the CJK are what a naive
|
||||
// "strip file://" would leave mangled.
|
||||
const targetPath = path.join(tmpDir, 'ワンピース sdh.srt');
|
||||
const targetBody = '1\n00:00:03,000 --> 00:00:04,000\nドロップ\n';
|
||||
fs.writeFileSync(targetPath, targetBody);
|
||||
const referenceUrl = host.url('/subs/en.srt');
|
||||
const sentCommands: Array<Array<string | number>> = [];
|
||||
|
||||
const deps: Pick<TriggerSubsyncFromConfigDeps, 'getMpvClient' | 'getResolvedConfig'> = {
|
||||
getMpvClient: () => ({
|
||||
connected: true,
|
||||
currentAudioStreamIndex: null,
|
||||
send: (payload) => {
|
||||
sentCommands.push(payload.command);
|
||||
},
|
||||
requestProperty: async (name: string) => {
|
||||
if (name === 'path') return host.url('/stream/video.m3u8');
|
||||
if (name === 'sid') return 2;
|
||||
if (name === 'secondary-sid') return null;
|
||||
if (name === 'track-list') {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
type: 'sub',
|
||||
selected: true,
|
||||
external: true,
|
||||
lang: 'en',
|
||||
'external-filename': referenceUrl,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'sub',
|
||||
selected: true,
|
||||
external: true,
|
||||
lang: 'ja',
|
||||
'external-filename': pathToFileURL(targetPath).href,
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
getResolvedConfig: () => ({
|
||||
alassPath,
|
||||
ffsubsyncPath: '',
|
||||
ffmpegPath: path.join(tmpDir, 'no-such-ffmpeg'),
|
||||
replace: true,
|
||||
}),
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await runSubsyncManual(
|
||||
{ engine: 'alass', referenceTrackId: 1, targetTrackId: 2 },
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.equal(result.ok, true, result.message);
|
||||
|
||||
const alassArgs = fs.readFileSync(alassLogPath, 'utf8').trim().split('\n');
|
||||
// The dropped file went in as-is, decoded, and was retimed in place.
|
||||
assert.equal(alassArgs[1], targetPath);
|
||||
assert.equal(fs.readFileSync(path.join(tmpDir, 'target.copy'), 'utf8'), targetBody);
|
||||
assert.equal(alassArgs[2], targetPath);
|
||||
assert.equal(fs.readFileSync(targetPath, 'utf8'), 'retimed');
|
||||
|
||||
const loadCommand = sentCommands.find((command) => command[0] === 'sub-add');
|
||||
assert.equal(loadCommand?.[1], targetPath);
|
||||
} finally {
|
||||
await host.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
formatTrackLabel,
|
||||
getTrackById,
|
||||
MpvTrack,
|
||||
resolveLocalMediaPath,
|
||||
runCommand,
|
||||
summarizeCommandFailure,
|
||||
SubsyncContext,
|
||||
@@ -136,7 +137,9 @@ async function gatherSubsyncContext(client: MpvClientLike): Promise<SubsyncConte
|
||||
client.requestProperty('track-list'),
|
||||
]);
|
||||
|
||||
const videoPath = typeof videoPathRaw === 'string' ? videoPathRaw : '';
|
||||
// A dropped file is reported as a `file://` URL; alass, ffsubsync and ffmpeg
|
||||
// all need the plain path, and the stream checks must not read it as remote.
|
||||
const videoPath = typeof videoPathRaw === 'string' ? resolveLocalMediaPath(videoPathRaw) : '';
|
||||
if (!videoPath) {
|
||||
throw new Error('No video is currently loaded');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user