Compare commits
5 Commits
1e81b889f9
...
39fa5065d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39fa5065d2 | ||
|
|
2b379e5e6a | ||
|
|
cf9d7b8334 | ||
|
|
6c47c63fa8 | ||
|
|
4607ba9cc6 |
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.5.2",
|
||||
"version": "4.5.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.5.2",
|
||||
"version": "4.5.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.5.2",
|
||||
"version": "4.5.3",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
30
src/main.ts
30
src/main.ts
@ -685,20 +685,26 @@ function formatDurationDashed(seconds: number): string {
|
||||
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 {
|
||||
if (!fs.existsSync(filePath)) return filePath;
|
||||
const dir = path.dirname(filePath);
|
||||
const ext = path.extname(filePath);
|
||||
const base = path.basename(filePath, ext);
|
||||
let counter = 1;
|
||||
let candidate = filePath;
|
||||
while (fs.existsSync(candidate)) {
|
||||
candidate = path.join(dir, `${base}_${counter}${ext}`);
|
||||
let counter = 0;
|
||||
while (fs.existsSync(candidate) || claimedFilenames.has(candidate)) {
|
||||
counter++;
|
||||
candidate = path.join(dir, `${base}_${counter}${ext}`);
|
||||
}
|
||||
claimedFilenames.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function releaseClaimedFilename(filePath: string): void {
|
||||
claimedFilenames.delete(filePath);
|
||||
}
|
||||
|
||||
function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string {
|
||||
const cleaned = (input || '')
|
||||
.replace(/[<>:"|?*\x00-\x1f]/g, '_')
|
||||
@ -2204,17 +2210,19 @@ function downloadVODPart(
|
||||
if (speed > 0 && downloadedBytes > 0) {
|
||||
const itemStartTime = itemTracking.startTime;
|
||||
const elapsedSec = (Date.now() - itemStartTime) / 1000;
|
||||
if (elapsedSec > 3) {
|
||||
if (elapsedSec > 5) { // Wait at least 5 seconds before showing ETA
|
||||
const avgSpeed = downloadedBytes / elapsedSec;
|
||||
if (expectedDurationSeconds && expectedDurationSeconds > 0) {
|
||||
const estimatedTotalBytes = avgSpeed * expectedDurationSeconds;
|
||||
if (estimatedTotalBytes > downloadedBytes) {
|
||||
const remainingSec = (estimatedTotalBytes - downloadedBytes) / avgSpeed;
|
||||
if (remainingSec > 0 && remainingSec < 86400) { // Between 0 and 24 hours
|
||||
etaStr = formatETA(remainingSec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onProgress({
|
||||
id: itemId,
|
||||
@ -2798,6 +2806,7 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
|
||||
|
||||
item.last_error = '';
|
||||
|
||||
try {
|
||||
let finalResult: DownloadResult = { success: false, error: 'Unbekannter Fehler beim Download' };
|
||||
const maxAttempts = getRetryAttemptLimit();
|
||||
|
||||
@ -2861,8 +2870,6 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
|
||||
|
||||
if (!hasQueueItemId(item.id)) {
|
||||
appendDebugLog('queue-item-finished-removed', { itemId: item.id });
|
||||
activeDownloads.delete(item.id);
|
||||
cancelledItemIds.delete(item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2877,9 +2884,6 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
|
||||
runtimeMetrics.downloadsFailed += 1;
|
||||
}
|
||||
|
||||
activeDownloads.delete(item.id);
|
||||
cancelledItemIds.delete(item.id);
|
||||
|
||||
appendDebugLog('queue-item-finished', {
|
||||
itemId: item.id,
|
||||
status: item.status,
|
||||
@ -2888,6 +2892,12 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
|
||||
|
||||
saveQueue(downloadQueue);
|
||||
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> {
|
||||
|
||||
@ -35,7 +35,7 @@ function getQueueRenderFingerprint(items: QueueItem[]): string {
|
||||
item.mergeGroup?.mergePhase || ''
|
||||
].join(':'));
|
||||
|
||||
return `${lang}|${selectedQueueIds.join(',')}|${pieces.join('|')}`;
|
||||
return `${lang}|${selectedQueueIds.join(',')}|${[...expandedQueueIds].join(',')}|${pieces.join('|')}`;
|
||||
}
|
||||
|
||||
function hasActiveQueueDuplicate(url: string, streamer: string, date: string, customClip?: CustomClip): boolean {
|
||||
@ -199,16 +199,35 @@ function updateQueueItemProgress(progress: DownloadProgress): void {
|
||||
}
|
||||
|
||||
function toggleQueueDetails(id: string): void {
|
||||
const el = document.getElementById(`details-${id}`);
|
||||
if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
if (expandedQueueIds.has(id)) {
|
||||
expandedQueueIds.delete(id);
|
||||
} else {
|
||||
expandedQueueIds.add(id);
|
||||
}
|
||||
renderQueue();
|
||||
}
|
||||
|
||||
function initQueueDragDrop(): void {
|
||||
if (queueDragDropInitialized) return;
|
||||
queueDragDropInitialized = true;
|
||||
|
||||
const list = byId('queueList');
|
||||
|
||||
list.addEventListener('dragstart', (e: DragEvent) => {
|
||||
const el = (e.target as HTMLElement).closest('.queue-item') as HTMLElement;
|
||||
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;
|
||||
el.classList.add('dragging');
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
|
||||
@ -303,7 +322,7 @@ function renderQueue(): void {
|
||||
<div class="queue-progress-bar${progressClass}" style="width: ${progressValue}%;"></div>
|
||||
</div>
|
||||
<div class="queue-progress-text">${safeProgressText}</div>
|
||||
<div class="queue-details" id="details-${item.id}" style="display:none">
|
||||
<div class="queue-details" id="details-${item.id}" style="display:${expandedQueueIds.has(item.id) ? 'block' : 'none'}">
|
||||
<div>URL: ${escapeHtml(item.url)}</div>
|
||||
<div>Streamer: ${escapeHtml(item.streamer)}</div>
|
||||
<div>Dauer: ${escapeHtml(item.duration_str)}</div>
|
||||
|
||||
@ -25,6 +25,8 @@ let isConnected = false;
|
||||
let downloading = false;
|
||||
let queue: QueueItem[] = [];
|
||||
let selectedQueueIds: string[] = [];
|
||||
let expandedQueueIds: Set<string> = new Set();
|
||||
let queueDragDropInitialized = false;
|
||||
|
||||
let cutterFile: string | null = null;
|
||||
let cutterVideoInfo: VideoInfo | null = null;
|
||||
|
||||
@ -99,7 +99,7 @@ async function init(): Promise<void> {
|
||||
|
||||
// Update stats bar
|
||||
updateStatsBar();
|
||||
setInterval(updateStatsBar, 5000);
|
||||
const _statsInterval = setInterval(updateStatsBar, 5000);
|
||||
|
||||
if (config.client_id && config.client_secret) {
|
||||
await connect();
|
||||
@ -275,9 +275,14 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
|
||||
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 {
|
||||
...item,
|
||||
progress: item.progress > 0 ? item.progress : prev.progress,
|
||||
progress: bestProgress,
|
||||
speed: item.speed || prev.speed,
|
||||
eta: item.eta || prev.eta,
|
||||
currentPart: item.currentPart || prev.currentPart,
|
||||
|
||||
@ -358,8 +358,9 @@ body {
|
||||
}
|
||||
|
||||
.queue-selector {
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 3px;
|
||||
border: 2px solid var(--text-secondary);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -367,7 +368,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--bg-primary);
|
||||
user-select: none;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user