NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Behance Project Downloader (High Resolution)
// @namespace sg.behance.downloader
// @version 1.2
// @description Download all images from a Behance project in the highest available resolution.
// @license MIT
// @match https://www.behance.net/*
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @connect *
// ==/UserScript==
(function () {
'use strict';
/* ================= CONFIG ================= */
const DOWNLOAD_DELAY = 1200;
const POST_SUCCESS_DELAY = 300;
const MAX_RETRIES = 2;
const RETRY_DELAY = 1500;
const STORAGE_KEY = "downloaded_behance_projects";
const SIDEBAR_WIDTH = 420;
const sleep = ms => new Promise(r => setTimeout(r, ms));
/* ================= STORAGE ================= */
function getProjects() {
return GM_getValue(STORAGE_KEY, {});
}
function saveProjects(data) {
GM_setValue(STORAGE_KEY, data);
}
function resetProjectStatus(id) {
const projects = getProjects();
delete projects[id];
saveProjects(projects);
}
/* ================= UTIL ================= */
function sanitize(text) {
return text.replace(/[<>:"/\\|?*]+/g, '').trim();
}
function pad(n) {
return String(n).padStart(3, "0");
}
function getProjectTitle() {
return sanitize(document.title.replace(" :: Behance", ""));
}
function getUserDisplayName() {
const el = document.querySelector(".qa-user-link, .rf-owners__owner-name");
if (!el) return "Unknown Author";
return sanitize(el.textContent.replace("by:", "").trim());
}
function getProjectId() {
const match = location.pathname.match(/gallery\/(\d+)/);
return match ? match[1] : null;
}
function extractFilename(url) {
return url.split("/").pop().split("?")[0];
}
function extractExtension(url) {
return extractFilename(url).split(".").pop().toLowerCase();
}
function formatBytes(bytes) {
if (!bytes) return "";
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + sizes[i];
}
/* ================= SCORE (INALTERADO) ================= */
function scoreUrl(url) {
if (!url) return 0;
if (url.includes("/source/")) return 10000;
const maxMatch = url.match(/max_(\d+)/);
if (maxMatch) return parseInt(maxMatch[1]);
const genericMatch = url.match(/\/(\d+)_webp/);
if (genericMatch) return parseInt(genericMatch[1]);
if (url.includes("fs")) return 1920;
if (url.includes("hd")) return 1240;
if (url.includes("disp")) return 600;
return 0;
}
function collectCandidates(img) {
const candidates = new Set();
if (img.src) candidates.add(img.src);
if (img.srcset) {
img.srcset.split(",").forEach(entry => {
candidates.add(entry.trim().split(" ")[0]);
});
}
const picture = img.closest("picture");
if (picture) {
picture.querySelectorAll("source[srcset]").forEach(source => {
source.getAttribute("srcset").split(",").forEach(entry => {
candidates.add(entry.trim().split(" ")[0]);
});
});
}
return [...candidates];
}
function getOrderedCandidates(img) {
const candidates = collectCandidates(img);
candidates.sort((a, b) => scoreUrl(b) - scoreUrl(a));
return candidates;
}
/* ================= SIDEBAR (EVOLUÇÃO VISUAL) ================= */
const Sidebar = (() => {
let total = 0;
let completed = 0;
let projectBytes = 0;
function mount(count) {
total = count;
completed = 0;
projectBytes = 0;
if (document.getElementById("bp-sidebar")) return;
const el = document.createElement("div");
el.id = "bp-sidebar";
el.innerHTML = `
<div class="bp-header">
<span>Project Downloader</span>
<button id="bp-close">✕</button>
</div>
<div class="bp-counter">0 / ${count} (0%)</div>
<div class="bp-total">
<div class="bp-total-bar"><div class="bp-total-fill"></div></div>
</div>
<div class="bp-list"></div>
`;
document.body.appendChild(el);
const btn = document.getElementById("behance-project-downloader");
if (btn) {
btn.style.transition = "right 0.25s ease";
btn.style.right = (SIDEBAR_WIDTH + 20) + "px";
}
document.getElementById("bp-close").onclick = () => {
el.remove();
if (btn) btn.style.right = "20px";
};
injectStyles();
}
function updateCounter(current) {
const percent = Math.round((current / total) * 100);
const counter = document.querySelector(".bp-counter");
if (counter) {
counter.textContent =
`${current} / ${total} (${percent}%)` +
(projectBytes ? ` • ${formatBytes(projectBytes)}` : "");
}
}
function finishCounter() {
const counter = document.querySelector(".bp-counter");
if (counter) {
counter.textContent =
`Concluído ${total} / ${total} (100%) • ${formatBytes(projectBytes)}`;
}
}
function addItem(id, fullPath) {
const parts = fullPath.split("/");
const filename = parts.pop();
const path = parts.join("/");
const list = document.querySelector(".bp-list");
const item = document.createElement("div");
item.className = "bp-item";
item.dataset.id = id;
item.innerHTML = `
<div class="bp-path">${path}</div>
<div class="bp-name-line">
<span class="bp-name">${filename}</span>
<span class="bp-resolution"></span>
<span class="bp-size"></span>
</div>
<div class="bp-bar"><div class="bp-fill"></div></div>
<div class="bp-meta">
<span class="bp-percent">0%</span>
<span class="bp-status">Waiting</span>
</div>
`;
list.appendChild(item);
}
function setResolution(id, resolution) {
const item = document.querySelector(`[data-id="${id}"]`);
if (!item) return;
const el = item.querySelector(".bp-resolution");
let color = "#e74c3c";
if (resolution === 10000) color = "#2ecc71";
else if (resolution >= 3000) color = "#27ae60";
else if (resolution >= 2000) color = "#3498db";
else if (resolution >= 1000) color = "#f39c12";
el.textContent = `• ${resolution === 10000 ? "MAX" : resolution + "px"}`;
el.style.color = color;
el.style.fontWeight = resolution === 10000 ? "700" : "600";
}
function setFileSize(id, bytes) {
const item = document.querySelector(`[data-id="${id}"]`);
if (!item) return;
item.querySelector(".bp-size").textContent =
bytes ? ` • ${formatBytes(bytes)}` : "";
projectBytes += bytes || 0;
}
function updateProgress(id, loaded, totalBytes) {
const item = document.querySelector(`[data-id="${id}"]`);
if (!item) return;
if (totalBytes) {
const percent = Math.floor((loaded / totalBytes) * 100);
item.querySelector(".bp-fill").style.width = percent + "%";
item.querySelector(".bp-percent").textContent = percent + "%";
}
item.querySelector(".bp-status").textContent = "Downloading";
}
function markDone(id) {
const item = document.querySelector(`[data-id="${id}"]`);
if (!item) return;
item.querySelector(".bp-fill").style.width = "100%";
item.querySelector(".bp-percent").textContent = "100%";
item.querySelector(".bp-status").textContent = "Done";
completed++;
const percent = Math.round((completed / total) * 100);
document.querySelector(".bp-total-fill").style.width = percent + "%";
}
function injectStyles() {
if (document.getElementById("bp-style")) return;
const style = document.createElement("style");
style.id = "bp-style";
style.textContent = `
#bp-sidebar {
position: fixed;
top: 0;
right: 0;
width: ${SIDEBAR_WIDTH}px;
height: 100vh;
background: #111;
color: #eee;
z-index: 999999;
display: flex;
flex-direction: column;
box-shadow: -4px 0 20px rgba(0,0,0,0.6);
font-family: Arial;
font-size: 13px;
}
.bp-header { padding:12px; display:flex; justify-content:space-between; background:#1a1a1a; font-weight:bold; }
.bp-counter { padding:10px; }
.bp-total { padding:0 10px 10px; }
.bp-total-bar { height:6px; background:#222; border-radius:4px; overflow:hidden; }
.bp-total-fill { height:100%; width:0%; background:#4caf50; transition: width .3s; }
.bp-list { flex:1; overflow-y:auto; padding:10px; }
.bp-item { margin-bottom:14px; border-bottom:1px solid #222; padding-bottom:10px; }
.bp-path { font-size:11px; opacity:0.6; margin-bottom:2px; }
.bp-name-line { display:flex; gap:6px; font-size:12px; flex-wrap:wrap; }
.bp-bar { height:6px; background:#222; margin:4px 0; border-radius:4px; overflow:hidden; }
.bp-fill { height:100%; width:0%; background:#1e88e5; transition: width .2s; }
.bp-meta { display:flex; justify-content:space-between; opacity:0.8; font-size:11px; }
`;
document.head.appendChild(style);
}
return { mount, addItem, setResolution, setFileSize, updateProgress, markDone, updateCounter, finishCounter };
})();
/* ================= DOWNLOAD CORE (INALTERADO LÓGICA) ================= */
function downloadWithRetry(url, path, id, attempt = 1) {
return new Promise((resolve, reject) => {
GM_download({
url,
name: path,
saveAs: false,
conflictAction: "overwrite",
headers: { "Referer": location.href },
onprogress: e => Sidebar.updateProgress(id, e.loaded, e.total),
onload: async e => {
Sidebar.setFileSize(id, e.total || 0);
await sleep(POST_SUCCESS_DELAY);
resolve(true);
},
onerror: async () => {
if (attempt < MAX_RETRIES) {
await sleep(RETRY_DELAY);
resolve(downloadWithRetry(url, path, id, attempt + 1));
} else {
reject(false);
}
}
});
});
}
async function smartDownload(img, basePath, downloadedSet, id) {
const candidates = getOrderedCandidates(img);
for (let url of candidates) {
const filename = extractFilename(url);
if (downloadedSet.has(filename)) continue;
const ext = extractExtension(url);
const finalPath = `${basePath}.${ext}`;
try {
await downloadWithRetry(url, finalPath, id);
Sidebar.setResolution(id, scoreUrl(url));
downloadedSet.add(filename);
return true;
} catch {
continue;
}
}
return false;
}
/* ================= AUTO SCROLL ================= */
async function autoScrollToBottom() {
let lastHeight = 0;
let stable = 0;
while (stable < 3) {
window.scrollTo(0, document.body.scrollHeight);
await sleep(600);
const newHeight = document.body.scrollHeight;
if (newHeight === lastHeight) stable++;
else {
stable = 0;
lastHeight = newHeight;
}
}
}
function extractImages() {
return [...new Set([...document.querySelectorAll("img[src*='project_modules']")])];
}
/* ================= MAIN DOWNLOAD ================= */
async function downloadProject(btn) {
const projectId = getProjectId();
if (!projectId) return;
btn.style.background = "#ff9800";
btn.textContent = "Rolando página...";
await autoScrollToBottom();
const images = extractImages();
if (!images.length) return;
Sidebar.mount(images.length);
const project = getProjectTitle();
const author = getUserDisplayName();
const projects = getProjects();
const existing = projects[projectId] || {};
const downloaded = new Set(existing.downloadedFiles || []);
let current = 0;
let successCount = 0;
for (let img of images) {
current++;
Sidebar.updateCounter(current);
btn.textContent = `Baixando ${current}/${images.length}`;
const basePath =
`Behance/${author}/${project}/${project} - ${author} - ${pad(current)}`;
Sidebar.addItem(current, basePath);
const success = await smartDownload(img, basePath, downloaded, current);
if (success) {
successCount++;
Sidebar.markDone(current);
}
await sleep(DOWNLOAD_DELAY);
}
Sidebar.finishCounter();
projects[projectId] = {
status: successCount === images.length ? "complete" : "partial",
title: project,
author,
totalFiles: images.length,
downloadedFiles: [...downloaded],
downloadedAt: Date.now()
};
saveProjects(projects);
applyStoredStatus(btn);
}
/* ================= DETECÇÃO DE PERFIL ================= */
function isUserProfilePage() {
const segments = location.pathname.split("/").filter(Boolean);
if (segments.length === 0) return false;
const reservedRoutes = [
"gallery",
"galleries",
"for_you",
"search",
"jobs",
"hire",
"assets",
"subscriptions",
"live",
"blog"
];
// Se o primeiro segmento for reservado → não é perfil
if (reservedRoutes.includes(segments[0])) return false;
return true;
}
/* ================= PROFILE BADGE - VERSÃO FINAL OTIMIZADA ================= */
function reconcileProfileBadges() {
if (!isUserProfilePage()) return;
const projects = getProjects();
document.querySelectorAll("a[href*='/gallery/']").forEach(link => {
const match = link.href.match(/gallery\/(\d+)/);
if (!match) return;
const projectId = match[1];
const card = link.closest("article") || link.parentElement;
if (!card) return;
card.style.position = "relative";
const existingBadge = card.querySelector(".bp-badge");
const existingCheckbox = card.querySelector(".bp-checkbox");
const isComplete = projects[projectId]?.status === "complete";
/* ---------- COMPLETE ---------- */
if (isComplete) {
if (existingCheckbox) existingCheckbox.remove();
if (existingBadge) return;
const badge = document.createElement("div");
badge.className = "bp-badge";
badge.textContent = "✓";
Object.assign(badge.style, {
position: "absolute",
top: "8px",
right: "8px",
background: "#2e7d32",
color: "#fff",
width: "26px",
height: "26px",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: "bold",
fontSize: "16px",
boxShadow: "0 2px 6px rgba(0,0,0,0.4)",
cursor: "pointer",
zIndex: "9999"
});
badge.onclick = (e) => {
e.stopPropagation();
const updated = getProjects();
delete updated[projectId];
saveProjects(updated);
reconcileProfileBadges();
};
card.appendChild(badge);
}
/* ---------- NOT COMPLETE ---------- */
else {
if (existingBadge) existingBadge.remove();
if (existingCheckbox) return;
const checkbox = document.createElement("div");
checkbox.className = "bp-checkbox";
Object.assign(checkbox.style, {
position: "absolute",
top: "8px",
right: "8px",
width: "26px",
height: "26px",
borderRadius: "4px",
border: "2px solid #ffffff",
background: "rgba(0,0,0,0.35)",
boxShadow: "0 0 0 2px rgba(0,0,0,0.4)",
cursor: "pointer",
zIndex: "9999"
});
checkbox.onclick = (e) => {
e.stopPropagation();
const updated = getProjects();
updated[projectId] = {
status: "complete",
title: "Manual",
author: "",
totalFiles: 0,
downloadedFiles: [],
downloadedAt: Date.now()
};
saveProjects(updated);
reconcileProfileBadges();
};
card.appendChild(checkbox);
}
});
}
const markDownloadedProjectsInProfile = reconcileProfileBadges;
/* ================= EVENTOS OTIMIZADOS ================= */
function debounce(fn, delay = 150) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), delay);
};
}
const debouncedReconcile = debounce(reconcileProfileBadges, 150);
/* ---------- DOM Ready ---------- */
document.addEventListener("DOMContentLoaded", () => {
reconcileProfileBadges();
});
/* ---------- Scroll (lazy load trigger natural) ---------- */
window.addEventListener("scroll", debouncedReconcile, { passive: true });
/* ---------- SPA Navigation Hook ---------- */
const originalPushState = history.pushState;
history.pushState = function (...args) {
const result = originalPushState.apply(this, args);
setTimeout(reconcileProfileBadges, 300);
return result;
};
window.addEventListener("popstate", () => {
setTimeout(reconcileProfileBadges, 300);
});
/* ================= OBSERVER + SCROLL ================= */
function isGalleryPage() {
return /^\/gallery\//.test(location.pathname);
}
if (!isGalleryPage()) {
let observerTimeout;
const profileObserver = new MutationObserver(() => {
if (observerTimeout) return;
observerTimeout = setTimeout(() => {
markDownloadedProjectsInProfile();
observerTimeout = null;
}, 150);
});
profileObserver.observe(document.body, {
childList: true,
subtree: true
});
window.addEventListener("scroll", () => {
markDownloadedProjectsInProfile();
}, { passive: true });
}
/* ================= BUTTON ================= */
function applyStoredStatus(btn) {
const projectId = getProjectId();
if (!projectId) return;
const status = getProjects()[projectId]?.status;
if (status === "complete") {
btn.style.background = "#2e7d32";
btn.textContent = "✓ Já baixado";
} else if (status === "partial") {
btn.style.background = "#ff9800";
btn.textContent = "Parcial";
} else {
btn.style.background = "#0057ff";
btn.textContent = "⬇ Baixar Projeto";
}
}
function createButton() {
if (!location.pathname.includes("/gallery/")) return;
let btn = document.getElementById("behance-project-downloader");
if (!btn) {
btn = document.createElement("button");
btn.id = "behance-project-downloader";
Object.assign(btn.style, {
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: 999999,
padding: "10px 14px",
borderRadius: "6px",
border: "none",
color: "#fff",
cursor: "pointer"
});
btn.onclick = (e) => {
const projectId = getProjectId();
if (e.altKey) {
resetProjectStatus(projectId);
applyStoredStatus(btn);
alert("Status resetado.");
return;
}
downloadProject(btn);
};
document.body.appendChild(btn);
}
applyStoredStatus(btn);
}
/* ================= INIT ================= */
createButton();
markDownloadedProjectsInProfile();
})();