Enhanced audio downloader + auto-signup for Brain.fm with ID3 metadata embedding
// ==UserScript==
// @name Brain.fm Automator
// @description Enhanced audio downloader + auto-signup for Brain.fm with ID3 metadata embedding
// @author Soul
// @version 3.1.0
// @match https://my.brain.fm/*
// @match https://my.brain.fm/player*
// @match https://brain.fm/*
// @grant GM_download
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// @namespace https://greasyfork.org/users/1597689
// ==/UserScript==
(function () {
'use strict';
// ===== CONFIGURATION =====
const CONFIG = {
WIDGET_ID: 'brainfm-unified-widget-fixed',
POS_KEY: 'brainfm_widget_pos',
META_KEY: 'brainfm_meta_enabled',
DRAG_THRESHOLD: 3,
OBSERVER_DEBOUNCE: 250,
DOWNLOAD_TIMEOUT: 120000,
STATE_CHECK_INTERVAL: 800,
MAX_RETRY_ATTEMPTS: 3,
METADATA_ENABLED: true, // Default: embed metadata
SELECTORS: {
trackTitle: '[data-testid="currentTrackTitle"]',
trackGenre: '[data-testid="trackGenre"]',
trackArt: "[data-testid='currentTrackInformationCard'] img.sc-6e0b0444-1",
profileTab: '[data-testid="sideDeckProfileTab"]',
audio: 'audio',
source: 'source[src]'
}
};
// ===== UTILITIES =====
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
const sleep = ms => new Promise(r => setTimeout(r, ms));
const log = (...a) => console.log('%c[BrainFM]', 'color:#7df', ...a);
const warn = (...a) => console.warn('%c[BrainFM]', 'color:#fa3', ...a);
// Safe storage wrapper
const storage = {
get: (key, fallback) => {
try {
const raw = typeof GM_getValue === 'function'
? GM_getValue(key)
: localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch { return fallback; }
},
set: (key, val) => {
try {
const str = JSON.stringify(val);
typeof GM_setValue === 'function'
? GM_setValue(key, val)
: localStorage.setItem(key, str);
} catch (e) { warn('Storage error:', e); }
}
};
const debounce = (fn, wait) => {
let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn.apply(null, a), wait); };
};
// ===== ID3 METADATA EMBEDDER (Minimal MP3 Support) =====
const ID3Embedder = (() => {
const writeID3 = (audioBuffer, metadata) => {
const { title, artist, genre, coverBlob } = metadata;
const processCover = async (blob) => {
if (!blob) return null;
const img = new Image();
img.src = URL.createObjectURL(blob);
await new Promise(r => img.onload = r);
const maxDim = 600, quality = 0.85;
let { width, height } = img;
if (width > maxDim || height > maxDim) {
const ratio = Math.min(maxDim / width, maxDim / height);
width *= ratio; height *= ratio;
}
const canvas = Object.assign(document.createElement('canvas'), { width, height });
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
return new Promise(resolve => {
canvas.toBlob(resolve, 'image/jpeg', quality);
URL.revokeObjectURL(img.src);
});
};
const createTextFrame = (id, text) => {
if (!text) return null;
const encoder = new TextEncoder();
const encoded = encoder.encode(text);
const frameSize = encoded.length + 1;
return {
id,
data: new Uint8Array([0x00, ...encoded]),
size: frameSize
};
};
const createAPICFrame = (coverBuffer) => {
if (!coverBuffer) return null;
const encoder = new TextEncoder();
const mimeType = encoder.encode('image/jpeg\0');
const pictureType = new Uint8Array([0x03]);
const description = encoder.encode('\0');
const frameData = new Uint8Array(
1 + mimeType.length + 1 + pictureType.length + description.length + coverBuffer.length
);
let offset = 0;
frameData[offset++] = 0x00;
frameData.set(mimeType, offset); offset += mimeType.length;
frameData.set(pictureType, offset); offset += pictureType.length;
frameData.set(description, offset); offset += description.length;
frameData.set(coverBuffer, offset);
return { id: 'APIC', data: frameData, size: frameData.length };
};
const encodeFrame = (frame) => {
if (!frame) return null;
const header = new Uint8Array(10);
const encoder = new TextEncoder();
encoder.encodeInto(frame.id, header);
header[4] = (frame.size >> 24) & 0xFF;
header[5] = (frame.size >> 16) & 0xFF;
header[6] = (frame.size >> 8) & 0xFF;
header[7] = frame.size & 0xFF;
header[8] = 0x00; header[9] = 0x00;
return new Uint8Array([...header, ...frame.data]);
};
/* eslint-disable no-async-promise-executor */
/* eslint-disable no-unused-vars */
return new Promise(async (resolve, reject) => {
try {
const frames = [];
const titleFrame = createTextFrame('TIT2', title);
const artistFrame = createTextFrame('TPE1', artist || 'Brain.fm');
const genreFrame = createTextFrame('TCON', genre || 'Ambient');
if (titleFrame) frames.push(encodeFrame(titleFrame));
if (artistFrame) frames.push(encodeFrame(artistFrame));
if (genreFrame) frames.push(encodeFrame(genreFrame));
if (coverBlob) {
const processedCover = await processCover(coverBlob);
if (processedCover) {
const coverBuffer = new Uint8Array(await processedCover.arrayBuffer());
const apicFrame = createAPICFrame(coverBuffer);
if (apicFrame) frames.push(encodeFrame(apicFrame));
}
}
if (frames.length === 0) return resolve(audioBuffer);
const tagSize = frames.reduce((sum, f) => sum + f.length, 0) + 10;
const tagHeader = new Uint8Array(10);
tagHeader.set([0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00]);
tagHeader[6] = (tagSize >> 21) & 0x7F;
tagHeader[7] = (tagSize >> 14) & 0x7F;
tagHeader[8] = (tagSize >> 7) & 0x7F;
tagHeader[9] = tagSize & 0x7F;
const newBuffer = new Uint8Array(tagSize + audioBuffer.byteLength);
let offset = 0;
newBuffer.set(tagHeader, offset); offset += 10;
for (const frame of frames) {
newBuffer.set(frame, offset); offset += frame.length;
}
newBuffer.set(new Uint8Array(audioBuffer), offset);
resolve(newBuffer.buffer);
} catch (e) {
warn('ID3 embedding failed:', e);
resolve(audioBuffer);
}
});
};
return { writeID3 };
})();
// ===== STYLES =====
const createStyles = () => {
const css = `
#${CONFIG.WIDGET_ID} {
position: fixed; top: 20px; right: 20px; z-index: 2147483647;
display: flex; flex-direction: column; gap: 8px;
padding: 12px 14px; border-radius: 16px;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.08);
backdrop-filter: blur(30px) saturate(180%) brightness(1.25);
-webkit-backdrop-filter: blur(30px) saturate(180%) brightness(1.25);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.35), 0 8px 32px rgba(0,0,0,0.35);
color: #fff; font: 13px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
cursor: grab; user-select: none; min-width: 190px; max-width: 44vw;
transition: all 0.25s ease; will-change: transform;
}
#${CONFIG.WIDGET_ID}:hover {
background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.4);
box-shadow: inset 0 0 0 1.2px rgba(255,255,255,0.5), 0 10px 36px rgba(0,0,0,0.45);
transform: translateY(-1px);
}
#${CONFIG.WIDGET_ID}.dragging { cursor: grabbing; transition: none; }
#${CONFIG.WIDGET_ID}.error { animation: errflash 0.9s ease; }
#${CONFIG.WIDGET_ID} .row { display: flex; gap: 8px; align-items: center; }
#${CONFIG.WIDGET_ID} .primary-btn {
all: unset; cursor: pointer; flex: 1; padding: 8px 12px; text-align: center;
border-radius: 10px; background: linear-gradient(145deg, rgba(255,255,255,0.25), rgba(255,255,255,0.05));
border: 1px solid rgba(255,255,255,0.35); color: #fff; font-weight: 600;
backdrop-filter: blur(16px); transition: all 0.2s ease;
}
#${CONFIG.WIDGET_ID} .primary-btn:hover:not([disabled]) {
background: linear-gradient(145deg, rgba(255,255,255,0.4), rgba(255,255,255,0.1));
transform: translateY(-1px); box-shadow: 0 4px 14px rgba(0,0,0,0.35);
}
#${CONFIG.WIDGET_ID} .primary-btn[disabled] { opacity: 0.6; cursor: not-allowed; }
#${CONFIG.WIDGET_ID} .meta {
font-size: 12px; opacity: 0.9; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
#${CONFIG.WIDGET_ID} .icon-btn {
all: unset; width: 36px; height: 36px; display: inline-flex;
align-items: center; justify-content: center; border-radius: 10px;
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.25);
color: #fff; cursor: pointer; font-weight: 700; transition: all 0.2s ease;
}
#${CONFIG.WIDGET_ID} .icon-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.08); }
#${CONFIG.WIDGET_ID} .settings-row {
justify-content: space-between; font-size: 11px; opacity: 0.85;
display: flex; align-items: center; gap: 6px;
}
#${CONFIG.WIDGET_ID} .settings-row label {
display: flex; align-items: center; gap: 6px; cursor: pointer;
}
#${CONFIG.WIDGET_ID} .settings-row input[type="checkbox"] {
cursor: pointer; accent-color: #7df;
}
@keyframes errflash {
0%,100% { box-shadow: 0 8px 32px rgba(0,0,0,0.35); }
15% { box-shadow: 0 0 0 3px rgba(255,0,0,0.55); }
}
@media (prefers-reduced-motion: reduce) {
#${CONFIG.WIDGET_ID}, #${CONFIG.WIDGET_ID} * { transition: none !important; animation: none !important; }
}
`;
if (typeof GM_addStyle === 'function') GM_addStyle(css);
else { const s = document.createElement('style'); s.textContent = css; document.head.appendChild(s); }
};
// ===== WIDGET MANAGER =====
const Widget = (() => {
let el, state = { downloading: false, dragging: false, audioSrc: null };
const create = () => {
if (document.getElementById(CONFIG.WIDGET_ID)) return;
el = document.createElement('div');
el.id = CONFIG.WIDGET_ID;
el.innerHTML = `
<div class="meta" id="meta-display">Initializing...</div>
<div class="row">
<button class="primary-btn" id="btn-download">🎧 Download Audio</button>
<button class="icon-btn" id="btn-refresh" title="Refresh">↻</button>
</div>
<div class="row">
<button class="primary-btn" id="btn-auto">New Acc</button>
`;
document.body.appendChild(el);
return el;
};
const restorePosition = () => {
const pos = storage.get(CONFIG.POS_KEY, null);
if (!pos?.left || !pos?.top) return;
const { left, top } = pos;
const maxL = window.innerWidth - 200, maxT = window.innerHeight - 100;
el.style.cssText += `left:${Math.max(4, Math.min(left, maxL))}px;top:${Math.max(4, Math.min(top, maxT))}px;right:auto;bottom:auto;`;
};
const savePosition = () => {
const r = el.getBoundingClientRect();
storage.set(CONFIG.POS_KEY, { left: r.left, top: r.top });
};
const setupDrag = () => {
let startX, startY, origX, origY, moved = false;
const onDown = (e) => {
if (e.button !== 0 || e.target.closest('button')) return;
e.preventDefault();
const rect = el.getBoundingClientRect();
startX = e.clientX; startY = e.clientY;
origX = rect.left; origY = rect.top;
moved = false;
state.dragging = true;
el.classList.add('dragging');
el.setPointerCapture?.(e.pointerId);
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerup', onUp);
};
const onMove = (e) => {
if (!state.dragging) return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (!moved && (Math.abs(dx) > CONFIG.DRAG_THRESHOLD || Math.abs(dy) > CONFIG.DRAG_THRESHOLD)) {
moved = true; el.style.transition = 'none';
}
if (!moved) return;
const maxL = window.innerWidth - el.offsetWidth - 4;
const maxT = window.innerHeight - el.offsetHeight - 4;
el.style.left = `${Math.max(4, Math.min(origX + dx, maxL))}px`;
el.style.top = `${Math.max(4, Math.min(origY + dy, maxT))}px`;
el.style.right = 'auto'; el.style.bottom = 'auto';
};
const onUp = (e) => {
if (!state.dragging) return;
state.dragging = false;
el.classList.remove('dragging');
el.style.transition = '';
el.releasePointerCapture?.(e.pointerId);
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
if (moved) savePosition();
};
el.addEventListener('pointerdown', onDown);
el.addEventListener('dragstart', e => e.preventDefault());
};
const updateState = (info) => {
const meta = $('#meta-display', el);
const btn = $('#btn-download', el);
if (!meta || !btn) return;
if (info?.title) {
const display = info.title.length > 30 ? info.title.slice(0, 27) + '...' : info.title;
meta.textContent = display;
meta.title = info.title;
}
btn.disabled = state.downloading || !info?.src;
btn.textContent = state.downloading ? '⏳ Processing...' : '🎧 Download Audio';
};
const flashError = () => {
el.classList.add('error');
setTimeout(() => el.classList.remove('error'), 900);
};
return { create, restorePosition, setupDrag, updateState, flashError, getEl: () => el, getState: () => state, setState: (s) => Object.assign(state, s) };
})();
// ===== AUDIO HANDLER =====
const decodeTrackName = (str) => {
if (!str) return '';
return str
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/_/g, ' ')
.replace(/\s+/g, ' ')
.trim();
};
const AudioHandler = (() => {
const listened = new WeakSet();
let lastValidInfo = null;
let currentPageType = null;
const sanitize = (name) => (name || 'audio').replace(/[/\\?%*:|"<>]/g, '_').trim() || 'audio';
const getExt = (url, ct) => {
const urlExt = url?.match(/\.(\w+)(?:[?#]|$)/i)?.[1]?.toLowerCase();
if (urlExt && ['mp3','wav','ogg','m4a','aac','flac','opus'].includes(urlExt)) return urlExt;
const ctMap = {
'audio/mpeg':'mp3','audio/mp3':'mp3','audio/ogg':'ogg','audio/opus':'opus',
'audio/aac':'aac','audio/wav':'wav','audio/x-wav':'wav','audio/flac':'flac',
'audio/mp4':'m4a','audio/m4a':'m4a'
};
return ctMap[ct?.toLowerCase()] || 'mp3';
};
/* eslint-disable no-empty */
const parseCD = (cd) => {
if (!cd) return '';
const m1 = cd.match(/filename\*\s*=\s*[^']*'[^']*'([^;]+)/i);
if (m1) { try { return decodeURIComponent(m1[1]); } catch {} }
const m2 = cd.match(/filename\s*=\s*"?([^";]+)"?/i);
return m2?.[1] || '';
};
const getHeader = (headers, key) => {
if (!headers) return '';
const k = key.toLowerCase();
return headers.split(/\r?\n/).find(l => l.toLowerCase().startsWith(k + ':'))?.split(':', 2)[1]?.trim() || '';
};
// 🎨 NEW: Extract track cover art URL
const getTrackArtUrl = () => {
// Primary: Player page card image
const cardImg = $(CONFIG.SELECTORS.trackArt);
if (cardImg?.src && cardImg.src.includes('unsplash.com')) {
return cardImg.src.replace('&w=1080', '&w=1440'); // Higher res
}
// Fallback: Mini-player on homepage (styled-components class)
const miniImg = $('.bMnsXL ~ div img') || $('[class*="currentTrack"] img');
if (miniImg?.src) return miniImg.src;
return null;
};
const getTrackInfo = () => {
let title = null, genre = null;
if (location.pathname.startsWith('/player')) {
title = $("[data-testid='currentTrackTitle']")?.textContent?.trim();
genre = $("[data-testid='trackGenre']")?.textContent?.trim();
}
if (!title) {
const titleEl = $('.bMnsXL');
const genreEl = $('[class*="gustOw"][color]');
if (titleEl?.textContent?.trim()) title = titleEl.textContent.trim();
if (genreEl?.textContent?.trim()) genre = genreEl.textContent.trim();
}
if (!title) {
const audio = $$('audio').find(a => a.src || a.currentSrc);
if (audio) {
const src = audio.src || audio.currentSrc;
const filename = src?.split('/').pop()?.split('?')[0] || '';
if (filename) {
const parts = filename.replace('.mp3', '').split('_');
if (parts.length >= 2) {
title = parts[0].replace(/([a-z])([A-Z])/g, '$1 $2').trim();
const possibleGenre = parts.find(p =>
!/^\d+$/.test(p) && !p.includes('BPM') && !p.includes('VPR')
);
if (possibleGenre && possibleGenre !== parts[0]) {
genre = possibleGenre.charAt(0).toUpperCase() + possibleGenre.slice(1);
}
}
}
}
}
if (title) return { title: genre ? `${title} - ${genre}` : title, genre };
return null;
};
const findAudio = () => {
const candidates = $$('audio').map(audio => {
let src = audio.src || audio.currentSrc;
if (!src) {
const s = $('source[src]', audio);
if (s) src = s.src || s.getAttribute('src');
}
return src ? { audio, src: new URL(src, location.href).href } : null;
}).filter(Boolean);
const playing = candidates.find(c => !c.audio.paused && c.audio.currentTime > 0);
return playing || candidates[0] || null;
};
const attachListeners = (audio) => {
if (listened.has(audio)) return;
listened.add(audio);
const update = debounce(() => {
const info = buildAudioInfo();
if (info) Widget.updateState(info);
}, 100);
['loadedmetadata','canplay','play','pause','ended','emptied','stalled','suspend','error'].forEach(ev =>
audio.addEventListener(ev, update, { passive: true })
);
$$('source[src]', audio).forEach(s =>
['load','error'].forEach(ev => s.addEventListener(ev, update, { passive: true }))
);
};
const buildAudioInfo = () => {
const item = findAudio();
if (!item) {
if (lastValidInfo) return { ...lastValidInfo, title: lastValidInfo.title + ' (paused)' };
return null;
}
attachListeners(item.audio);
const track = getTrackInfo();
const ext = getExt(item.src);
const urlBase = item.src.split('/').pop()?.split('?')[0] || 'audio';
const base = sanitize(track?.title || decodeTrackName(urlBase.replace(/\.[^.]+$/, '')));
const filename = `${base}.${ext}`;
const info = {
src: item.src,
title: track?.title || decodeTrackName(urlBase),
filename,
genre: track?.genre,
coverUrl: getTrackArtUrl() // 🎨 Include cover URL
};
if (track?.title) lastValidInfo = info;
return info;
};
const scan = debounce(() => {
const info = buildAudioInfo();
if (info) {
Widget.updateState(info);
} else if (!lastValidInfo) {
Widget.updateState({ title: 'No audio detected' });
}
}, CONFIG.OBSERVER_DEBOUNCE);
const initObserver = () => {
const mo = new MutationObserver(scan);
mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] });
let lastURL = location.href;
const urlObserver = setInterval(() => {
if (location.href !== lastURL) {
lastURL = location.href;
currentPageType = null;
scan();
}
}, 500);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') scan();
});
window.addEventListener('beforeunload', () => clearInterval(urlObserver));
return scan;
};
return { buildAudioInfo, initObserver, getExt, sanitize, parseCD, getHeader, getTrackArtUrl, getTrackInfo };
})();
// ===== DOWNLOADER =====
const Downloader = (() => {
const gmXhr = (opts) => {
if (typeof GM_xmlhttpRequest === 'function') return GM_xmlhttpRequest(opts);
if (typeof GM?.xmlHttpRequest === 'function') return GM.xmlHttpRequest(opts);
throw new Error('GM_xhr unavailable');
};
const downloadBlob = (blob, filename, type) => {
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), { href: url, download: filename, rel: 'noopener' });
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 100);
};
// 🎨 UPDATED: viaGMXhr now accepts metadata parameter
const viaGMXhr = async (url, filename, metadata = {}) => {
return new Promise((resolve, reject) => {
gmXhr({
method: 'GET', url, responseType: 'arraybuffer',
headers: { Referer: location.href, Accept: 'audio/*', 'Cache-Control': 'no-cache' },
timeout: CONFIG.DOWNLOAD_TIMEOUT,
onload: async (r) => {
if (!r.response) return reject('Empty response');
let blob = new Blob([new Uint8Array(r.response)], {
type: AudioHandler.getHeader(r.responseHeaders, 'content-type') || 'audio/mpeg'
});
// 🎨 Embed metadata if enabled, MP3 format, and metadata provided
const isMp3 = filename.toLowerCase().endsWith('.mp3');
if (CONFIG.METADATA_ENABLED && isMp3 && (metadata.title || metadata.coverUrl)) {
try {
let coverBlob = null;
// Fetch cover art if URL provided
if (metadata.coverUrl) {
coverBlob = await new Promise((res, rej) => {
gmXhr({
url: metadata.coverUrl,
responseType: 'blob',
headers: { Referer: location.href },
onload: (imgResp) => res(imgResp.response),
onerror: rej,
timeout: 15000
});
});
log('🖼️ Cover art fetched');
}
// Embed ID3 tags
const taggedBuffer = await ID3Embedder.writeID3(r.response, {
title: metadata.title,
artist: 'Brain.fm',
genre: metadata.genre,
coverBlob
});
blob = new Blob([taggedBuffer], { type: 'audio/mpeg' });
log('✅ Metadata embedded successfully');
} catch (e) {
warn('⚠️ Metadata embedding skipped:', e);
// Continue with original blob - don't fail the download
}
}
downloadBlob(blob, filename, blob.type);
resolve();
},
onerror: reject, ontimeout: reject
});
});
};
const viaGMDownload = (url, filename, metadata = {}) => new Promise((resolve, reject) => {
if (typeof GM_download !== 'function') return reject('GM_download unavailable');
// Note: GM_download doesn't support post-processing, so metadata won't be embedded
GM_download({
url, name: filename, saveAs: true,
onload: resolve, onerror: reject, ontimeout: reject, timeout: CONFIG.DOWNLOAD_TIMEOUT
});
});
const viaAnchor = (url, filename) => {
const a = Object.assign(document.createElement('a'), {
href: url, download: filename, rel: 'noopener', target: '_blank'
});
document.body.appendChild(a); a.click(); a.remove();
};
// 🎨 UPDATED: download now accepts metadata parameter
const download = async (url, filename, metadata = {}) => {
// Try methods in order of reliability
// Prefer GM_xhr for metadata embedding support
try { await viaGMXhr(url, filename, metadata); return true; } catch {}
try { await viaGMDownload(url, filename, metadata); return true; } catch {}
viaAnchor(url, filename); return true; // Fallback (no metadata)
};
return { download };
})();
// ===== SIGNUP AUTOMATION =====
const AutoSignup = (() => {
const find = (textRe, tag = '*') =>
$$(tag).find(el => textRe.test(el.textContent?.trim() || el.getAttribute('aria-label') || ''));
const fillInput = (sel, val) => {
const el = $(sel); if (!el) return false;
const proto = Object.getPrototypeOf(el);
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
if (desc?.set) desc.set.call(el, val); else el.value = val;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
return true;
};
const clickEl = async (el) => {
if (!el) return false;
el.scrollIntoView({ behavior: 'auto', block: 'center' });
await sleep(200);
['mouseover','mousedown','mouseup','click'].forEach(ev =>
el.dispatchEvent(new MouseEvent(ev, { bubbles: true, cancelable: true }))
);
return true;
};
const waitFor = async (sel, timeout = 10000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = $(sel); if (el) return el;
await sleep(300);
}
return null;
};
const handleConfirmationModal = async () => {
const modal = await waitFor('[role="dialog"], .modal, [class*="modal"]', 3000);
if (!modal) return false;
const origConfirm = window.confirm;
window.confirm = () => true;
const okBtn = $$('button', modal).find(btn =>
/^(ok|yes|confirm|log out|logout)$/i.test(btn.textContent?.trim() || '')
);
if (okBtn) {
await clickEl(okBtn);
await sleep(500);
window.confirm = origConfirm;
return true;
}
const firstBtn = $('button', modal);
if (firstBtn && !/cancel|no|back/i.test(firstBtn.textContent?.trim() || '')) {
await clickEl(firstBtn);
await sleep(500);
window.confirm = origConfirm;
return true;
}
window.confirm = origConfirm;
return false;
};
const logoutFlow = async () => {
const openBtn = find(/open/i); if (!openBtn) return false;
await clickEl(openBtn); await sleep(1500);
const profile = await waitFor(CONFIG.SELECTORS.profileTab); if (!profile) return false;
await clickEl(profile); await sleep(2000);
const logout = $$('button,div,span').find(el =>
el.textContent?.trim().toLowerCase() === 'logout' && el.offsetParent
);
if (logout) {
await clickEl(logout);
await sleep(500);
await handleConfirmationModal();
await sleep(1500);
return true;
}
return false;
};
const signupFlow = async () => {
if (!await waitFor('input[type="email"]', 15000)) { warn('Signup form timeout'); return; }
const ts = Date.now(), rand = Math.random().toString(36).slice(2,7);
const creds = {
email: `user_${ts}@autotest.com`,
username: `user_${rand}`,
password: Math.random().toString(36).slice(2,10) + '!'
};
fillInput('input[id*="name"],input[name*="name"]', creds.username);
fillInput('input[id*="email"],input[name*="email"]', creds.email);
fillInput('input[id*="password"],input[name*="password"]', creds.password);
log('📝 Filled:', creds.email);
const submit = find(/create|sign\s*up/i, 'button');
if (submit) { await clickEl(submit); log('✅ Submitted'); }
else { warn('⚠️ Submit button not found'); }
const check = setInterval(() => {
if (location.href.includes('/welcome')) {
clearInterval(check);
const btn = $('#btn-auto'); if (btn) { btn.style.background = '#00c851'; btn.textContent = '✓ Success'; }
log('🎉 Signup complete');
}
}, 800);
setTimeout(() => clearInterval(check), 30000);
};
const run = async () => {
const btn = Widget.getEl()?.querySelector('#btn-auto');
if (!btn) return;
btn.disabled = true; btn.style.opacity = '0.7';
try {
if (location.href.includes('/welcome')) {
btn.style.background = '#00c851'; btn.textContent = '✓ Active';
log('✅ Already on welcome page');
return;
}
const didLogout = await logoutFlow();
if (didLogout) await sleep(2000);
await signupFlow();
} catch (e) { warn('Auto-signup error:', e); }
finally {
btn.disabled = false; btn.style.opacity = '1';
if (!location.href.includes('/welcome')) btn.textContent = 'New Acc';
}
};
return { run };
})();
// ===== INITIALIZATION =====
const init = () => {
// Restore metadata toggle preference
CONFIG.METADATA_ENABLED = storage.get(CONFIG.META_KEY, true);
createStyles();
Widget.create();
Widget.restorePosition();
Widget.setupDrag();
// 🎨 Setup metadata toggle
const metaToggle = $('#meta-toggle');
if (metaToggle) {
metaToggle.checked = CONFIG.METADATA_ENABLED;
metaToggle.addEventListener('change', (e) => {
CONFIG.METADATA_ENABLED = e.target.checked;
storage.set(CONFIG.META_KEY, CONFIG.METADATA_ENABLED);
log(`🎨 Metadata embedding: ${CONFIG.METADATA_ENABLED ? 'enabled' : 'disabled'}`);
});
}
// Button handlers
$('#btn-download')?.addEventListener('click', async () => {
const { downloading } = Widget.getState();
if (downloading) return;
const info = AudioHandler.buildAudioInfo();
if (!info?.src) { Widget.flashError(); return; }
// 🎨 Prepare metadata for embedding
const trackInfo = AudioHandler.getTrackInfo() || {};
const metadata = {
title: trackInfo.title,
genre: trackInfo.genre,
coverUrl: info.coverUrl
};
Widget.setState({ downloading: true });
const originalTitle = info.title;
Widget.updateState({ ...info, title: CONFIG.METADATA_ENABLED ? '⏳ Embedding...' : '⏳ Downloading...' });
try {
await Downloader.download(info.src, info.filename, metadata);
Widget.updateState({ ...info, title: CONFIG.METADATA_ENABLED ? '✓ Saved + Art!' : '✓ Saved!' });
setTimeout(() => Widget.updateState({ ...info, title: originalTitle }), 1500);
} catch (e) {
warn('Download failed:', e);
Widget.flashError();
Widget.updateState({ ...info, title: '✗ Failed' });
setTimeout(() => Widget.updateState({ ...info, title: originalTitle }), 1500);
} finally {
Widget.setState({ downloading: false });
}
});
$('#btn-refresh')?.addEventListener('click', (e) => { e.stopPropagation(); AudioHandler.initObserver()(); });
$('#btn-auto')?.addEventListener('click', (e) => { e.stopPropagation(); AutoSignup.run(); });
// Initial scan + observer
AudioHandler.initObserver()();
setTimeout(() => Widget.updateState(AudioHandler.buildAudioInfo()), CONFIG.STATE_CHECK_INTERVAL);
log(`✅ Widget initialized v3.1.0 | Metadata: ${CONFIG.METADATA_ENABLED ? 'ON' : 'OFF'}`);
};
// Start when DOM ready
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();