mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-02 06:22:42 -08:00
fix: sanitize jellyfin misc info formatting
This commit is contained in:
@@ -316,3 +316,33 @@ test('FieldGroupingMergeCollaborator deduplicates identical sentence, audio, and
|
|||||||
assert.equal(merged.Picture, '<img data-group-id="202" src="same.png">');
|
assert.equal(merged.Picture, '<img data-group-id="202" src="same.png">');
|
||||||
assert.equal(merged.ExpressionAudio, merged.SentenceAudio);
|
assert.equal(merged.ExpressionAudio, merged.SentenceAudio);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('AnkiIntegration.formatMiscInfoPattern avoids leaking Jellyfin api_key query params', () => {
|
||||||
|
const integration = new AnkiIntegration(
|
||||||
|
{
|
||||||
|
metadata: {
|
||||||
|
pattern: '[SubMiner] %f (%t)',
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
{} as never,
|
||||||
|
{
|
||||||
|
currentSubText: '',
|
||||||
|
currentVideoPath:
|
||||||
|
'stream?static=true&api_key=secret-token&MediaSourceId=a762ab23d26d4347e3cacdb83aaae405&AudioStreamIndex=3',
|
||||||
|
currentTimePos: 426,
|
||||||
|
currentSubStart: 426,
|
||||||
|
currentSubEnd: 428,
|
||||||
|
currentAudioStreamIndex: 3,
|
||||||
|
currentMediaTitle: '[Jellyfin/direct] Bocchi the Rock! - S01E02',
|
||||||
|
send: () => true,
|
||||||
|
} as unknown as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateApi = integration as unknown as {
|
||||||
|
formatMiscInfoPattern: (fallbackFilename: string, startTimeSeconds?: number) => string;
|
||||||
|
};
|
||||||
|
const result = privateApi.formatMiscInfoPattern('audio_123.mp3', 426);
|
||||||
|
|
||||||
|
assert.equal(result, '[SubMiner] [Jellyfin/direct] Bocchi the Rock! - S01E02 (00:07:06)');
|
||||||
|
assert.equal(result.includes('api_key='), false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -58,6 +58,56 @@ interface NoteInfo {
|
|||||||
|
|
||||||
type CardKind = 'sentence' | 'audio';
|
type CardKind = 'sentence' | 'audio';
|
||||||
|
|
||||||
|
function trimToNonEmptyString(value: unknown): string | null {
|
||||||
|
if (typeof value !== 'string') return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeURIComponentSafe(value: string): string {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFilenameFromMediaPath(rawPath: string): string {
|
||||||
|
const trimmedPath = rawPath.trim();
|
||||||
|
if (!trimmedPath) return '';
|
||||||
|
|
||||||
|
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmedPath)) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(trimmedPath);
|
||||||
|
return decodeURIComponentSafe(path.basename(parsed.pathname));
|
||||||
|
} catch {
|
||||||
|
// Fall through to separator-based handling below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const separatorIndex = trimmedPath.search(/[?#]/);
|
||||||
|
const pathWithoutQuery =
|
||||||
|
separatorIndex >= 0 ? trimmedPath.slice(0, separatorIndex) : trimmedPath;
|
||||||
|
return decodeURIComponentSafe(path.basename(pathWithoutQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPreferMediaTitleForMiscInfo(rawPath: string, filename: string): boolean {
|
||||||
|
const loweredPath = rawPath.toLowerCase();
|
||||||
|
const loweredFilename = filename.toLowerCase();
|
||||||
|
if (loweredPath.includes('api_key=')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (loweredPath.startsWith('http://') || loweredPath.startsWith('https://')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
loweredFilename === 'stream' ||
|
||||||
|
loweredFilename === 'master.m3u8' ||
|
||||||
|
loweredFilename === 'index.m3u8' ||
|
||||||
|
loweredFilename === 'playlist.m3u8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export class AnkiIntegration {
|
export class AnkiIntegration {
|
||||||
private client: AnkiConnectClient;
|
private client: AnkiConnectClient;
|
||||||
private mediaGenerator: MediaGenerator;
|
private mediaGenerator: MediaGenerator;
|
||||||
@@ -729,8 +779,12 @@ export class AnkiIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentVideoPath = this.mpvClient.currentVideoPath || '';
|
const currentVideoPath = this.mpvClient.currentVideoPath || '';
|
||||||
const videoFilename = currentVideoPath ? path.basename(currentVideoPath) : '';
|
const videoFilename = extractFilenameFromMediaPath(currentVideoPath);
|
||||||
const filenameWithExt = videoFilename || fallbackFilename;
|
const mediaTitle = trimToNonEmptyString(this.mpvClient.currentMediaTitle);
|
||||||
|
const filenameWithExt =
|
||||||
|
(shouldPreferMediaTitleForMiscInfo(currentVideoPath, videoFilename)
|
||||||
|
? mediaTitle || videoFilename
|
||||||
|
: videoFilename || mediaTitle) || fallbackFilename;
|
||||||
const filenameWithoutExt = filenameWithExt.replace(/\.[^.]+$/, '');
|
const filenameWithoutExt = filenameWithExt.replace(/\.[^.]+$/, '');
|
||||||
|
|
||||||
const currentTimePos =
|
const currentTimePos =
|
||||||
|
|||||||
@@ -57,6 +57,26 @@ test('MpvIpcClient handles sub-text property change and broadcasts tokenized sub
|
|||||||
assert.equal(events[0]!.isOverlayVisible, false);
|
assert.equal(events[0]!.isOverlayVisible, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MpvIpcClient clears cached media title when media path changes', async () => {
|
||||||
|
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());
|
||||||
|
|
||||||
|
await invokeHandleMessage(client, {
|
||||||
|
event: 'property-change',
|
||||||
|
name: 'media-title',
|
||||||
|
data: '[Jellyfin/direct] Episode 1',
|
||||||
|
});
|
||||||
|
assert.equal(client.currentMediaTitle, '[Jellyfin/direct] Episode 1');
|
||||||
|
|
||||||
|
await invokeHandleMessage(client, {
|
||||||
|
event: 'property-change',
|
||||||
|
name: 'path',
|
||||||
|
data: '/tmp/new-episode.mkv',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(client.currentVideoPath, '/tmp/new-episode.mkv');
|
||||||
|
assert.equal(client.currentMediaTitle, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
||||||
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());
|
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());
|
||||||
const seen: Array<Record<string, unknown>> = [];
|
const seen: Array<Record<string, unknown>> = [];
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
private firstConnection = true;
|
private firstConnection = true;
|
||||||
private hasConnectedOnce = false;
|
private hasConnectedOnce = false;
|
||||||
public currentVideoPath = '';
|
public currentVideoPath = '';
|
||||||
|
public currentMediaTitle: string | null = null;
|
||||||
public currentTimePos = 0;
|
public currentTimePos = 0;
|
||||||
public currentSubStart = 0;
|
public currentSubStart = 0;
|
||||||
public currentSubEnd = 0;
|
public currentSubEnd = 0;
|
||||||
@@ -330,6 +331,7 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
this.emit('media-path-change', payload);
|
this.emit('media-path-change', payload);
|
||||||
},
|
},
|
||||||
emitMediaTitleChange: (payload) => {
|
emitMediaTitleChange: (payload) => {
|
||||||
|
this.currentMediaTitle = payload.title;
|
||||||
this.emit('media-title-change', payload);
|
this.emit('media-title-change', payload);
|
||||||
},
|
},
|
||||||
emitSubtitleMetricsChange: (patch) => {
|
emitSubtitleMetricsChange: (patch) => {
|
||||||
@@ -364,6 +366,7 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
},
|
},
|
||||||
setCurrentVideoPath: (value: string) => {
|
setCurrentVideoPath: (value: string) => {
|
||||||
this.currentVideoPath = value;
|
this.currentVideoPath = value;
|
||||||
|
this.currentMediaTitle = null;
|
||||||
},
|
},
|
||||||
emitSecondarySubtitleVisibility: (payload) => {
|
emitSecondarySubtitleVisibility: (payload) => {
|
||||||
this.emit('secondary-subtitle-visibility', payload);
|
this.emit('secondary-subtitle-visibility', payload);
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export interface NotificationOptions {
|
|||||||
export interface MpvClient {
|
export interface MpvClient {
|
||||||
currentSubText: string;
|
currentSubText: string;
|
||||||
currentVideoPath: string;
|
currentVideoPath: string;
|
||||||
|
currentMediaTitle?: string | null;
|
||||||
currentTimePos: number;
|
currentTimePos: number;
|
||||||
currentSubStart: number;
|
currentSubStart: number;
|
||||||
currentSubEnd: number;
|
currentSubEnd: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user