Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CodeQL Analysis

on:
push:
branches:
- main
- refactor
pull_request:
branches:
- main
schedule:
- cron: '0 0 * * 0'

jobs:
analyze:
name: Analyze Code
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: [ 'javascript-typescript' ]

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
14 changes: 7 additions & 7 deletions src/utils/ffprobe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ mock.module('@clack/prompts', () => ({
}));

describe('utils/ffprobe.ts', () => {
const execSyncSpy = spyOn(child_process, 'execSync');
const execFileSyncSpy = spyOn(child_process, 'execFileSync');

afterEach(() => {
execSyncSpy.mockClear();
execFileSyncSpy.mockClear();
});

test('runQuickScan should complete successfully on valid media', () => {
execSyncSpy.mockReturnValueOnce(Buffer.from(''));
execFileSyncSpy.mockReturnValueOnce(Buffer.from(''));

expect(() => runQuickScan('valid.mkv')).not.toThrow();
});

test('runQuickScan should throw ValidationError when media is corrupted', () => {
execSyncSpy.mockImplementationOnce(() => {
execFileSyncSpy.mockImplementationOnce(() => {
throw new Error('Command failed');
});

Expand All @@ -47,7 +47,7 @@ describe('utils/ffprobe.ts', () => {
});

test('runQuickScan should throw JellyError when ffprobe is missing', () => {
execSyncSpy.mockImplementationOnce(() => {
execFileSyncSpy.mockImplementationOnce(() => {
const err = new Error('spawn ENOENT');
(err as any).code = 'ENOENT';
throw err;
Expand All @@ -73,7 +73,7 @@ describe('utils/ffprobe.ts', () => {

test('getMediaInfo should return parsed JSON data', () => {
const mockData = { format: { format_name: 'matroska' }, streams: [] };
execSyncSpy.mockReturnValueOnce(Buffer.from(JSON.stringify(mockData)));
execFileSyncSpy.mockReturnValueOnce(Buffer.from(JSON.stringify(mockData)));

const result = getMediaInfo('video.mkv');

Expand All @@ -84,7 +84,7 @@ describe('utils/ffprobe.ts', () => {
});

test('getMediaInfo should throw JellyError on execution failure', () => {
execSyncSpy.mockImplementationOnce(() => {
execFileSyncSpy.mockImplementationOnce(() => {
throw new Error('Parse fail');
});

Expand Down
31 changes: 25 additions & 6 deletions src/utils/ffprobe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import { spinner } from '@clack/prompts';
import pc from 'picocolors';
import { t } from './i18n.ts';
Expand All @@ -9,7 +9,16 @@ export function runQuickScan(videoPath: string) {
const qsSpinner = spinner();
qsSpinner.start(t('scanQuickStart'));
try {
execSync(`ffprobe -v error -show_entries format -of default=noprint_wrappers=1 "${videoPath}"`, { stdio: 'pipe' });
execFileSync(
'ffprobe',
[
'-v', 'error',
'-show_entries', 'format',
'-of', 'default=noprint_wrappers=1',
videoPath
],
{ stdio: 'pipe' }
);
qsSpinner.stop(pc.green(t('scanQuickPass')));
} catch (err) {
qsSpinner.stop(pc.red(t('scanQuickFail')));
Expand All @@ -25,13 +34,23 @@ export function getMediaInfo(videoPath: string): FFprobeData {
s.start(t('scanAnalyze'));

try {
const cmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`;
const result = execSync(cmd, { encoding: 'utf-8' });
const probeData = JSON.parse(result) as FFprobeData;
const resultBuffer = execFileSync(
'ffprobe',
[
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
videoPath
],
{ encoding: 'utf-8' }
);

const probeData = JSON.parse(resultBuffer as string) as FFprobeData;
s.stop(t('scanAnalyzeDone'));
return probeData;
} catch (err) {
s.stop(pc.red(t('scanAnalyzeErr')));
throw new JellyError(t('scanAnalyzeErr'), 'FFPROBE_JSON_ERROR');
}
}
}
9 changes: 8 additions & 1 deletion src/utils/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test, spyOn } from 'bun:test';
import fs from 'fs';
import { t, setLanguage, availableLanguages } from './i18n.ts';
import { t, setLanguage, availableLanguages, detectLanguage } from './i18n.ts';

describe('utils/i18n.ts', () => {
test('availableLanguages should expose only supported locales', () => {
Expand Down Expand Up @@ -38,4 +38,11 @@ describe('utils/i18n.ts', () => {

writeSpy.mockRestore();
});

test('detectLanguage should return lang from config file if valid', () => {
const readSpy = spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ lang: 'pt-BR' }));

expect(detectLanguage()).toBe('pt-BR');
readSpy.mockRestore();
});
});
29 changes: 15 additions & 14 deletions src/utils/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ const dictionaries = {
'en-US': enUS
} as const;

function detectLanguage(): keyof typeof dictionaries {
if (fs.existsSync(CONFIG_PATH)) {
try {
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) as Partial<UserSettings>;
if (config.lang && config.lang in dictionaries) {
return config.lang;
}
} catch (e) {}
export function detectLanguage(): keyof typeof dictionaries {
try {
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) as Partial<UserSettings>;
if (config.lang && config.lang in dictionaries) {
return config.lang;
}
} catch (e) {
// Ignored: File doesn't exist or JSON is invalid
}

const sysLocale = Intl.DateTimeFormat().resolvedOptions().locale;
Expand Down Expand Up @@ -48,15 +48,16 @@ export function setLanguage(lang: string) {
if (!(lang in dictionaries)) throw new Error(`Idioma ${lang} não suportado.`);

let config: Partial<UserSettings> = {};
if (fs.existsSync(CONFIG_PATH)) {
try {
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) as Partial<UserSettings>;
} catch (e) {
config = {};
}

try {
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) as Partial<UserSettings>;
} catch (e) {
config = {};
}

config.lang = lang as UserSettings['lang'];

fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
}

Expand Down
Loading
Loading