fix: clip time validation, cutter 0-byte check, pagination guard, atomic config write
H1: Add NaN/negative/zero-duration validation to clip dialog before IPC call H2: Reject cut video output <= 256 bytes as effectively empty H3: Add paginated VOD fetching with MAX_VOD_PAGES=50 safety guard H4: Atomic write (tmp+rename) for config and queue persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
39fa5065d2
commit
5f2e85e455
89
src/main.ts
89
src/main.ts
@ -269,8 +269,16 @@ function loadConfig(): Config {
|
||||
}
|
||||
|
||||
function saveConfig(config: Config): void {
|
||||
const tmpPath = CONFIG_FILE + '.tmp';
|
||||
try {
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
||||
try {
|
||||
fs.renameSync(tmpPath, CONFIG_FILE);
|
||||
} catch {
|
||||
// On Windows, rename can fail if target exists in some edge cases
|
||||
fs.copyFileSync(tmpPath, CONFIG_FILE);
|
||||
try { fs.unlinkSync(tmpPath); } catch { }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error saving config:', e);
|
||||
}
|
||||
@ -321,8 +329,16 @@ function writeQueueToDisk(queue: QueueItem[]): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const tmpPath = QUEUE_FILE + '.tmp';
|
||||
try {
|
||||
fs.writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2));
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(queue, null, 2));
|
||||
try {
|
||||
fs.renameSync(tmpPath, QUEUE_FILE);
|
||||
} catch {
|
||||
// On Windows, rename can fail if target exists in some edge cases
|
||||
fs.copyFileSync(tmpPath, QUEUE_FILE);
|
||||
try { fs.unlinkSync(tmpPath); } catch { }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error saving queue:', e);
|
||||
}
|
||||
@ -1633,13 +1649,18 @@ async function getVODs(userId: string, forceRefresh = false): Promise<VOD[]> {
|
||||
|
||||
if (!(await ensureTwitchAuth())) return await getVodsViaPublicApi();
|
||||
|
||||
const fetchVods = async () => {
|
||||
const MAX_VOD_PAGES = 50; // 50 pages x 100 per page = 5000 VODs max
|
||||
|
||||
const fetchVodsPage = async (cursor?: string) => {
|
||||
const params: Record<string, string | number> = {
|
||||
user_id: userId,
|
||||
type: 'archive',
|
||||
first: 100
|
||||
};
|
||||
if (cursor) params.after = cursor;
|
||||
|
||||
return await axios.get('https://api.twitch.tv/helix/videos', {
|
||||
params: {
|
||||
user_id: userId,
|
||||
type: 'archive',
|
||||
first: 100
|
||||
},
|
||||
params,
|
||||
headers: {
|
||||
'Client-ID': config.client_id,
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
@ -1648,26 +1669,38 @@ async function getVODs(userId: string, forceRefresh = false): Promise<VOD[]> {
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchVods();
|
||||
const vods = response.data.data || [];
|
||||
const login = vods[0]?.user_login;
|
||||
if (login) {
|
||||
userIdLoginCache.set(userId, normalizeLogin(login));
|
||||
}
|
||||
const fetchAllVodPages = async (): Promise<VOD[]> => {
|
||||
const allVods: VOD[] = [];
|
||||
let cursor: string | undefined;
|
||||
let pageCount = 0;
|
||||
|
||||
do {
|
||||
const response = await fetchVodsPage(cursor);
|
||||
const pageVods = response.data.data || [];
|
||||
allVods.push(...pageVods);
|
||||
|
||||
if (pageCount === 0) {
|
||||
const login = pageVods[0]?.user_login;
|
||||
if (login) {
|
||||
userIdLoginCache.set(userId, normalizeLogin(login));
|
||||
}
|
||||
}
|
||||
|
||||
cursor = response.data.pagination?.cursor;
|
||||
pageCount++;
|
||||
} while (cursor && pageCount < MAX_VOD_PAGES);
|
||||
|
||||
return allVods;
|
||||
};
|
||||
|
||||
try {
|
||||
const vods = await fetchAllVodPages();
|
||||
setCachedValue(vodListCache, cacheKey, vods, MAX_VOD_LIST_CACHE_ENTRIES);
|
||||
return vods;
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 401 && (await ensureTwitchAuth(true))) {
|
||||
try {
|
||||
const retryResponse = await fetchVods();
|
||||
const vods = retryResponse.data.data || [];
|
||||
const login = vods[0]?.user_login;
|
||||
if (login) {
|
||||
userIdLoginCache.set(userId, normalizeLogin(login));
|
||||
}
|
||||
|
||||
const vods = await fetchAllVodPages();
|
||||
setCachedValue(vodListCache, cacheKey, vods, MAX_VOD_LIST_CACHE_ENTRIES);
|
||||
return vods;
|
||||
} catch (retryError) {
|
||||
@ -1906,7 +1939,17 @@ async function cutVideo(
|
||||
|
||||
proc.on('close', (code) => {
|
||||
currentProcess = null;
|
||||
resolve(code === 0 && fs.existsSync(outputFile));
|
||||
if (code === 0 && fs.existsSync(outputFile)) {
|
||||
const stats = fs.statSync(outputFile);
|
||||
if (stats.size <= 256) {
|
||||
appendDebugLog('cut-video-empty-output', { outputFile, bytes: stats.size });
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', () => {
|
||||
|
||||
@ -805,17 +805,28 @@ async function confirmClipDialog(): Promise<void> {
|
||||
|
||||
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||
const durationSec = endSec - startSec;
|
||||
const startPartStr = byId<HTMLInputElement>('clipStartPart').value.trim();
|
||||
const startPart = startPartStr ? parseInt(startPartStr, 10) : 1;
|
||||
const filenameFormat = getSelectedFilenameFormat();
|
||||
const filenameTemplate = byId<HTMLInputElement>('clipFilenameTemplate').value.trim();
|
||||
|
||||
if (endSec <= startSec) {
|
||||
if (isNaN(startSec) || isNaN(endSec) || isNaN(durationSec)) {
|
||||
alert('Invalid time values');
|
||||
return;
|
||||
}
|
||||
|
||||
if (startSec < 0) {
|
||||
alert(UI_TEXT.clips.outOfRange);
|
||||
return;
|
||||
}
|
||||
|
||||
if (durationSec <= 0) {
|
||||
alert(UI_TEXT.clips.endBeforeStart);
|
||||
return;
|
||||
}
|
||||
|
||||
if (startSec < 0 || endSec > clipTotalSeconds) {
|
||||
if (endSec > clipTotalSeconds) {
|
||||
alert(UI_TEXT.clips.outOfRange);
|
||||
return;
|
||||
}
|
||||
@ -833,7 +844,6 @@ async function confirmClipDialog(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const durationSec = endSec - startSec;
|
||||
const customClip: CustomClip = {
|
||||
startSec,
|
||||
durationSec,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user