feat: add pause, resume, and stop download IPC handlers and implement multilingual UI support

This commit is contained in:
Ümit Tunç
2026-05-28 07:51:41 +03:00
parent 0bc3a15b82
commit da042b5749
8 changed files with 316 additions and 73 deletions
+15
View File
@@ -93,6 +93,21 @@ ipcMain.handle('download-media', async (event, { items, browser }) => {
} }
}); });
// IPC Handler: Pause Download
ipcMain.handle('pause-download', () => {
return downloader.pauseDownload();
});
// IPC Handler: Resume Download
ipcMain.handle('resume-download', () => {
return downloader.resumeDownload();
});
// IPC Handler: Stop Download
ipcMain.handle('stop-download', () => {
return downloader.stopDownload();
});
// IPC Handler: Open Path in File Explorer // IPC Handler: Open Path in File Explorer
ipcMain.handle('open-path', async (event, folderPath) => { ipcMain.handle('open-path', async (event, folderPath) => {
try { try {
+16
View File
@@ -246,6 +246,22 @@
<p id="download-subtext" class="subtext" style="margin-top: 0; font-size: 0.7rem;">Lütfen bekleyin...</p> <p id="download-subtext" class="subtext" style="margin-top: 0; font-size: 0.7rem;">Lütfen bekleyin...</p>
</div> </div>
</div> </div>
<!-- Control Buttons (Pause / Resume / Stop) -->
<div class="download-controls-row" style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
<button id="dl-pause-btn" class="dl-ctrl-btn" style="background: rgba(249, 203, 40, 0.12); border: 1px solid rgba(249, 203, 40, 0.25); color: #f9cb28; font-size: 0.72rem; padding: 4px 10px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 4px; font-weight: 600;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width: 11px; height: 11px;"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>
<span data-i18n="btn_pause">Duraklat</span>
</button>
<button id="dl-resume-btn" class="dl-ctrl-btn" style="background: rgba(0, 255, 135, 0.12); border: 1px solid rgba(0, 255, 135, 0.25); color: #00ff87; font-size: 0.72rem; padding: 4px 10px; border-radius: 6px; cursor: pointer; display: none; align-items: center; gap: 4px; font-weight: 600;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width: 11px; height: 11px;"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
<span data-i18n="btn_resume">Devam Et</span>
</button>
<button id="dl-stop-btn" class="dl-ctrl-btn" style="background: rgba(255, 77, 77, 0.12); border: 1px solid rgba(255, 77, 77, 0.25); color: #ff4d4d; font-size: 0.72rem; padding: 4px 10px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 4px; font-weight: 600;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width: 11px; height: 11px;"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect></svg>
<span data-i18n="btn_stop">Durdur</span>
</button>
</div>
</div> </div>
<button id="download-btn" class="glow-button full-width"> <button id="download-btn" class="glow-button full-width">
+4 -1
View File
@@ -71,5 +71,8 @@
"min_suffix_short": "د", "min_suffix_short": "د",
"sec_suffix_short": "ث", "sec_suffix_short": "ث",
"hr_suffix_short": "س", "hr_suffix_short": "س",
"overall_status_label": "التقدم الإجمالي" "overall_status_label": "التقدم الإجمالي",
"btn_pause": "إيقاف مؤقت",
"btn_resume": "استئناف",
"btn_stop": "إيقاف"
} }
+4 -1
View File
@@ -71,5 +71,8 @@
"min_suffix_short": "Min.", "min_suffix_short": "Min.",
"sec_suffix_short": "Sek.", "sec_suffix_short": "Sek.",
"hr_suffix_short": "Std.", "hr_suffix_short": "Std.",
"overall_status_label": "Gesamtfortschritt" "overall_status_label": "Gesamtfortschritt",
"btn_pause": "Pause",
"btn_resume": "Fortsetzen",
"btn_stop": "Stoppen"
} }
+4 -1
View File
@@ -71,5 +71,8 @@
"min_suffix_short": "min", "min_suffix_short": "min",
"sec_suffix_short": "sec", "sec_suffix_short": "sec",
"hr_suffix_short": "hr", "hr_suffix_short": "hr",
"overall_status_label": "Overall Progress" "overall_status_label": "Overall Progress",
"btn_pause": "Pause",
"btn_resume": "Resume",
"btn_stop": "Stop"
} }
+4 -1
View File
@@ -71,5 +71,8 @@
"min_suffix_short": "мин.", "min_suffix_short": "мин.",
"sec_suffix_short": "сек.", "sec_suffix_short": "сек.",
"hr_suffix_short": "ч.", "hr_suffix_short": "ч.",
"overall_status_label": "Общий прогресс" "overall_status_label": "Общий прогресс",
"btn_pause": "Пауза",
"btn_resume": "Продолжить",
"btn_stop": "Остановить"
} }
+4 -1
View File
@@ -71,5 +71,8 @@
"min_suffix_short": "dk", "min_suffix_short": "dk",
"sec_suffix_short": "sn", "sec_suffix_short": "sn",
"hr_suffix_short": "sa", "hr_suffix_short": "sa",
"overall_status_label": "Genel İlerleme" "overall_status_label": "Genel İlerleme",
"btn_pause": "Duraklat",
"btn_resume": "Devam Et",
"btn_stop": "Durdur"
} }
+240 -43
View File
@@ -331,89 +331,286 @@ function downloadSingle(url, format, quality, outputDir, browser, onProgress) {
}); });
} }
// Download list of items sequentially // Module-level Queue State variables
async function downloadMedia(items, outputDir, browser, onProgress) { let currentProcess = null;
console.log(`[DEBUG] Media download requested for ${items.length} items.`); let isPaused = false;
const results = []; let isStopped = false;
let currentItemIndex = 0;
let downloadQueue = [];
let queueOutputDir = '';
let queueBrowser = null;
let queueOnProgressCallback = null;
let queueResults = [];
let queueStartTime = 0;
let activeResolve = null;
let activeReject = null;
function getQueueStats() {
const durationSec = ((Date.now() - queueStartTime) / 1000).toFixed(1);
const completedCount = queueResults.filter(r => r.success).length;
return {
totalItems: downloadQueue.length,
completed: completedCount,
duration: `${durationSec} saniye`,
outputDir: queueOutputDir,
results: queueResults
};
}
function downloadSingleItemWithProcessRef(item) {
return new Promise((resolve, reject) => {
let args = [];
const outputTemplate = path.join(queueOutputDir, '%(title)s.%(ext)s');
const startTime = Date.now(); const startTime = Date.now();
let completedCount = 0; let totalSize = 'Bilinmiyor';
for (let i = 0; i < items.length; i++) { if (item.format === 'mp3') {
const item = items[i]; const q = item.quality ? `${item.quality}K` : '320K';
console.log(`[DEBUG] Queue processing [${i+1}/${items.length}]: "${item.title}"`); args = [
'--no-playlist',
'--ffmpeg-location', FFMPEG_PATH,
'-x',
'--audio-format', 'mp3',
'--audio-quality', q,
'-o', outputTemplate
];
} else {
const res = item.quality || '1080';
args = [
'--no-playlist',
'--ffmpeg-location', FFMPEG_PATH,
'-f', `bv*[height<=${res}][ext=mp4]+ba[ext=m4a]/b[height<=${res}][ext=mp4]/bv*[height<=${res}]+ba/b[height<=${res}]/bestvideo+bestaudio/best`,
'-o', outputTemplate
];
}
// Notify start of this item if (queueBrowser) {
onProgress({ args.push('--cookies-from-browser', queueBrowser);
}
args.push(item.url);
console.log(`[DEBUG] Spawning single downloader process: ${YTDLP_PATH} ${args.join(' ')}`);
currentProcess = spawn(YTDLP_PATH, args);
currentProcess.stdout.on('data', (data) => {
const line = data.toString();
const sizeMatch = line.match(/of\s+(\d+(?:\.\d+)?\s*[a-zA-Z]+)/);
if (sizeMatch) {
totalSize = sizeMatch[1];
}
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) {
const percent = parseFloat(match[1]);
queueOnProgressCallback({
itemId: item.id, itemId: item.id,
index: i, index: currentItemIndex,
total: items.length, total: downloadQueue.length,
percent: percent,
status: `İndiriliyor...`,
title: item.title
});
} else if (line.includes('[ExtractAudio]') || line.includes('[Merger]')) {
queueOnProgressCallback({
itemId: item.id,
index: currentItemIndex,
total: downloadQueue.length,
percent: 95,
status: `İşleniyor/Dönüştürülüyor...`,
title: item.title
});
}
});
currentProcess.stderr.on('data', (data) => {
console.warn(`[DEBUG] [yt-dlp stderr]:`, data.toString());
});
currentProcess.on('close', (code) => {
console.log(`[DEBUG] Single downloader process exited with code ${code}`);
currentProcess = null;
if (code === 0) {
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
resolve({
size: totalSize,
duration: `${durationSec} saniye`
});
} else {
reject(new Error(`Download exited with code ${code}`));
}
});
});
}
async function processQueue() {
if (isStopped) {
activeResolve({
success: false,
stopped: true,
stats: getQueueStats()
});
return;
}
if (isPaused) {
queueOnProgressCallback({
itemId: downloadQueue[currentItemIndex].id,
index: currentItemIndex,
total: downloadQueue.length,
percent: 0,
status: `Duraklatıldı`,
title: downloadQueue[currentItemIndex].title
});
return;
}
if (currentItemIndex >= downloadQueue.length) {
activeResolve({
success: true,
stats: getQueueStats()
});
return;
}
const item = downloadQueue[currentItemIndex];
console.log(`[DEBUG] Queue processing [${currentItemIndex+1}/${downloadQueue.length}]: "${item.title}"`);
queueOnProgressCallback({
itemId: item.id,
index: currentItemIndex,
total: downloadQueue.length,
percent: 0, percent: 0,
status: `İndiriliyor...`, status: `İndiriliyor...`,
title: item.title title: item.title
}); });
try { try {
const result = await downloadSingle(item.url, item.format, item.quality, outputDir, browser, (percent, status) => { const result = await downloadSingleItemWithProcessRef(item);
onProgress({
itemId: item.id,
index: i,
total: items.length,
percent: percent,
status: status,
title: item.title
});
});
completedCount++; queueResults.push({
results.push({
success: true, success: true,
item, item,
size: result.size, size: result.size,
duration: result.duration duration: result.duration
}); });
// Notify completion of this item queueOnProgressCallback({
onProgress({
itemId: item.id, itemId: item.id,
index: i, index: currentItemIndex,
total: items.length, total: downloadQueue.length,
percent: 100, percent: 100,
status: `Tamamlandı`, status: `Tamamlandı`,
title: item.title title: item.title
}); });
currentItemIndex++;
processQueue(); // Process next item
} catch (error) { } catch (error) {
if (isStopped) {
console.log('[DEBUG] Process aborted by user stop command.');
activeResolve({
success: false,
stopped: true,
stats: getQueueStats()
});
return;
}
if (isPaused) {
console.log('[DEBUG] Process suspended by user pause command.');
return;
}
console.error(`[DEBUG] Queue error on item "${item.title}":`, error); console.error(`[DEBUG] Queue error on item "${item.title}":`, error);
results.push({ queueResults.push({
success: false, success: false,
item, item,
error: error.message error: error.message
}); });
onProgress({ queueOnProgressCallback({
itemId: item.id, itemId: item.id,
index: i, index: currentItemIndex,
total: items.length, total: downloadQueue.length,
percent: 0, percent: 0,
status: `Hata`, status: `Hata`,
title: item.title, title: item.title,
failed: true failed: true
}); });
}
}
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1); currentItemIndex++;
return { processQueue(); // Continue queue on item error
totalItems: items.length, }
completed: completedCount, }
duration: `${durationSec} saniye`,
outputDir: outputDir, // Download list of items sequentially
results: results async function downloadMedia(items, outputDir, browser, onProgress) {
}; return new Promise((resolve, reject) => {
console.log(`[DEBUG] Media download requested for ${items.length} items.`);
downloadQueue = items;
queueOutputDir = outputDir;
queueBrowser = browser;
queueOnProgressCallback = onProgress;
isPaused = false;
isStopped = false;
currentItemIndex = 0;
queueResults = [];
queueStartTime = Date.now();
activeResolve = resolve;
activeReject = reject;
processQueue();
});
}
function pauseDownload() {
console.log('[DEBUG] Pause request received.');
isPaused = true;
if (currentProcess) {
try {
currentProcess.kill('SIGINT');
} catch (e) {
console.error('[DEBUG] Failed to interrupt process on pause:', e);
}
}
return { success: true };
}
function resumeDownload() {
console.log('[DEBUG] Resume request received.');
if (!isPaused) return { success: false, error: 'Not paused' };
isPaused = false;
processQueue();
return { success: true };
}
function stopDownload() {
console.log('[DEBUG] Stop request received.');
isStopped = true;
if (currentProcess) {
try {
currentProcess.kill('SIGINT');
} catch (e) {
console.error('[DEBUG] Failed to interrupt process on stop:', e);
}
} else if (activeResolve) {
activeResolve({
success: false,
stopped: true,
stats: getQueueStats()
});
}
return { success: true };
} }
module.exports = { module.exports = {
checkDependencies, checkDependencies,
getVideoInfo, getVideoInfo,
downloadMedia downloadMedia,
pauseDownload,
resumeDownload,
stopDownload
}; };