|
// ==UserScript== |
|
// @name OpenGameArt Asset Downloader and Markdown Generator |
|
// @namespace http://tampermonkey.net/ |
|
// @version 1.3 |
|
// @description Download assets and create markdown with one click, compressed into a single zip file |
|
// @author Nathaniel Perry and Claude 3.5 Sonnet |
|
// @match https://opengameart.org/content/* |
|
// @grant GM_xmlhttpRequest |
|
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js |
|
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js |
|
// @license CC0 1.0 Universal https://creativecommons.org/publicdomain/zero/1.0/?ref=chooser-v1 |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
console.log("OpenGameArt Asset Downloader and Markdown Generator: loading"); |
|
|
|
function log_debug(...args) { |
|
// console.log(...args); |
|
} |
|
|
|
function decodeURLCharacters(str) { |
|
return decodeURIComponent(str.replace(/\+/g, ' ')); |
|
} |
|
|
|
// Extracts the slug from the current URL |
|
function getSlugFromURL() { |
|
const path = window.location.pathname; |
|
const parts = path.split('/'); |
|
const slug = parts[parts.length - 1]; |
|
log_debug(`getSlugFromURL slug "${slug}"`); |
|
return slug; |
|
} |
|
|
|
// Sanitizes a filename to ensure it's valid across different operating systems |
|
function sanitizeFilename(name, preserveSlashes = false) { |
|
log_debug(`sanitizeFilename name "${name}"`); |
|
|
|
name = decodeURLCharacters(name); |
|
log_debug(`sanitizeFilename decodeURLCharacters(name) "${name}"`); |
|
|
|
if (!preserveSlashes) { |
|
name = name.replace(/\//g, '_'); |
|
} |
|
log_debug(`sanitizeFilename name.replace(/\//g, '_') "${name}"`); |
|
|
|
// Check if there's an extension |
|
const lastDotIndex = name.lastIndexOf('.'); |
|
let base, ext; |
|
if (lastDotIndex > 0 && lastDotIndex < name.length - 1) { |
|
base = name.slice(0, lastDotIndex); |
|
ext = name.slice(lastDotIndex + 1); |
|
} else { |
|
base = name; |
|
ext = ''; |
|
} |
|
log_debug(`sanitizeFilename base "${base}", ext "${ext}"`); |
|
|
|
const sanitizedBase = base |
|
.replace(/[\\\/:*?"<>\|]/gi, '_') |
|
.replace(/_+/g, '_') |
|
.toLowerCase(); |
|
log_debug(`sanitizeFilename sanitizedBase "${sanitizedBase}"`); |
|
|
|
const sanitizedName = (sanitizedBase + (ext ? '.' + ext : '')) |
|
.replace(/^_+|_+$/g, ''); |
|
log_debug(`sanitizeFilename RETURN sanitizedName "${sanitizedName}"`); |
|
|
|
return sanitizedName; |
|
} |
|
|
|
// Displays a loading spinner on the download button |
|
function showSpinner(button) { |
|
button.disabled = true; |
|
button.innerHTML = 'Downloading... <span class="spinner"></span>'; |
|
button.style.position = 'relative'; |
|
const spinner = button.querySelector('.spinner'); |
|
spinner.style.display = 'inline-block'; |
|
spinner.style.width = '20px'; |
|
spinner.style.height = '20px'; |
|
spinner.style.border = '3px solid rgba(255,255,255,.3)'; |
|
spinner.style.borderRadius = '50%'; |
|
spinner.style.borderTopColor = '#fff'; |
|
spinner.style.animation = 'spin 1s linear infinite'; |
|
spinner.style.marginLeft = '10px'; |
|
|
|
const style = document.createElement('style'); |
|
style.textContent = ` |
|
@keyframes spin { |
|
to { transform: rotate(360deg); } |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
|
|
// Hides the spinner and updates the button text |
|
function hideSpinner(button, text = 'Download Files & Markdown') { |
|
button.disabled = false; |
|
button.innerHTML = text; |
|
} |
|
|
|
// Displays an error message to the user |
|
function showError(button, message) { |
|
hideSpinner(button, 'Error'); |
|
button.style.backgroundColor = 'red'; |
|
const errorMessage = document.createElement('div'); |
|
errorMessage.textContent = message; |
|
errorMessage.style.color = 'red'; |
|
errorMessage.style.marginTop = '5px'; |
|
button.parentNode.insertBefore(errorMessage, button.nextSibling); |
|
setTimeout(() => { |
|
button.style.backgroundColor = ''; |
|
errorMessage.remove(); |
|
hideSpinner(button); |
|
}, 5000); |
|
} |
|
|
|
// Main function to handle the download process |
|
function handleDownload(event) { |
|
const button = event.target; |
|
showSpinner(button); |
|
try { |
|
const info = getInfo(); |
|
log_debug("Info gathered:", info); |
|
const markdownContent = generateMarkdown(info); |
|
log_debug("Markdown generated"); |
|
const slug = getSlugFromURL(); |
|
log_debug(`handleDownload slug "${slug}"`); |
|
const template = `OpenGameArt_${sanitizeFilename(slug)}_by_${sanitizeFilename(info.author)}`; |
|
log_debug(`handleDownload template "${template}"`); |
|
createAndDownloadZip(template, markdownContent, info, button); |
|
} catch (error) { |
|
console.error('Error in handleDownload:', error); |
|
showError(button, 'An error occurred while preparing the download.'); |
|
} |
|
} |
|
|
|
// Creates a zip file containing all downloadable content and metadata |
|
function createAndDownloadZip(template, markdownContent, info, button) { |
|
const zip = new JSZip(); |
|
zip.file(`${sanitizeFilename(info.title)}.md`, markdownContent); |
|
|
|
const downloadLinks = document.querySelectorAll('.field-name-field-art-files a[href^="https://opengameart.org/sites/default/files"]'); |
|
const totalFiles = downloadLinks.length + info.previewImages.length; |
|
let completedFiles = 0; |
|
const usedFilenames = new Set(); |
|
|
|
function getUniqueFilename(filename) { |
|
if (!usedFilenames.has(filename)) { |
|
usedFilenames.add(filename); |
|
return filename; |
|
} |
|
|
|
let uniqueFilename = filename; |
|
let counter = 1; |
|
const lastDotIndex = filename.lastIndexOf('.'); |
|
const name = lastDotIndex > 0 ? filename.substring(0, lastDotIndex) : filename; |
|
const ext = lastDotIndex > 0 ? filename.substring(lastDotIndex) : ''; |
|
|
|
while (usedFilenames.has(uniqueFilename)) { |
|
uniqueFilename = `${name}_${counter}${ext}`; |
|
counter++; |
|
} |
|
|
|
usedFilenames.add(uniqueFilename); |
|
return uniqueFilename; |
|
} |
|
|
|
function checkCompletion() { |
|
completedFiles++; |
|
if (completedFiles === totalFiles) { |
|
finalizeZip(zip, template, button); |
|
} |
|
} |
|
|
|
downloadLinks.forEach((link) => { |
|
// Use the displayed filename instead of the URL |
|
const displayedFilename = link.textContent.trim(); |
|
|
|
const filename = sanitizeFilename(displayedFilename, true); |
|
const uniqueFilename = getUniqueFilename(filename); |
|
downloadFile(link.href, (content) => { |
|
if (content) { |
|
zip.file(uniqueFilename, content); |
|
checkCompletion(); |
|
} else { |
|
console.error(`Failed to download: ${uniqueFilename}`); |
|
checkCompletion(); |
|
} |
|
}); |
|
}); |
|
|
|
info.previewImages.forEach((img) => { |
|
const previewFilename = `preview_${sanitizeFilename(img.filename)}`; |
|
const uniqueFilename = getUniqueFilename(previewFilename); |
|
downloadFile(img.src, (content) => { |
|
if (content) { |
|
zip.file(uniqueFilename, content); |
|
checkCompletion(); |
|
} else { |
|
console.error(`Failed to download preview: ${uniqueFilename}`); |
|
checkCompletion(); |
|
} |
|
}); |
|
}); |
|
|
|
// Fallback for cases where there are no files to download |
|
// This ensures the zip is created even if the page has no downloadable content |
|
if (totalFiles === 0) { |
|
finalizeZip(zip, template, button); |
|
} |
|
} |
|
|
|
// Downloads a file from a given URL |
|
function downloadFile(url, callback) { |
|
GM_xmlhttpRequest({ |
|
method: 'GET', |
|
url: url, |
|
responseType: 'arraybuffer', |
|
onload: function(response) { |
|
callback(response.response); |
|
}, |
|
onerror: function(error) { |
|
console.error(`Error downloading file: ${url}`, error); |
|
callback(null); |
|
} |
|
}); |
|
} |
|
|
|
// Generates the final zip file and initiates the download |
|
function finalizeZip(zip, template, button) { |
|
zip.generateAsync({type:"blob"}) |
|
.then(function(content) { |
|
hideSpinner(button, 'Download Ready'); |
|
saveAs(content, `${template}.zip`); |
|
console.log(`Download complete: ${template}.zip`); |
|
}) |
|
.catch(function(error) { |
|
console.error('Error generating zip:', error); |
|
showError(button, 'Failed to generate zip file.'); |
|
}); |
|
} |
|
|
|
// Extracts relevant information from the webpage |
|
function getInfo() { |
|
const info = {}; |
|
info.title = document.querySelector('div.group-header div.field-name-title h2').innerText; |
|
const authorElement = document.querySelector('span.username > a'); |
|
info.author = authorElement ? authorElement.innerText : ''; |
|
info.submitter = document.querySelector('.field-name-author-submitter .field-item strong > a')?.innerText; |
|
info.url = window.location.href; |
|
info.publishDate = document.querySelector('.field-name-post-date').innerText; |
|
info.licenses = Array.from(document.querySelectorAll('.field-name-field-art-licenses .license-name')).map(el => el.innerText); |
|
info.description = document.querySelector('.field-name-body .field-item').innerText; |
|
|
|
info.tags = Array.from(document.querySelectorAll('.field-name-field-art-tags a')).map(el => ({ |
|
name: el.innerText, |
|
url: el.href |
|
})); |
|
|
|
info.previewImages = Array.from(document.querySelectorAll('.field-name-field-art-preview img')).map(img => ({ |
|
src: img.src, |
|
filename: img.src.split('/').pop() |
|
})); |
|
|
|
return info; |
|
} |
|
|
|
// Generates markdown content based on the extracted information |
|
function generateMarkdown(info) { |
|
let md = `# [${info.title}](${info.url})\n\n`; |
|
md += `Author: [${info.author}](https://opengameart.org/users/${info.author.toLowerCase()})\n\n`; |
|
if (info.submitter) { |
|
md += `(Submitted by **${info.submitter}**)\n\n`; |
|
} |
|
md += `Published: ${info.publishDate}\n\n`; |
|
md += `License(s):\n${info.licenses.map(license => `- ${license}`).join('\n')}\n\n`; |
|
md += `Tags:\n${info.tags.map(tag => `- [${tag.name}](${tag.url})`).join('\n')}\n\n`; |
|
md += `## Description\n\n${info.description}\n`; |
|
return md; |
|
} |
|
|
|
// Adds the download button to the webpage |
|
function addDownloadButton() { |
|
const titleElement = document.querySelector('div.group-header div.field-name-title h2'); |
|
if (!titleElement) return; |
|
const button = document.createElement('button'); |
|
button.innerHTML = 'Download Files & Markdown'; |
|
button.style.marginLeft = '10px'; |
|
titleElement.parentNode.insertBefore(button, titleElement.nextSibling); |
|
button.addEventListener('click', handleDownload); |
|
log_debug("Download button added successfully"); |
|
} |
|
|
|
addDownloadButton(); |
|
})(); |