Skip to content

Instantly share code, notes, and snippets.

@vothanhkiet
Forked from ptanov/gopro-media-download.js
Created December 24, 2025 01:51
Show Gist options
  • Select an option

  • Save vothanhkiet/64d334d8fa551eeef62773e0c56cab69 to your computer and use it in GitHub Desktop.

Select an option

Save vothanhkiet/64d334d8fa551eeef62773e0c56cab69 to your computer and use it in GitHub Desktop.
console.error("WARNING: you must check browser download status for failed downloads!");
// time to wait before starting download in ms, you can change it during the download and changes will take effect
var TIMEOUT_WAIT_DOWNLOAD_VIDEO = 2 * 60 * 1000;
var TIMEOUT_WAIT_DOWNLOAD_PHOTO = 10 * 1000;
// if you want to download only selected files change it to const DOWNLOAD_ONLY = ["GX010709.MP4", "GX010710.MP4"];
const DOWNLOAD_ONLY = [];
// time to wait for popup initialization (load available download options), increase if it takes more
var TIMEOUT_WAIT_POPUP = 3000;
(function() {
var stats = null;
var stopped = false;
function click(item) {
const event = document.createEvent ('MouseEvents');
event.initEvent ("click", true, true);
item.dispatchEvent(event);
}
function xpath(context, query) {
return document.evaluate(query, context, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
function startDownload(item) {
// open menu
click(xpath(item.parentElement, "div[contains(@class, 'dropdown')]/button"));
setTimeout(function() {
// find download button, wait for it if necessary
if (stopped) {
console.log(new Date().toISOString(), "Explicitly stopped");
return;
}
const fullLength = xpath(item.parentElement, "div[@class='dropdown open']//a[contains(@id,' Original quality - Full length')]");
const originalQuality = xpath(item.parentElement, "div[@class='dropdown open']//a[contains(@id, 'Original quality')]");
const singleMedia = xpath(item.parentElement, "div[@class='dropdown open']//li[@class='download-menu-item']/a[contains(@id, 'download-single-media')]");
const download = fullLength || originalQuality || singleMedia;
if (download) {
click(download);
// deselect video:
click(item);
// continue with the rest of the videos:
setTimeout(() => startAll(), isPhoto(getFilename(item)) ? TIMEOUT_WAIT_DOWNLOAD_PHOTO : TIMEOUT_WAIT_DOWNLOAD_VIDEO);
return;
}
console.log(new Date().toISOString(), "Can't download ", getFilename(item), item, ", retrying");
startDownload(item);
}, TIMEOUT_WAIT_POPUP);
}
function calculateExpected(current) {
const expected = current.photo * (TIMEOUT_WAIT_DOWNLOAD_PHOTO + TIMEOUT_WAIT_POPUP) + (current.edit + current.video) * (TIMEOUT_WAIT_DOWNLOAD_VIDEO + TIMEOUT_WAIT_POPUP);
if (expected < 1000) {
return "1 second";
}
if (expected < 60 * 1000) {
return `${expected / 1000} seconds`;
}
if (expected < 60 * 60 * 1000) {
return `${(expected / (60 * 1000)).toFixed(2)} minutes`;
}
return `${(expected / (60 * 60 * 1000)).toFixed(2)} hours`;
}
function isPhoto(name) {
return name.toUpperCase().endsWith(".JPG");
}
function showEstimates() {
const filenames = videos.map(a => getFilename(a));
const all = filenames.length;
const photo = filenames.filter(a => isPhoto(a)).length;
const edit = filenames.filter(a => !a).length;
const current = {
all: all,
photo: photo,
edit: edit,
video: all - photo - edit,
};
if (!stats) {
stats = current;
}
const completed = {
all: stats.all - current.all,
photo: stats.photo - current.photo,
edit: stats.edit - current.edit,
video: stats.video - current.video,
};
const expected = calculateExpected(current);
console.log(new Date().toISOString(), `More than ${expected} expected, completed: ${completed.all}/${stats.all}, photo: ${completed.photo}/${stats.photo}, edit: ${completed.edit}/${stats.edit}, video: ${completed.video}/${stats.video}, you can adjust variables TIMEOUT_WAIT_DOWNLOAD_VIDEO and TIMEOUT_WAIT_DOWNLOAD_PHOTO while script is running`);
}
function startAll() {
showEstimates();
item = videos.shift();
if (!item) {
console.log(new Date().toISOString(), "DONE");
stopPrevious();
return;
}
console.log(new Date().toISOString(), "Starting", getFilename(item), item);
startDownload(item);
}
function stopPrevious() {
const stopButton = xpath(document, "//button[@id='stopButton']");
if (stopButton) {
click(stopButton);
}
}
function getFilename(item) {
const label = xpath(item.parentElement.parentElement, "div[@class='filename-overlay']");
if (!label) {
// it is edit
return "";
}
return label.textContent;
}
function videosToDownload() {
const selected = [];
const query = document.evaluate("//button[@class='collection-item-menu-button select-button show-button selected']", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0, length = query.snapshotLength; i < length; ++i) {
selected.push(query.snapshotItem(i));
}
if (DOWNLOAD_ONLY.length === 0) {
return selected;
}
if (new Set(DOWNLOAD_ONLY).size !== DOWNLOAD_ONLY.length) {
throw new Error("Duplicates in DOWNLOAD_ONLY");
}
const result = selected.filter(a => DOWNLOAD_ONLY.indexOf(getFilename(a)) !== -1);
const filenames = result.map(a => getFilename(a));
DOWNLOAD_ONLY.filter(a => filenames.indexOf(a) === -1).forEach(a => console.error(`Can't find ${a}`));
DOWNLOAD_ONLY.filter(a => filenames.filter(b => b === a).length > 1).forEach(a => console.error(`More than once (${filenames.filter(b => b === a).length}): ${a}`));
return result;
}
stopPrevious();
const videos = videosToDownload();
console.log(new Date().toISOString(), "Going to download: ", videos, videos.map(a => getFilename(a)));
const stopButton = document.createElement("button");
stopButton.innerHTML = "------~~~~~~[[[[[[ STOP DOWNLOADS ]]]]]]~~~~~~------";
stopButton.id = "stopButton";
stopButton.style.position = "fixed";
stopButton.style.top = 0;
stopButton.style.zIndex = 1016;
stopButton.addEventListener("click", () => {
stopped = true;
stopButton.remove();
console.error("WARNING: you must check browser download status for failed downloads!");
}, false);
document.body.appendChild(stopButton);
startAll();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment