Compare commits

..

No commits in common. "39fa5065d23348b55b8aec51aa644d05e50dda79" and "1e81b889f9a4a35ba7f1419dbf7a4a5a76f05b78" have entirely different histories.

7 changed files with 92 additions and 129 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.5.3", "version": "4.5.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.5.3", "version": "4.5.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.5.3", "version": "4.5.2",
"description": "Twitch VOD Manager - Download Twitch VODs easily", "description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js", "main": "dist/main.js",
"author": "xRangerDE", "author": "xRangerDE",

View File

@ -685,26 +685,20 @@ function formatDurationDashed(seconds: number): string {
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`; return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
} }
const claimedFilenames = new Set<string>();
function ensureUniqueFilename(filePath: string): string { function ensureUniqueFilename(filePath: string): string {
if (!fs.existsSync(filePath)) return filePath;
const dir = path.dirname(filePath); const dir = path.dirname(filePath);
const ext = path.extname(filePath); const ext = path.extname(filePath);
const base = path.basename(filePath, ext); const base = path.basename(filePath, ext);
let counter = 1;
let candidate = filePath; let candidate = filePath;
let counter = 0; while (fs.existsSync(candidate)) {
while (fs.existsSync(candidate) || claimedFilenames.has(candidate)) {
counter++;
candidate = path.join(dir, `${base}_${counter}${ext}`); candidate = path.join(dir, `${base}_${counter}${ext}`);
counter++;
} }
claimedFilenames.add(candidate);
return candidate; return candidate;
} }
function releaseClaimedFilename(filePath: string): void {
claimedFilenames.delete(filePath);
}
function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string { function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string {
const cleaned = (input || '') const cleaned = (input || '')
.replace(/[<>:"|?*\x00-\x1f]/g, '_') .replace(/[<>:"|?*\x00-\x1f]/g, '_')
@ -2210,19 +2204,17 @@ function downloadVODPart(
if (speed > 0 && downloadedBytes > 0) { if (speed > 0 && downloadedBytes > 0) {
const itemStartTime = itemTracking.startTime; const itemStartTime = itemTracking.startTime;
const elapsedSec = (Date.now() - itemStartTime) / 1000; const elapsedSec = (Date.now() - itemStartTime) / 1000;
if (elapsedSec > 5) { // Wait at least 5 seconds before showing ETA if (elapsedSec > 3) {
const avgSpeed = downloadedBytes / elapsedSec; const avgSpeed = downloadedBytes / elapsedSec;
if (expectedDurationSeconds && expectedDurationSeconds > 0) { if (expectedDurationSeconds && expectedDurationSeconds > 0) {
const estimatedTotalBytes = avgSpeed * expectedDurationSeconds; const estimatedTotalBytes = avgSpeed * expectedDurationSeconds;
if (estimatedTotalBytes > downloadedBytes) { if (estimatedTotalBytes > downloadedBytes) {
const remainingSec = (estimatedTotalBytes - downloadedBytes) / avgSpeed; const remainingSec = (estimatedTotalBytes - downloadedBytes) / avgSpeed;
if (remainingSec > 0 && remainingSec < 86400) { // Between 0 and 24 hours
etaStr = formatETA(remainingSec); etaStr = formatETA(remainingSec);
} }
} }
} }
} }
}
onProgress({ onProgress({
id: itemId, id: itemId,
@ -2806,7 +2798,6 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
item.last_error = ''; item.last_error = '';
try {
let finalResult: DownloadResult = { success: false, error: 'Unbekannter Fehler beim Download' }; let finalResult: DownloadResult = { success: false, error: 'Unbekannter Fehler beim Download' };
const maxAttempts = getRetryAttemptLimit(); const maxAttempts = getRetryAttemptLimit();
@ -2870,6 +2861,8 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
if (!hasQueueItemId(item.id)) { if (!hasQueueItemId(item.id)) {
appendDebugLog('queue-item-finished-removed', { itemId: item.id }); appendDebugLog('queue-item-finished-removed', { itemId: item.id });
activeDownloads.delete(item.id);
cancelledItemIds.delete(item.id);
return; return;
} }
@ -2884,6 +2877,9 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
runtimeMetrics.downloadsFailed += 1; runtimeMetrics.downloadsFailed += 1;
} }
activeDownloads.delete(item.id);
cancelledItemIds.delete(item.id);
appendDebugLog('queue-item-finished', { appendDebugLog('queue-item-finished', {
itemId: item.id, itemId: item.id,
status: item.status, status: item.status,
@ -2892,12 +2888,6 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
saveQueue(downloadQueue); saveQueue(downloadQueue);
emitQueueUpdated(); emitQueueUpdated();
} finally {
activeDownloads.delete(item.id);
cancelledItemIds.delete(item.id);
// Release any filenames claimed during this download (prevents stale claims blocking re-downloads)
claimedFilenames.clear();
}
} }
async function processQueue(): Promise<void> { async function processQueue(): Promise<void> {

View File

@ -35,7 +35,7 @@ function getQueueRenderFingerprint(items: QueueItem[]): string {
item.mergeGroup?.mergePhase || '' item.mergeGroup?.mergePhase || ''
].join(':')); ].join(':'));
return `${lang}|${selectedQueueIds.join(',')}|${[...expandedQueueIds].join(',')}|${pieces.join('|')}`; return `${lang}|${selectedQueueIds.join(',')}|${pieces.join('|')}`;
} }
function hasActiveQueueDuplicate(url: string, streamer: string, date: string, customClip?: CustomClip): boolean { function hasActiveQueueDuplicate(url: string, streamer: string, date: string, customClip?: CustomClip): boolean {
@ -199,35 +199,16 @@ function updateQueueItemProgress(progress: DownloadProgress): void {
} }
function toggleQueueDetails(id: string): void { function toggleQueueDetails(id: string): void {
if (expandedQueueIds.has(id)) { const el = document.getElementById(`details-${id}`);
expandedQueueIds.delete(id); if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
} else {
expandedQueueIds.add(id);
}
renderQueue();
} }
function initQueueDragDrop(): void { function initQueueDragDrop(): void {
if (queueDragDropInitialized) return;
queueDragDropInitialized = true;
const list = byId('queueList'); const list = byId('queueList');
list.addEventListener('dragstart', (e: DragEvent) => { list.addEventListener('dragstart', (e: DragEvent) => {
const el = (e.target as HTMLElement).closest('.queue-item') as HTMLElement; const el = (e.target as HTMLElement).closest('.queue-item') as HTMLElement;
if (!el) return; if (!el) return;
// Prevent dragging items that are no longer pending (race window between status change and re-render)
const itemId = el.dataset.id;
if (itemId) {
const item = queue.find(i => i.id === itemId);
if (!item || item.status !== 'pending') {
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'none';
e.dataTransfer.clearData();
}
return;
}
}
draggedQueueItemId = el.dataset.id || null; draggedQueueItemId = el.dataset.id || null;
el.classList.add('dragging'); el.classList.add('dragging');
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'; if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
@ -322,7 +303,7 @@ function renderQueue(): void {
<div class="queue-progress-bar${progressClass}" style="width: ${progressValue}%;"></div> <div class="queue-progress-bar${progressClass}" style="width: ${progressValue}%;"></div>
</div> </div>
<div class="queue-progress-text">${safeProgressText}</div> <div class="queue-progress-text">${safeProgressText}</div>
<div class="queue-details" id="details-${item.id}" style="display:${expandedQueueIds.has(item.id) ? 'block' : 'none'}"> <div class="queue-details" id="details-${item.id}" style="display:none">
<div>URL: ${escapeHtml(item.url)}</div> <div>URL: ${escapeHtml(item.url)}</div>
<div>Streamer: ${escapeHtml(item.streamer)}</div> <div>Streamer: ${escapeHtml(item.streamer)}</div>
<div>Dauer: ${escapeHtml(item.duration_str)}</div> <div>Dauer: ${escapeHtml(item.duration_str)}</div>

View File

@ -25,8 +25,6 @@ let isConnected = false;
let downloading = false; let downloading = false;
let queue: QueueItem[] = []; let queue: QueueItem[] = [];
let selectedQueueIds: string[] = []; let selectedQueueIds: string[] = [];
let expandedQueueIds: Set<string> = new Set();
let queueDragDropInitialized = false;
let cutterFile: string | null = null; let cutterFile: string | null = null;
let cutterVideoInfo: VideoInfo | null = null; let cutterVideoInfo: VideoInfo | null = null;

View File

@ -99,7 +99,7 @@ async function init(): Promise<void> {
// Update stats bar // Update stats bar
updateStatsBar(); updateStatsBar();
const _statsInterval = setInterval(updateStatsBar, 5000); setInterval(updateStatsBar, 5000);
if (config.client_id && config.client_secret) { if (config.client_id && config.client_secret) {
await connect(); await connect();
@ -275,14 +275,9 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
return item; return item;
} }
// Keep the higher progress value to prevent backward jumps from stale data
const bestProgress = (prev.status === 'downloading' && prev.progress > item.progress)
? prev.progress
: (item.progress > 0 ? item.progress : prev.progress);
return { return {
...item, ...item,
progress: bestProgress, progress: item.progress > 0 ? item.progress : prev.progress,
speed: item.speed || prev.speed, speed: item.speed || prev.speed,
eta: item.eta || prev.eta, eta: item.eta || prev.eta,
currentPart: item.currentPart || prev.currentPart, currentPart: item.currentPart || prev.currentPart,

View File

@ -358,9 +358,8 @@ body {
} }
.queue-selector { .queue-selector {
min-width: 22px; width: 22px;
height: 22px; height: 22px;
padding: 0 3px;
border: 2px solid var(--text-secondary); border: 2px solid var(--text-secondary);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@ -368,7 +367,7 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 11px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: var(--bg-primary); color: var(--bg-primary);
user-select: none; user-select: none;