-
Notifications
You must be signed in to change notification settings - Fork 754
Open
Description
Issue Title
Add Batch URL Import - Import multiple URLs from file or text area
Summary
Currently, ytDownloader only supports downloading one video at a time by pasting a single URL from the clipboard. Users who want to download multiple videos must paste each URL individually, which is time-consuming.
This feature would allow users to:
- Paste multiple URLs at once (one per line) via a dedicated text area
- Import URLs from a
.txtfile containing a list of video links - Queue all URLs for sequential downloading
Use Case
- Users who have a list of videos to download (e.g., from a course, tutorial series, or curated collection)
- Users who save video links in a text file for later downloading
- Power users who want to batch process downloads without manual intervention
Proposed Implementation
- UI Changes (html/index.html)
Add a new "Batch Import" button next to the paste button, and a modal dialog for batch input:
<!-- Add next to existing pasteUrl button -->
<button id="batchImportBtn" class="blueBtn" style="margin-left: 10px;">
<span data-translate="batchImport">Batch Import</span>
</button>
<!-- Batch Import Modal -->
<div id="batchModal" class="modal">
<div class="modal-content">
<img src="../assets/images/close.png" alt="close" id="closeBatchModal" class="modal-close">
<h2 data-translate="batchImportTitle">Import Multiple URLs</h2>
<div class="batch-options">
<textarea id="batchUrlInput"
placeholder="Paste URLs here (one per line) https://youtube.com/watch?v=... https://youtube.com/watch?v=..."
rows="8"></textarea>
<div class="batch-divider">
<span data-translate="or">OR</span>
</div>
<button id="importFromFile" class="blueBtn">
<span data-translate="importFromFile">Import from File (.txt)</span>
</button>
<p id="importedFileInfo"></p>
</div>
<div class="batch-footer">
<span id="urlCount">0 URLs detected</span>
<button id="startBatchDownload" class="submitBtn" data-translate="addToQueue">Add to Queue</button>
</div>
</div>
</div>
2. CSS Styles (assets/css/index.css)
/* Batch Import Modal */
.modal {
display: none;
position: fixed;
z-index: 10;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(17, 25, 40, 0.85);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
background-color: var(--box-main);
margin: 10% auto;
padding: 25px;
border-radius: 16px;
width: 500px;
max-width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.modal-close {
position: absolute;
top: 15px;
right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
#batchUrlInput {
width: 100%;
padding: 12px;
border: 2px solid var(--box-separation);
border-radius: 8px;
background-color: var(--box-toggle);
color: var(--text);
font-family: "JetBrains", monospace;
font-size: 14px;
resize: vertical;
min-height: 150px;
}
#batchUrlInput:focus {
border-color: var(--blueBtn);
outline: none;
}
.batch-divider {
display: flex;
align-items: center;
margin: 20px 0;
color: var(--text);
opacity: 0.6;
}
.batch-divider::before,
.batch-divider::after {
content: "";
flex: 1;
height: 1px;
background-color: var(--box-separation);
}
.batch-divider span {
padding: 0 15px;
}
.batch-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--box-separation);
}
#urlCount {
color: var(--text);
opacity: 0.8;
}
3. JavaScript Logic (src/renderer.js)
// Add to CONSTANTS.DOM_IDS
BATCH_IMPORT_BTN: "batchImportBtn",
BATCH_MODAL: "batchModal",
CLOSE_BATCH_MODAL: "closeBatchModal",
BATCH_URL_INPUT: "batchUrlInput",
IMPORT_FROM_FILE: "importFromFile",
START_BATCH_DOWNLOAD: "startBatchDownload",
URL_COUNT: "urlCount",
IMPORTED_FILE_INFO: "importedFileInfo",
// Add to _addEventListeners()
$(CONSTANTS.DOM_IDS.BATCH_IMPORT_BTN).addEventListener("click", () =>
this._openBatchModal()
);
$(CONSTANTS.DOM_IDS.CLOSE_BATCH_MODAL).addEventListener("click", () =>
this._closeBatchModal()
);
$(CONSTANTS.DOM_IDS.BATCH_URL_INPUT).addEventListener("input", () =>
this._updateUrlCount()
);
$(CONSTANTS.DOM_IDS.IMPORT_FROM_FILE).addEventListener("click", () =>
this._importUrlsFromFile()
);
$(CONSTANTS.DOM_IDS.START_BATCH_DOWNLOAD).addEventListener("click", () =>
this._processBatchUrls()
);
// New methods
_openBatchModal() {
$(CONSTANTS.DOM_IDS.BATCH_MODAL).style.display = "block";
$(CONSTANTS.DOM_IDS.BATCH_URL_INPUT).value = "";
this._updateUrlCount();
}
_closeBatchModal() {
$(CONSTANTS.DOM_IDS.BATCH_MODAL).style.display = "none";
}
_parseUrls(text) {
return text
.split(/[\n\r]+/)
.map(line => line.trim())
.filter(line => {
// Basic URL validation
try {
new URL(line);
return true;
} catch {
return false;
}
});
}
_updateUrlCount() {
const urls = this._parseUrls($(CONSTANTS.DOM_IDS.BATCH_URL_INPUT).value);
const count = urls.length;
$(CONSTANTS.DOM_IDS.URL_COUNT).textContent =
`${count} ${count === 1 ? 'URL' : 'URLs'} ${i18n.__("detected") || "detected"}`;
}
async _importUrlsFromFile() {
ipcRenderer.send("select-text-file");
}
// Add IPC listener in _addEventListeners()
ipcRenderer.on("text-file-selected", (event, result) => {
if (result.success) {
$(CONSTANTS.DOM_IDS.BATCH_URL_INPUT).value = result.content;
$(CONSTANTS.DOM_IDS.IMPORTED_FILE_INFO).textContent =
`Imported: ${result.filename}`;
this._updateUrlCount();
}
});
async _processBatchUrls() {
const urls = this._parseUrls($(CONSTANTS.DOM_IDS.BATCH_URL_INPUT).value);
if (urls.length === 0) {
this._showPopup(i18n.__("noValidUrls") || "No valid URLs found", true);
return;
}
this._closeBatchModal();
this._showPopup(`${urls.length} ${i18n.__("urlsAddedToQueue") || "URLs added to queue"}`, false);
// Process each URL sequentially
for (const url of urls) {
// Add a small delay between fetching info to avoid rate limiting
await this._addUrlToQueue(url);
await new Promise(resolve => setTimeout(resolve, 500));
}
}
async _addUrlToQueue(url) {
try {
const metadata = await this._fetchVideoMetadata(url);
// Create a download job with default settings
const downloadJob = {
type: "video", // Default to video, could be made configurable
url: url,
title: metadata.title,
thumbnail: metadata.thumbnail,
options: {...this.state.downloadOptions},
uiSnapshot: {
videoFormat: this._getBestVideoFormat(metadata.formats),
audioForVideoFormat: this._getBestAudioFormat(metadata.formats),
audioFormat: "",
extractFormat: "mp3",
extractQuality: "5",
},
};
if (this.state.currentDownloads < this.state.maxActiveDownloads) {
this._startDownload(downloadJob);
} else {
this._queueDownload(downloadJob);
}
} catch (error) {
console.error(`Failed to fetch info for ${url}:`, error);
// Optionally show a notification for failed URLs
}
}
_getBestVideoFormat(formats) {
// Return best format matching user preferences
const preferredQuality = this.state.preferences.videoQuality;
const videoFormats = formats.filter(f => f.vcodec !== "none" && f.height);
const sorted = videoFormats.sort((a, b) => (b.height || 0) - (a.height || 0));
const best = sorted.find(f => f.height <= preferredQuality) || sorted[0];
return best ? `${best.format_id}|${best.ext}|${best.height}|${best.vcodec}` : "";
}
_getBestAudioFormat(formats) {
const audioFormats = formats.filter(f => f.acodec !== "none" && f.vcodec === "none");
const best = audioFormats.sort((a, b) => (b.abr || 0) - (a.abr || 0))[0];
return best ? `${best.format_id}|${best.ext}` : "none|none";
}
4. Main Process Handler (main.js)
// Add file dialog handler for text files
ipcMain.on("select-text-file", (event) => {
dialog.showOpenDialog({
properties: ["openFile"],
filters: [
{ name: "Text Files", extensions: ["txt"] },
{ name: "All Files", extensions: ["*"] }
]
}).then(result => {
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0];
const content = fs.readFileSync(filePath, "utf-8");
const filename = path.basename(filePath);
event.sender.send("text-file-selected", {
success: true,
content,
filename
});
}
}).catch(err => {
console.error("Error reading file:", err);
event.sender.send("text-file-selected", { success: false });
});
});
5. Add Translation Keys (translations/en.json)
{
"batchImport": "Batch Import",
"batchImportTitle": "Import Multiple URLs",
"or": "OR",
"importFromFile": "Import from File (.txt)",
"addToQueue": "Add to Queue",
"detected": "detected",
"noValidUrls": "No valid URLs found",
"urlsAddedToQueue": "URLs added to queue"
}
Benefits
- Time-saving - Download dozens of videos with one action
- Workflow integration - Import from curated lists
- Power user friendly - Works with existing queue system
- Non-disruptive - Existing single-URL workflow remains unchanged
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels