ToolsWeb DevContent Creation
I Built a Free Asset Downloader for Images, Music & SFX (No Sign-ups Needed)
A single self-contained HTML page that searches multiple free stock-asset APIs at once, lets you preview results, and batch-downloads whatever you pick — all client-side, no backend, no account required.

Why I Made This
Every time I needed a quick background photo, a bit of royalty-free music, or a UI sound effect for a project, I found myself bouncing between five different websites, creating accounts, and copy-pasting URLs. So I put together a small tool that does the searching for me: one search box, multiple free sources queried in parallel, and instant previews before you commit to downloading anything.
What It Does
- Multi-source search — searches several free, largely keyless APIs at once and merges the results into one grid.
- Comma-separated queries — type
dog, cat, fishand it runs each term as its own search, so you get a batch of unrelated assets in one go. - Live previews — play audio clips right in the results list before downloading, and see image thumbnails up front.
- One-click and batch download — grab a single asset or select a pile of them and download everything at once.
- Runs entirely in the browser — it’s one HTML file. No server, no build step, no install.
Sources It Pulls From
| Type | Source | API key needed? |
|---|---|---|
| Images | Openverse (CC-licensed photos & illustrations) | No |
| Images | Pixabay / Pexels | Optional, for higher volume |
| Music | Wikimedia Commons Audio | No |
| SFX | Wikimedia Commons Audio | No |
| Audio (fallback) | Bensound / Mixkit curated samples | No |
| Audio (optional) | Freesound | Optional |
How to Use It
- Open the HTML file in any browser — no installation needed.
- Pick a type: image, music, or SFX.
- Type one search term, or several separated by commas.
- Preview results, then download individually or in bulk.
The Code
It’s a single self-contained HTML file — just save it as asset_downloader.html and open it in your browser. No dependencies, no build tools.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Asset Downloader</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#0f1117;color:#e2e8f0;min-height:100vh}
.app{max-width:1200px;margin:0 auto;padding:24px}
h1{font-size:22px;font-weight:600;margin-bottom:4px;color:#f1f5f9}
.subtitle{font-size:13px;color:#64748b;margin-bottom:24px}
.search-panel{background:#1a1d27;border:1px solid #2d3148;border-radius:12px;padding:20px;margin-bottom:16px}
.row{display:flex;gap:10px;margin-bottom:12px;flex-wrap:wrap;align-items:flex-start}
.row:last-child{margin-bottom:0}
textarea{flex:1;min-width:200px;background:#0f1117;border:1px solid #2d3148;border-radius:8px;color:#e2e8f0;padding:10px;font-size:13px;resize:vertical;min-height:52px;font-family:inherit}
textarea::placeholder{color:#475569}
textarea:focus{outline:none;border-color:#6366f1}
select{background:#0f1117;border:1px solid #2d3148;border-radius:8px;color:#e2e8f0;padding:9px 12px;font-size:13px;cursor:pointer;height:40px}
select:focus{outline:none;border-color:#6366f1}
button{border-radius:8px;padding:9px 18px;font-size:13px;font-weight:500;cursor:pointer;border:none;transition:all .15s}
.btn-primary{background:#6366f1;color:#fff;height:40px}
.btn-primary:hover{background:#5457d4}
.btn-primary:disabled{background:#2d3148;color:#475569;cursor:not-allowed}
.btn-ghost{background:transparent;color:#94a3b8;border:1px solid #2d3148}
.btn-ghost:hover{background:#1e2235;color:#e2e8f0}
.btn-success{background:#065f46;color:#6ee7b7}
.btn-success:hover{background:#047857}
.api-section{background:#1a1d27;border:1px solid #2d3148;border-radius:12px;padding:16px;margin-bottom:16px}
.api-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#94a3b8;user-select:none}
.api-toggle:hover{color:#e2e8f0}
.api-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:14px}
.api-field label{font-size:11px;color:#64748b;display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
.api-field input{width:100%;background:#0f1117;border:1px solid #2d3148;border-radius:6px;color:#e2e8f0;padding:8px 10px;font-size:12px;font-family:monospace}
.api-field input:focus{outline:none;border-color:#6366f1}
.api-field input::placeholder{color:#334155}
.status-bar{background:#1a1d27;border:1px solid #2d3148;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:12px;color:#94a3b8;display:flex;align-items:center;gap:8px}
.dot{width:8px;height:8px;border-radius:50%;background:#334155;flex-shrink:0}
.dot.loading{background:#f59e0b;animation:pulse 1s infinite}
.dot.done{background:#10b981}
.dot.error{background:#ef4444}
.results-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}
.results-header h2{font-size:15px;font-weight:500;color:#94a3b8}
.selection-actions{display:flex;gap:8px;align-items:center}
.count-badge{background:#1e2235;border:1px solid #2d3148;border-radius:6px;padding:3px 10px;font-size:12px;color:#94a3b8}
.results-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px;margin-bottom:100px}
.card{background:#1a1d27;border:1px solid #2d3148;border-radius:10px;overflow:hidden;cursor:pointer;transition:border-color .15s,transform .1s;position:relative;display:flex;flex-direction:column}
.card:hover{border-color:#6366f1;transform:translateY(-1px)}
.card.selected{border-color:#6366f1;background:#1b1c36}
.card-check{position:absolute;top:8px;right:8px;width:22px;height:22px;background:#6366f1;border-radius:5px;display:none;align-items:center;justify-content:center;font-size:13px;color:#fff;font-weight:700;z-index:5;pointer-events:none}
.card.selected .card-check{display:flex}
.img-wrap{width:100%;height:130px;background:#12111a;overflow:hidden;position:relative;flex-shrink:0}
.img-wrap img{width:100%;height:100%;object-fit:cover;display:block;opacity:0;transition:opacity .4s}
.img-wrap img.loaded{opacity:1}
.img-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:30px;color:#2d3148}
/* PNG preview: checkerboard bg to show transparency */
.png-wrap{width:100%;height:130px;background:
repeating-conic-gradient(#1e1e2e 0% 25%,#2a2a3e 0% 50%)
0 0/20px 20px;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;flex-shrink:0}
.png-wrap img{max-width:90%;max-height:110px;object-fit:contain;opacity:0;transition:opacity .3s}
.png-wrap img.loaded{opacity:1}
.icon-wrap{width:100%;height:130px;background:#12111a;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.icon-wrap img{width:72px;height:72px;object-fit:contain;opacity:0;transition:opacity .3s}
.icon-wrap img.loaded{opacity:1}
.audio-wrap{width:100%;height:130px;background:#12111a;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;flex-shrink:0}
.play-btn{width:52px;height:52px;border-radius:50%;background:#6366f1;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s,transform .1s}
.play-btn:hover{background:#5457d4;transform:scale(1.06)}
.play-btn:active{transform:scale(.95)}
.play-icon{width:22px;height:22px;fill:#fff;margin-left:3px}
.pause-icon{width:22px;height:22px;fill:#fff}
.audio-dur{font-size:11px;color:#475569}
.prog-bar{width:80%;height:4px;background:#2d3148;border-radius:2px;cursor:pointer;overflow:hidden}
.prog-inner{height:100%;background:#6366f1;width:0%;pointer-events:none;transition:width .15s linear}
.card-info{padding:10px;flex:1}
.card-title{font-size:12px;font-weight:500;color:#e2e8f0;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card-meta{font-size:11px;color:#475569}
.card-source{display:inline-block;font-size:10px;background:#1e2235;border:1px solid #2d3148;border-radius:4px;padding:1px 6px;color:#64748b;margin-top:4px}
/* PNG source badge – distinct purple tint */
.card-source.png{background:#1e1b36;border-color:#4338a0;color:#a5b4fc}
.tag-pills{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.pill{font-size:11px;padding:3px 10px;border-radius:20px;background:#1e2235;border:1px solid #2d3148;color:#64748b;cursor:pointer}
.pill:hover{border-color:#6366f1;color:#a5b4fc}
.error-msg{background:#1a0a0a;border:1px solid #7f1d1d;border-radius:8px;padding:10px 14px;font-size:12px;color:#fca5a5;margin-bottom:12px}
.dl-bar{position:fixed;bottom:0;left:0;right:0;background:#0f1117;border-top:1px solid #2d3148;padding:14px 24px;display:flex;gap:10px;align-items:center;justify-content:flex-end;z-index:100}
.dl-bar.hidden{display:none}
.empty{text-align:center;padding:60px 20px;color:#475569;grid-column:1/-1}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
</style>
</head>
<body>
<div class="app">
<h1>🎨 Asset Downloader</h1>
<p class="subtitle">Search images, PNGs, icons, music & SFX from free sources. Multi-select → download as ZIP or save to folder.</p>
<div class="search-panel">
<div class="row">
<textarea id="searchInput" placeholder='Comma-separated: fire, water, tech Or JSON: [{"query":"fire","type":"sfx"},{"query":"arrow","type":"png"}]' rows="2"></textarea>
<select id="typeFilter">
<option value="all">All types</option>
<option value="image">🖼 Images</option>
<option value="png">🟣 PNG (transparent)</option>
<option value="icon">🔷 Icons</option>
<option value="music">🎵 Music</option>
<option value="sfx">🔊 SFX</option>
</select>
<button class="btn-primary" onclick="runSearch()" id="searchBtn">Search</button>
</div>
<div class="tag-pills" id="recentTags"></div>
</div>
<div class="api-section">
<div class="api-toggle" onclick="toggleApi()">
<span id="apiArrow">▶</span>
<span>API keys — optional, enables more sources & results</span>
</div>
<div id="apiPanel" style="display:none">
<div class="api-grid">
<div class="api-field"><label>Unsplash Access Key</label><input type="password" id="keyUnsplash" placeholder="paste key..."></div>
<div class="api-field"><label>Pexels API Key</label><input type="password" id="keyPexels" placeholder="paste key..."></div>
<div class="api-field"><label>Pixabay API Key</label><input type="password" id="keyPixabay" placeholder="paste key... (also used for PNGs)"></div>
<div class="api-field"><label>Freesound API Key</label><input type="password" id="keyFreesound" placeholder="paste key..."></div>
</div>
<p style="font-size:11px;color:#475569;margin-top:10px">Keys stored in localStorage only. Never sent to any server. App always falls back to no-key sources.</p>
</div>
</div>
<!-- PNG Sources info box -->
<div style="background:#12111a;border:1px solid #2d3148;border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:12px;color:#64748b;line-height:1.7">
<span style="color:#a5b4fc;font-weight:600">🟣 Free PNG Sources</span> ·
<strong style="color:#94a3b8">OpenClipart</strong> — free API, no key needed, clipart & illustrations ·
<strong style="color:#94a3b8">Wikimedia Commons</strong> — free API, no key needed, huge transparent PNG library ·
<strong style="color:#94a3b8">Pixabay PNG filter</strong> — requires Pixabay API key above, high-quality photos + cutouts ·
<strong style="color:#94a3b8">Sticker.guru</strong> — free, no key, sticker-style PNGs
</div>
<div style="background:#12111a;border:1px solid #2d3148;border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:12px;color:#64748b;line-height:1.7">
<span style="color:#6ee7b7;font-weight:600">🎵 Free Audio & 🖼 Extra Image Sources</span> ·
<strong style="color:#94a3b8">Openverse</strong> — free API, no key needed, CC-licensed photos & illustrations, query-matched ·
<strong style="color:#94a3b8">Wikimedia Commons Audio</strong> — free API, no key needed, real query-matched music & SFX ·
<strong style="color:#94a3b8">Bensound / Mixkit samples</strong> — curated fallback, only used (and labeled) when no query match is found
</div>
<div id="statusBar" class="status-bar" style="display:none">
<div class="dot" id="statusDot"></div>
<span id="statusText">Searching...</span>
</div>
<div id="errorMsg" class="error-msg" style="display:none"></div>
<div id="resultsSection" style="display:none">
<div class="results-header">
<h2 id="resultsCount">0 results</h2>
<div class="selection-actions">
<span class="count-badge" id="selectedCount">0 selected</span>
<button class="btn-ghost" style="padding:5px 12px;font-size:12px" onclick="selectAll()">Select all</button>
<button class="btn-ghost" style="padding:5px 12px;font-size:12px" onclick="clearSel()">Clear</button>
</div>
</div>
<div class="results-grid" id="resultsGrid"></div>
</div>
<div class="dl-bar hidden" id="dlBar">
<span style="font-size:13px;color:#94a3b8" id="dlCount">0 selected</span>
<button class="btn-success" onclick="downloadZip()">⬇ Download ZIP</button>
<button class="btn-ghost" onclick="saveToFolder()">📁 Save to Folder</button>
</div>
</div>
<script>
let results=[], selected=new Set(), activeAudio=null, activePlayBtn=null, activeIdx=-1;
let recent=JSON.parse(localStorage.getItem('adl_recent')||'[]');
function gk(k){return localStorage.getItem('adl_'+k)||document.getElementById(k)?.value?.trim()||''}
function saveKeys(){['keyUnsplash','keyPexels','keyPixabay','keyFreesound'].forEach(k=>{const v=document.getElementById(k).value.trim();if(v)localStorage.setItem('adl_'+k,v)})}
function loadKeys(){['keyUnsplash','keyPexels','keyPixabay','keyFreesound'].forEach(k=>{const v=localStorage.getItem('adl_'+k);if(v)document.getElementById(k).value=v})}
let apiOpen=false;
function toggleApi(){
apiOpen=!apiOpen;
document.getElementById('apiPanel').style.display=apiOpen?'block':'none';
document.getElementById('apiArrow').textContent=apiOpen?'▼':'▶';
}
function parseInput(raw){
raw=raw.trim();
if(raw.startsWith('[')){try{return JSON.parse(raw)}catch(e){}}
const t=document.getElementById('typeFilter').value;
return raw.split(',').map(q=>q.trim()).filter(Boolean).map(q=>({query:q,type:t}));
}
function setStatus(state,msg){
document.getElementById('statusBar').style.display='flex';
document.getElementById('statusText').textContent=msg;
document.getElementById('statusDot').className='dot '+(state==='loading'?'loading':state==='done'?'done':'error');
}
function showErr(m){const e=document.getElementById('errorMsg');e.style.display='block';e.textContent=m}
function hideErr(){document.getElementById('errorMsg').style.display='none'}
async function runSearch(){
const raw=document.getElementById('searchInput').value;
if(!raw.trim()){showErr('Enter search terms.');return}
saveKeys();hideErr();stopAudio();
results=[];selected=new Set();
document.getElementById('resultsSection').style.display='none';
document.getElementById('dlBar').classList.add('hidden');
setStatus('loading','Searching...');
document.getElementById('searchBtn').disabled=true;
addRecent(raw);
const queries=parseInput(raw);
let all=[];
for(const q of queries){
const types=(!q.type||q.type==='all')?['image','png','icon','music','sfx']:[q.type];
for(const t of types){
try{const r=await fetchType(q.query,t);all=all.concat(r)}catch(e){console.warn(t,e)}
}
}
results=all;
renderResults();
document.getElementById('searchBtn').disabled=false;
setStatus('done',results.length+' assets found');
}
async function fetchType(q,type){
let items=[];
if(type==='image') items=await fetchImages(q);
else if(type==='png') items=await fetchPNGs(q);
else if(type==='icon') items=await fetchIcons(q);
else if(type==='music'||type==='sfx') items=await fetchAudio(q,type);
/* Rename every file to just the search query + counter.
e.g. query "apple red" → apple_red.jpg, apple_red_2.jpg, apple_red_3.jpg */
const base=san(q);
items.forEach((item,i)=>{
const ext=item.ext||'jpg';
item.filename=i===0?`${base}.${ext}`:`${base}_${i+1}.${ext}`;
});
return items;
}
/* ── IMAGES ─────────────────────────── */
async function fetchImages(q){
const items=[];
/* Openverse – free, no key, huge CC-licensed photo/illustration search that
actually matches the query text. This is now the primary keyless source. */
try{
const d=await apiFetch(`https://api.openverse.org/v1/images/?q=${enc(q)}&page_size=12`);
(d.results||[]).forEach(h=>{
const ext=(h.url.split('.').pop()||'jpg').split('?')[0].slice(0,4);
items.push({id:'ov_'+h.id,title:h.title||q,thumb:h.thumbnail||h.url,downloadUrl:h.url,source:'Openverse',type:'image',ext,filename:san(h.title||q)+'_openverse_'+h.id+'.'+ext});
});
}catch(e){console.warn('Openverse',e)}
const pxk=gk('keyPixabay');
if(pxk){
try{
const d=await apiFetch(`https://pixabay.com/api/?key=${pxk}&q=${enc(q)}&image_type=photo&per_page=9&safesearch=true`);
(d.hits||[]).forEach(h=>items.push({id:'px_'+h.id,title:h.tags||q,thumb:h.previewURL,downloadUrl:h.largeImageURL,source:'Pixabay',type:'image',ext:'jpg',filename:san(q)+'_pixabay_'+h.id+'.jpg'}));
}catch(e){}
}
const usk=gk('keyUnsplash');
if(usk){
try{
const d=await apiFetch(`https://api.unsplash.com/search/photos?query=${enc(q)}&per_page=9`,{Authorization:'Client-ID '+usk});
(d.results||[]).forEach(h=>items.push({id:'us_'+h.id,title:h.alt_description||q,thumb:h.urls.small,downloadUrl:h.urls.regular,source:'Unsplash',type:'image',ext:'jpg',filename:san(q)+'_unsplash_'+h.id+'.jpg'}));
}catch(e){}
}
const pxlk=gk('keyPexels');
if(pxlk){
try{
const d=await apiFetch(`https://api.pexels.com/v1/search?query=${enc(q)}&per_page=9`,{Authorization:pxlk});
(d.photos||[]).forEach(h=>items.push({id:'pxl_'+h.id,title:h.alt||q,thumb:h.src.medium,downloadUrl:h.src.large,source:'Pexels',type:'image',ext:'jpg',filename:san(q)+'_pexels_'+h.id+'.jpg'}));
}catch(e){}
}
/* Picsum is just random placeholder photos keyed by seed number — it does
NOT search by keyword, so it never actually matches the query. Only use
it as a last-resort filler when every real search above found nothing,
and label it clearly so it's not mistaken for a real match. */
if(items.length===0){
const seed=encodeURIComponent(q);
for(let i=0;i<6;i++){
const s=seed+i;
items.push({id:'picsum_'+s,title:q+' (random placeholder, not query-matched)',thumb:`https://picsum.photos/seed/${s}/400/300`,downloadUrl:`https://picsum.photos/seed/${s}/1200/900`,source:'Picsum (random)',type:'image',ext:'jpg',filename:san(q)+'_placeholder_'+i+'.jpg'});
}
}
return items;
}
/* ── PNGs (transparent) ─────────────── */
async function fetchPNGs(q){
const items=[];
/* 1. OpenClipart – free REST API, no key, SVG+PNG clipart */
try{
const d=await apiFetch(`https://openclipart.org/search/json/?query=${enc(q)}&amount=12&offset=0`);
(d.payload||[]).forEach(clip=>{
/* thumbnail is SVG-rendered PNG via their CDN */
const thumb=clip.svg?.png_2400||clip.svg?.png_480||'';
const dlUrl=clip.svg?.png_2400||clip.svg?.url||'';
if(!thumb) return;
items.push({
id:'oc_'+clip.id,
title:clip.title||q,
thumb,
downloadUrl:dlUrl,
source:'OpenClipart',
type:'png',
ext: dlUrl.endsWith('.svg')?'svg':'png',
filename:san(clip.title||q)+'_openclipart_'+clip.id+(dlUrl.endsWith('.svg')?'.svg':'.png'),
isPng:true
});
});
}catch(e){console.warn('OpenClipart',e)}
/* 2. Wikimedia Commons – free MediaWiki API, no key */
try{
const url=`https://commons.wikimedia.org/w/api.php?action=query&generator=search&gsrnamespace=6&gsrsearch=${enc(q)}+filetype:png&gsrlimit=12&prop=imageinfo&iiprop=url|mime|size|thumburl&iiurlwidth=300&format=json&origin=*`;
const d=await apiFetch(url);
const pages=Object.values(d?.query?.pages||{});
pages.forEach(p=>{
const ii=p.imageinfo?.[0];
if(!ii||ii.mime!=='image/png') return;
items.push({
id:'wm_'+p.pageid,
title:p.title?.replace('File:','')||q,
thumb:ii.thumburl||ii.url,
downloadUrl:ii.url,
source:'Wikimedia',
type:'png',
ext:'png',
filename:san(p.title?.replace('File:','')||q)+'.png',
isPng:true
});
});
}catch(e){console.warn('Wikimedia',e)}
/* 3. Pixabay PNG filter (transparent clipart/vector) – needs key */
const pxk=gk('keyPixabay');
if(pxk){
try{
const d=await apiFetch(`https://pixabay.com/api/?key=${pxk}&q=${enc(q)}&image_type=clipart&colors=transparent&per_page=10&safesearch=true`);
(d.hits||[]).forEach(h=>{
items.push({
id:'pxpng_'+h.id,
title:h.tags||q,
thumb:h.previewURL,
downloadUrl:h.largeImageURL,
source:'Pixabay PNG',
type:'png',
ext:'png',
filename:san(q)+'_pixabay_png_'+h.id+'.png',
isPng:true
});
});
}catch(e){console.warn('Pixabay PNG',e)}
}
/* 4. Sticker.guru – free, no-key, sticker PNG CDN via search */
try{
/* They expose a simple JSON endpoint used by their site */
const d=await apiFetch(`https://sticker.guru/api/search?q=${enc(q)}&limit=8`);
(d.results||d.stickers||[]).slice(0,8).forEach((s,i)=>{
const thumb=s.thumbnail||s.url||s.image||'';
const dlUrl=s.url||s.image||thumb;
if(!thumb) return;
items.push({
id:'sg_'+i+'_'+q,
title:s.title||q+' sticker',
thumb,
downloadUrl:dlUrl,
source:'Sticker.guru',
type:'png',
ext:'png',
filename:san(s.title||q)+'_sticker_'+i+'.png',
isPng:true
});
});
}catch(e){/* sticker.guru may 404 if endpoint changes – silent */}
return items;
}
/* ── ICONS ─────────────────────────── */
async function fetchIcons(q){
const items=[];
try{
const d=await apiFetch(`https://api.iconify.design/search?query=${enc(q)}&limit=24`);
(d.icons||[]).slice(0,20).forEach(icon=>{
const [prefix,name]=icon.split(':');
const thumbUrl=`https://api.iconify.design/${prefix}/${name}.svg?color=%236366f1`;
const dlUrl=`https://api.iconify.design/${prefix}/${name}.svg`;
items.push({id:'ify_'+icon,title:name+' ('+prefix+')',thumb:thumbUrl,downloadUrl:dlUrl,source:'Iconify',type:'icon',ext:'svg',filename:san(name)+'.svg'});
});
}catch(e){}
return items;
}
/* ── AUDIO ─────────────────────────── */
async function fetchAudio(q,subtype){
const items=[];
const fsk=gk('keyFreesound');
if(fsk){
try{
const filter=subtype==='music'?'&filter=tag:music':'';
const d=await apiFetch(`https://freesound.org/apiv2/search/text/?query=${enc(q)}${filter}&fields=id,name,previews,duration&token=${fsk}&page_size=10`);
(d.results||[]).forEach(s=>{
const prev=s.previews?.['preview-lq-mp3']||'';
if(!prev)return;
items.push({id:'fs_'+s.id,title:s.name,previewUrl:prev,downloadUrl:prev,source:'Freesound',type:subtype,ext:'mp3',duration:Math.round(s.duration),filename:san(s.name)+'.mp3'});
});
}catch(e){console.warn('Freesound',e)}
}
/* Wikimedia Commons audio – free, no key, and actually matches the query
(searches ogg/mp3/wav/flac files whose title/description contains q) */
try{
const url=`https://commons.wikimedia.org/w/api.php?action=query&generator=search&gsrnamespace=6&gsrsearch=${enc(q)}+(filetype:ogg+OR+filetype:mp3+OR+filetype:wav+OR+filetype:flac)&gsrlimit=12&prop=imageinfo&iiprop=url|mime|size|duration&format=json&origin=*`;
const d=await apiFetch(url);
const pages=Object.values(d?.query?.pages||{});
pages.forEach(p=>{
const ii=p.imageinfo?.[0];
if(!ii||!/^audio\//.test(ii.mime||'')) return;
const ext=(ii.url.split('.').pop()||'mp3').toLowerCase();
const title=p.title?.replace('File:','')||q;
items.push({id:'wma_'+p.pageid,title,previewUrl:ii.url,downloadUrl:ii.url,source:'Wikimedia',type:subtype,ext,duration:ii.duration?Math.round(ii.duration):undefined,filename:san(title)+'.'+ext});
});
}catch(e){console.warn('Wikimedia audio',e)}
/* Curated sample packs (Bensound/Mixkit) only kick in as a fallback when
nothing above actually matched the query — and only if a track's own
name loosely matches the search term, so they don't get mixed into
unrelated queries. */
if(items.length===0){
const ql=q.toLowerCase();
if(subtype==='music'){
const tracks=[
{t:'Ukulele',u:'https://www.bensound.com/bensound-music/bensound-ukulele.mp3'},
{t:'Sunny',u:'https://www.bensound.com/bensound-music/bensound-sunny.mp3'},
{t:'Acoustic Breeze',u:'https://www.bensound.com/bensound-music/bensound-acousticbreeze.mp3'},
{t:'Creative Minds',u:'https://www.bensound.com/bensound-music/bensound-creativeminds.mp3'},
{t:'Epic',u:'https://www.bensound.com/bensound-music/bensound-epic.mp3'},
];
const matched=tracks.filter(t=>t.t.toLowerCase().includes(ql)||ql.includes(t.t.toLowerCase()));
const pool=matched.length?matched:tracks;
pool.forEach((t,i)=>items.push({id:'bs_'+i+'_'+san(q),title:'Bensound – '+t.t+(matched.length?'':' (sample, not query-matched)'),previewUrl:t.u,downloadUrl:t.u,source:'Bensound',type:'music',ext:'mp3',filename:'bensound_'+san(t.t)+'.mp3'}));
}
if(subtype==='sfx'){
const sfx=[
{t:'Notification Bell',u:'https://assets.mixkit.co/sfx/preview/mixkit-software-interface-start-2574.mp3'},
{t:'Button Click',u:'https://assets.mixkit.co/sfx/preview/mixkit-select-click-1109.mp3'},
{t:'Success Chime',u:'https://assets.mixkit.co/sfx/preview/mixkit-achievement-bell-600.mp3'},
{t:'Alert Tone',u:'https://assets.mixkit.co/sfx/preview/mixkit-alarm-clock-beep-988.mp3'},
{t:'Game Win',u:'https://assets.mixkit.co/sfx/preview/mixkit-winning-chimes-2015.mp3'},
{t:'Swoosh',u:'https://assets.mixkit.co/sfx/preview/mixkit-fast-small-sweep-transition-166.mp3'},
];
const matched=sfx.filter(s=>s.t.toLowerCase().includes(ql)||ql.includes(s.t.toLowerCase()));
const pool=matched.length?matched:sfx;
pool.forEach((s,i)=>items.push({id:'mx_'+i+'_'+san(q),title:'Mixkit – '+s.t+(matched.length?'':' (sample, not query-matched)'),previewUrl:s.u,downloadUrl:s.u,source:'Mixkit',type:'sfx',ext:'mp3',filename:'mixkit_'+san(s.t)+'.mp3'}));
}
}
return items;
}
async function apiFetch(url,headers={}){
const r=await fetch(url,{headers:{...headers}});
if(!r.ok) throw new Error('HTTP '+r.status);
return r.json();
}
/* ── RENDER ─────────────────────────── */
function renderResults(){
const grid=document.getElementById('resultsGrid');
grid.innerHTML='';
document.getElementById('resultsSection').style.display='block';
document.getElementById('resultsCount').textContent=results.length+' results';
if(!results.length){
grid.innerHTML='<div class="empty"><p>No results. Try different terms or add API keys for more sources.</p></div>';
return;
}
results.forEach((item,idx)=>{
const card=document.createElement('div');
card.className='card';
card.dataset.idx=idx;
let preview='';
if(item.type==='png'){
/* checkerboard bg to reveal transparency */
preview=`<div class="png-wrap">
<img src="${esc(item.thumb)}" loading="lazy" decoding="async"
onload="this.classList.add('loaded')"
onerror="this.outerHTML='<span style=\'font-size:36px\'>🖼️</span>'"
alt="${esc(item.title)}">
</div>`;
} else if(item.type==='image'){
preview=`<div class="img-wrap">
<div class="img-placeholder">🖼</div>
<img src="${esc(item.thumb)}" loading="lazy" decoding="async"
onload="this.classList.add('loaded');this.previousElementSibling.style.display='none'"
onerror="this.remove()"
alt="${esc(item.title)}">
</div>`;
} else if(item.type==='icon'){
preview=`<div class="icon-wrap">
<img src="${esc(item.thumb)}" loading="lazy" decoding="async"
onload="this.classList.add('loaded')"
onerror="this.outerHTML='<span style=\'font-size:40px;color:#6366f1\'>🔷</span>'"
alt="${esc(item.title)}" style="width:72px;height:72px">
</div>`;
} else {
const dur=item.duration?fmtDur(item.duration):'';
preview=`<div class="audio-wrap">
<button class="play-btn" onclick="togglePlay(event,${idx})" aria-label="Play preview">
<svg class="play-icon" viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" fill="white"/></svg>
<svg class="pause-icon" viewBox="0 0 24 24" style="display:none"><rect x="5" y="3" width="4" height="18" fill="white"/><rect x="15" y="3" width="4" height="18" fill="white"/></svg>
</button>
${dur?`<div class="audio-dur">${dur}</div>`:''}
<div class="prog-bar" onclick="seekAudio(event,${idx})">
<div class="prog-inner" id="prog_${idx}"></div>
</div>
</div>`;
}
const sourceCls=item.isPng?'card-source png':'card-source';
card.innerHTML=`
<div class="card-check">✓</div>
${preview}
<div class="card-info">
<div class="card-title" title="${esc(item.title)}">${esc(item.title)}</div>
<div class="card-meta" title="${esc(item.filename||'')}">${(item.ext||'').toUpperCase()}${item.isPng?' · transparent':''}<br><span style="color:#6366f1;font-size:10px;word-break:break-all">${esc((item.filename||'').slice(0,32))}${(item.filename||'').length>32?'…':''}</span></div>
<span class="${sourceCls}">${item.source}</span>
</div>`;
card.addEventListener('click',e=>{
if(e.target.closest('.play-btn')||e.target.closest('.prog-bar')) return;
toggleSel(idx,card);
});
grid.appendChild(card);
});
updateUI();
}
/* ── AUDIO PLAYBACK ─────────────────── */
function togglePlay(e,idx){
e.stopPropagation();
const item=results[idx];
if(!item?.previewUrl) return;
const btn=e.currentTarget;
if(activeIdx===idx&&activeAudio){
if(activeAudio.paused){activeAudio.play();setPlayState(btn,true)}
else{activeAudio.pause();setPlayState(btn,false)}
return;
}
stopAudio();
const audio=new Audio(item.previewUrl);
audio.preload='auto';
audio.addEventListener('timeupdate',()=>{
if(!audio.duration) return;
const bar=document.getElementById('prog_'+idx);
if(bar) bar.style.width=(audio.currentTime/audio.duration*100).toFixed(2)+'%';
});
audio.addEventListener('ended',()=>{setPlayState(btn,false);const bar=document.getElementById('prog_'+idx);if(bar)bar.style.width='0%';activeIdx=-1});
audio.addEventListener('error',()=>{setPlayState(btn,false);activeIdx=-1});
audio.play().then(()=>setPlayState(btn,true)).catch(()=>setPlayState(btn,false));
activeAudio=audio;activePlayBtn=btn;activeIdx=idx;
}
function seekAudio(e,idx){
if(!activeAudio||activeIdx!==idx) return;
const rect=e.currentTarget.getBoundingClientRect();
activeAudio.currentTime=((e.clientX-rect.left)/rect.width)*activeAudio.duration;
}
function stopAudio(){
if(activeAudio){activeAudio.pause();activeAudio.src='';activeAudio=null}
if(activePlayBtn){setPlayState(activePlayBtn,false);activePlayBtn=null}
document.querySelectorAll('.prog-inner').forEach(b=>b.style.width='0%');
activeIdx=-1;
}
function setPlayState(btn,playing){
const pi=btn.querySelector('.play-icon'),pa=btn.querySelector('.pause-icon');
if(pi)pi.style.display=playing?'none':'block';
if(pa)pa.style.display=playing?'block':'none';
}
/* ── SELECTION ──────────────────────── */
function toggleSel(idx,card){selected.has(idx)?selected.delete(idx):selected.add(idx);card.classList.toggle('selected',selected.has(idx));updateUI()}
function selectAll(){results.forEach((_,i)=>selected.add(i));rerenderSel()}
function clearSel(){selected.clear();rerenderSel()}
function rerenderSel(){document.querySelectorAll('.card').forEach(c=>c.classList.toggle('selected',selected.has(+c.dataset.idx)));updateUI()}
function updateUI(){
const n=selected.size;
document.getElementById('selectedCount').textContent=n+' selected';
document.getElementById('dlCount').textContent=n+' selected';
document.getElementById('dlBar').classList.toggle('hidden',n===0);
}
/* ── DOWNLOAD ───────────────────────── */
async function downloadZip(){
const items=[...selected].map(i=>results[i]);
if(!items.length) return;
setStatus('loading','Packaging '+items.length+' files into ZIP...');
const zip=new JSZip(),seen={};
for(const item of items){
try{
let fn=item.filename||san(item.title)+'.'+item.ext;
if(seen[fn]){seen[fn]++;fn=fn.replace(/(\.\w+)$/,'_'+seen[fn]+'$1')}else seen[fn]=1;
const r=await fetch(item.downloadUrl,{mode:'cors'});
if(!r.ok) throw new Error('HTTP '+r.status);
zip.file(fn,await r.blob());
}catch(er){console.warn('Skipped',item.title,er.message)}
}
const blob=await zip.generateAsync({type:'blob',compression:'DEFLATE'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download='assets_'+Date.now()+'.zip';
a.click();
setStatus('done','ZIP downloaded!');
}
async function saveToFolder(){
if(!window.showDirectoryPicker){showErr('Save to Folder requires Chrome/Edge. Use ZIP download instead.');return}
const items=[...selected].map(i=>results[i]);
if(!items.length) return;
try{
const dir=await window.showDirectoryPicker({mode:'readwrite'});
setStatus('loading','Saving '+items.length+' files...');
const seen={};
for(const item of items){
try{
let fn=item.filename||san(item.title)+'.'+item.ext;
if(seen[fn]){seen[fn]++;fn=fn.replace(/(\.\w+)$/,'_'+seen[fn]+'$1')}else seen[fn]=1;
const r=await fetch(item.downloadUrl,{mode:'cors'});
if(!r.ok) throw new Error('HTTP '+r.status);
const fh=await dir.getFileHandle(fn,{create:true});
const w=await fh.createWritable();
await w.write(await r.blob());
await w.close();
}catch(er){console.warn('Skipped',item.title,er.message)}
}
setStatus('done','Files saved!');
}catch(er){if(er.name!=='AbortError')showErr('Folder save failed: '+er.message)}
}
/* ── HELPERS ────────────────────────── */
function san(s){return (s||'file').replace(/[^a-zA-Z0-9_\-]/g,'_').slice(0,40)}
function enc(s){return encodeURIComponent(s||'')}
function esc(s){return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
function fmtDur(s){const m=Math.floor(s/60);return m+':'+(s%60<10?'0':'')+Math.round(s%60)}
function addRecent(q){recent=recent.filter(r=>r!==q);recent.unshift(q);recent=recent.slice(0,8);localStorage.setItem('adl_recent',JSON.stringify(recent));renderRecent()}
function renderRecent(){
const c=document.getElementById('recentTags');c.innerHTML='';
recent.slice(0,6).forEach(q=>{const p=document.createElement('span');p.className='pill';p.textContent=q.length>30?q.slice(0,28)+'…':q;p.onclick=()=>{document.getElementById('searchInput').value=q;runSearch()};c.appendChild(p)});
}
loadKeys();renderRecent();
document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Enter'&&(e.ctrlKey||e.metaKey))runSearch()});
['keyUnsplash','keyPexels','keyPixabay','keyFreesound'].forEach(k=>document.getElementById(k).addEventListener('change',saveKeys));
</script>
</body>
</html>
Copy the code above into a file named asset_downloader.html, or grab the file directly if you downloaded it from this post.
What’s Next
Planning to add a few more free, keyless sources over time, plus better duplicate detection across sources. If you have a favorite free asset API that doesn’t require sign-up, let me know in the comments.
Leave a Reply