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
79
src/main.ts
79
src/main.ts
@ -269,8 +269,16 @@ function loadConfig(): Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveConfig(config: Config): void {
|
function saveConfig(config: Config): void {
|
||||||
|
const tmpPath = CONFIG_FILE + '.tmp';
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
console.error('Error saving config:', e);
|
console.error('Error saving config:', e);
|
||||||
}
|
}
|
||||||
@ -321,8 +329,16 @@ function writeQueueToDisk(queue: QueueItem[]): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tmpPath = QUEUE_FILE + '.tmp';
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
console.error('Error saving queue:', 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();
|
if (!(await ensureTwitchAuth())) return await getVodsViaPublicApi();
|
||||||
|
|
||||||
const fetchVods = async () => {
|
const MAX_VOD_PAGES = 50; // 50 pages x 100 per page = 5000 VODs max
|
||||||
return await axios.get('https://api.twitch.tv/helix/videos', {
|
|
||||||
params: {
|
const fetchVodsPage = async (cursor?: string) => {
|
||||||
|
const params: Record<string, string | number> = {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
type: 'archive',
|
type: 'archive',
|
||||||
first: 100
|
first: 100
|
||||||
},
|
};
|
||||||
|
if (cursor) params.after = cursor;
|
||||||
|
|
||||||
|
return await axios.get('https://api.twitch.tv/helix/videos', {
|
||||||
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
'Client-ID': config.client_id,
|
'Client-ID': config.client_id,
|
||||||
'Authorization': `Bearer ${accessToken}`
|
'Authorization': `Bearer ${accessToken}`
|
||||||
@ -1648,26 +1669,38 @@ async function getVODs(userId: string, forceRefresh = false): Promise<VOD[]> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const fetchAllVodPages = async (): Promise<VOD[]> => {
|
||||||
const response = await fetchVods();
|
const allVods: VOD[] = [];
|
||||||
const vods = response.data.data || [];
|
let cursor: string | undefined;
|
||||||
const login = vods[0]?.user_login;
|
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) {
|
if (login) {
|
||||||
userIdLoginCache.set(userId, normalizeLogin(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);
|
setCachedValue(vodListCache, cacheKey, vods, MAX_VOD_LIST_CACHE_ENTRIES);
|
||||||
return vods;
|
return vods;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (axios.isAxiosError(e) && e.response?.status === 401 && (await ensureTwitchAuth(true))) {
|
if (axios.isAxiosError(e) && e.response?.status === 401 && (await ensureTwitchAuth(true))) {
|
||||||
try {
|
try {
|
||||||
const retryResponse = await fetchVods();
|
const vods = await fetchAllVodPages();
|
||||||
const vods = retryResponse.data.data || [];
|
|
||||||
const login = vods[0]?.user_login;
|
|
||||||
if (login) {
|
|
||||||
userIdLoginCache.set(userId, normalizeLogin(login));
|
|
||||||
}
|
|
||||||
|
|
||||||
setCachedValue(vodListCache, cacheKey, vods, MAX_VOD_LIST_CACHE_ENTRIES);
|
setCachedValue(vodListCache, cacheKey, vods, MAX_VOD_LIST_CACHE_ENTRIES);
|
||||||
return vods;
|
return vods;
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
@ -1906,7 +1939,17 @@ async function cutVideo(
|
|||||||
|
|
||||||
proc.on('close', (code) => {
|
proc.on('close', (code) => {
|
||||||
currentProcess = null;
|
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', () => {
|
proc.on('error', () => {
|
||||||
|
|||||||
@ -805,17 +805,28 @@ async function confirmClipDialog(): Promise<void> {
|
|||||||
|
|
||||||
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||||
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||||
|
const durationSec = endSec - startSec;
|
||||||
const startPartStr = byId<HTMLInputElement>('clipStartPart').value.trim();
|
const startPartStr = byId<HTMLInputElement>('clipStartPart').value.trim();
|
||||||
const startPart = startPartStr ? parseInt(startPartStr, 10) : 1;
|
const startPart = startPartStr ? parseInt(startPartStr, 10) : 1;
|
||||||
const filenameFormat = getSelectedFilenameFormat();
|
const filenameFormat = getSelectedFilenameFormat();
|
||||||
const filenameTemplate = byId<HTMLInputElement>('clipFilenameTemplate').value.trim();
|
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);
|
alert(UI_TEXT.clips.endBeforeStart);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startSec < 0 || endSec > clipTotalSeconds) {
|
if (endSec > clipTotalSeconds) {
|
||||||
alert(UI_TEXT.clips.outOfRange);
|
alert(UI_TEXT.clips.outOfRange);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -833,7 +844,6 @@ async function confirmClipDialog(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const durationSec = endSec - startSec;
|
|
||||||
const customClip: CustomClip = {
|
const customClip: CustomClip = {
|
||||||
startSec,
|
startSec,
|
||||||
durationSec,
|
durationSec,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user