您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mass-edit gradient for album pages on Genius
// ==UserScript== // @name Genius Gradient Assistant // @namespace https://genius.com/ // @version 2.387 // @description Mass-edit gradient for album pages on Genius // @author thousandeyes // @match *://genius.com/*-lyrics // @match *://genius.com/*-lyrics?* // @icon https://imgur.com/qgv8m0o.png // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function() { 'use strict'; GM_addStyle(` #genius-gradient-batch-ui { position: fixed; bottom: 16px; right: 16px; background: #fff; padding: 16px; border: 1px solid #e0e0e0; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999999; width: 360px; font-family: 'Programme', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; } #genius-gradient-batch-ui input[type="text"] { width: 100%; padding: 8px; margin-bottom: 6px; border: 1px solid #ccc; border-radius: 8px; background: #fff; color: #000; } #genius-gradient-batch-ui div { color: #333; } #genius-gradient-batch-ui button { padding: 10px; border: 1px solid #000000; border-radius: 10px; background: #f5f5f5; color: #000; cursor: pointer; font-family: 'Programme', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; } #genius-gradient-batch-ui button[aria-label="Close"] { background: transparent; border: none; color: #333; font-size: 16px; } #genius-gradient-batch-ui div[style*="max-height: 240px"] { background: #fafafa; border-radius: 8px; padding: 8px; overflow-y: auto; } #genius-gradient-batch-ui div[style*="height: 6px"] { background: #e0e0e0; } #genius-gradient-batch-ui div[style*="height: 6px"] > div { background: #ffd700; } #gradient-assistant-toggle { position: fixed; bottom: 16px; right: 16px; padding: 10px 16px; border: 1px solid #ccc; border-radius: 8px; background: #f5f5f5; color: #000; cursor: pointer; font-family: 'Programme', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 500; z-index: 999998; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } #gradient-assistant-toggle:hover { background: #e0e0e0; } `); function backgroundHandler(request) { if (request.type === "getCookie") { return new Promise((resolve) => { const token = getCsrf(); resolve(token || null); }); } } (function installNetworkHexTap() { const HEX6 = /^#?[0-9a-fA-F]{6}$/; const wantField = (k) => /song_art_primary_color|song_art_secondary_color/i.test(k); function normalizeHex6(s) { if (!s) return null; const m = String(s).match(/[0-9a-fA-F]{6}/); if (m) return "#" + m[0].toUpperCase(); const rgbMatch = String(s).match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i); if (rgbMatch) { const r = Math.min(255, parseInt(rgbMatch[1], 10)); const g = Math.min(255, parseInt(rgbMatch[2], 10)); const b = Math.min(255, parseInt(rgbMatch[3], 10)); return "#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("").toUpperCase(); } return null; } function pullFromObject(obj) { if (!obj || typeof obj !== "object") return null; let out = {}; for (const k of Object.keys(obj)) { const v = obj[k]; if (wantField(k) && typeof v === "string") { const hex = normalizeHex6(v); if (hex) out[k] = hex; } } return Object.keys(out).length ? out : null; } function pullFromUrlEncoded(str) { try { const p = new URLSearchParams(str); let out = {}; for (const [k, v] of p.entries()) { if (wantField(k)) { const hex = normalizeHex6(v); if (hex) out[k] = hex; } } return Object.keys(out).length ? out : null; } catch (e) { return null; } } function maybeEmit(found, context) { if (!found) return; const colors = { primary: found.song_art_primary_color || found.primary, secondary: found.song_art_secondary_color || found.secondary }; if (!HEX6.test(colors.primary)) colors.primary = null; if (!HEX6.test(colors.secondary)) colors.secondary = null; if (colors.primary || colors.secondary) { window.__lastSongArtHex = colors; window.dispatchEvent(new CustomEvent("song-art-hex", { detail: colors, bubbles: false })); console.log("[HEX tap]", context, colors); } } const _fetch = window.fetch; window.fetch = async function(input, init = {}) { try { let body = init && init.body; if (body) { if (typeof body === "string") { let found = null; if (body.trim().startsWith("{")) { try { found = pullFromObject(JSON.parse(body)); } catch {} } if (!found) found = pullFromUrlEncoded(body); maybeEmit(found, "fetch:string-body"); } else if (body instanceof FormData) { const obj = {}; for (const [k, v] of body.entries()) obj[k] = v; maybeEmit(pullFromObject(obj), "fetch:formdata"); } } } catch (e) { console.warn("HEX tap(fetch) error:", e); } return _fetch.apply(this, arguments); }; const _open = XMLHttpRequest.prototype.open; const _send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this.__hexTapUrl = url; this.__hexTapMethod = method; return _open.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { try { if (typeof body === "string") { let found = null; if (body.trim().startsWith("{")) { try { found = pullFromObject(JSON.parse(body)); } catch {} } if (!found) found = pullFromUrlEncoded(body); maybeEmit(found, "xhr:string-body"); } else if (body instanceof FormData) { const obj = {}; for (const [k, v] of body.entries()) obj[k] = v; maybeEmit(pullFromObject(obj), "xhr:formdata"); } } catch (e) { console.warn("HEX tap(xhr) error:", e); } return _send.apply(this, arguments); }; })(); const uiId = "genius-gradient-batch-ui"; function createUI() { if (document.getElementById(uiId)) return; const ui = document.createElement("div"); ui.id = uiId; ui.style.position = "fixed"; ui.style.bottom = "16px"; ui.style.right = "16px"; ui.style.zIndex = "999999"; ui.style.background = "#fff"; ui.style.borderRadius = "12px"; ui.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; ui.style.padding = "16px"; ui.style.width = "360px"; ui.style.display = "grid"; ui.style.gap = "12px"; ui.style.fontFamily = "'Programme', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif"; const title = document.createElement("div"); title.textContent = "Gradient Assistant"; title.style.fontWeight = "600"; title.style.fontSize = "16px"; title.style.color = "#000"; const copySection = document.createElement("div"); copySection.style.display = "grid"; copySection.style.gap = "6px"; const copyLabel = document.createElement("div"); copyLabel.textContent = "Current Gradient:"; copyLabel.style.fontSize = "12px"; copyLabel.style.color = "#333"; const copyBtn = document.createElement("button"); copyBtn.textContent = "Copy Gradient"; copyBtn.style.padding = "8px"; copyBtn.style.background = "#f5f5f5"; copyBtn.style.color = "#000"; copyBtn.style.border = "1px solid #ccc"; copyBtn.style.borderRadius = "8px"; copyBtn.style.cursor = "pointer"; copyBtn.style.fontWeight = "500"; copySection.appendChild(copyLabel); copySection.appendChild(copyBtn); const pasteSection = document.createElement("div"); pasteSection.style.display = "grid"; pasteSection.style.gap = "6px"; const pasteLabel = document.createElement("div"); pasteLabel.textContent = "Paste Gradient:"; pasteLabel.style.fontSize = "12px"; pasteLabel.style.color = "#333"; const gradientInput = document.createElement("input"); gradientInput.type = "text"; gradientInput.id = "gradient-input"; gradientInput.placeholder = "linear-gradient(...)"; gradientInput.style.width = "100%"; gradientInput.style.padding = "8px"; gradientInput.style.borderRadius = "8px"; gradientInput.style.border = "1px solid #ccc"; gradientInput.style.background = "#fff"; gradientInput.style.color = "#000"; pasteSection.appendChild(pasteLabel); pasteSection.appendChild(gradientInput); const controls = document.createElement("div"); controls.style.display = "grid"; controls.style.gridTemplateColumns = "1fr 1fr 1fr 1fr"; controls.style.gap = "8px"; function smallBtn(txt) { const b = document.createElement("button"); b.textContent = txt; b.style.height = "50px"; b.style.border = "1px solid #ccc"; b.style.borderRadius = "8px"; b.style.fontWeight = "500"; b.style.cursor = "pointer"; b.style.background = "#f5f5f5"; b.style.color = "#000"; b.style.width = "100%"; b.style.boxSizing = "border-box"; return b; } const reloadBtn = smallBtn("Reload tracks"); const allBtn = smallBtn("Select all"); const noneBtn = smallBtn("Deselect all"); const applyBtn = document.createElement("button"); applyBtn.textContent = "Apply to selected"; applyBtn.style.height = "42px"; applyBtn.style.border = "none"; applyBtn.style.borderRadius = "8px"; applyBtn.style.fontWeight = "600"; applyBtn.style.cursor = "pointer"; applyBtn.style.background = "rgba(255, 255, 100, 1)"; applyBtn.style.color = "#000"; applyBtn.style.gridColumn = "span 4"; controls.appendChild(reloadBtn); controls.appendChild(allBtn); controls.appendChild(noneBtn); controls.appendChild(applyBtn); const listWrap = document.createElement("div"); listWrap.style.display = "grid"; listWrap.style.gap = "6px"; const listTitle = document.createElement("div"); listTitle.textContent = "Tracks in this album (pick which to update)"; listTitle.style.fontSize = "12px"; listTitle.style.color = "#333"; const list = document.createElement("div"); list.style.maxHeight = "240px"; list.style.overflow = "auto"; list.style.background = "#fafafa"; list.style.borderRadius = "8px"; list.style.padding = "8px"; listWrap.appendChild(listTitle); listWrap.appendChild(list); const bar = document.createElement("div"); bar.style.height = "6px"; bar.style.background = "#e0e0e0"; bar.style.borderRadius = "999px"; const fill = document.createElement("div"); fill.style.height = "100%"; fill.style.width = "0%"; fill.style.background = "#ffd700"; fill.style.borderRadius = "inherit"; bar.appendChild(fill); const status = document.createElement("div"); status.style.fontSize = "12px"; status.style.color = "#333"; status.textContent = "Ready"; const close = document.createElement("button"); close.textContent = "×"; close.style.position = "absolute"; close.style.top = "8px"; close.style.right = "8px"; close.style.background = "transparent"; close.style.color = "#333"; close.style.border = "none"; close.style.cursor = "pointer"; close.style.fontSize = "16px"; close.setAttribute("aria-label", "Close"); close.onclick = () => ui.remove(); ui.appendChild(close); ui.appendChild(title); ui.appendChild(copySection); ui.appendChild(pasteSection); ui.appendChild(controls); ui.appendChild(listWrap); ui.appendChild(bar); ui.appendChild(status); document.body.appendChild(ui); return { ui, copyBtn, gradientInput, list, status, fill, reloadBtn, allBtn, noneBtn, applyBtn }; } async function findCurrentGradient() { const candidates = [ ...document.querySelectorAll('[class*="header"], [class*="album"], [class*="art"]'), ...document.querySelectorAll('[style*="gradient"], [data-gradient], [data-colors]') ]; for (const el of candidates) { const style = window.getComputedStyle(el); if (style.backgroundImage.includes('gradient')) { return style.backgroundImage; } if (el.dataset.gradient) { return el.dataset.gradient; } if (el.dataset.primaryColor && el.dataset.secondaryColor) { return `linear-gradient(135deg, ${el.dataset.primaryColor}, ${el.dataset.secondaryColor})`; } } const metaGradient = document.querySelector('meta[name="gradient-colors"]'); if (metaGradient) { try { const colors = JSON.parse(metaGradient.content); if (colors.primary && colors.secondary) { return `linear-gradient(135deg, ${colors.primary}, ${colors.secondary})`; } } catch (e) {} } const allElements = document.querySelectorAll('*'); for (const el of allElements) { const style = window.getComputedStyle(el); if (style.backgroundImage.includes('gradient')) { return style.backgroundImage; } } return null; } function parseGradient(gradient) { const colorPat = '(?:#[0-9a-fA-F]{3,6}|\\w+\\([^)]+\\)|[a-zA-Z]+)'; const directionPat = '(?:to\\s+(?:top|bottom|left|right)(?:\\s+(?:top|bottom|left|right))?|\\d+deg)'; const regex = new RegExp(`linear-gradient\\s*\\(\\s*(?:${directionPat}\\s*,)?\\s*(${colorPat})\\s*,\\s*(${colorPat})\\s*\\)`, 'i'); const match = gradient.match(regex); if (match) { const color1 = match[1].trim(); const color2 = match[2].trim(); const primary = normalizeColor(color1); const secondary = normalizeColor(color2); if (primary && secondary) { return { primary, secondary }; } } throw new Error('Invalid format.'); } function normalizeColor(colorStr) { if (!colorStr) return null; if (/^#[0-9a-fA-F]{6}$/.test(colorStr)) { return colorStr.toUpperCase(); } if (/^#[0-9a-fA-F]{3}$/.test(colorStr)) { return '#' + colorStr.slice(1).split('').map(c => c + c).join('').toUpperCase(); } const rgbMatch = colorStr.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i); if (rgbMatch) { const [_, r, g, b] = rgbMatch.map(Number); return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`; } const hslMatch = colorStr.match(/^hsl\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*\)$/i); if (hslMatch) { const [_, h, s, l] = hslMatch.map(Number); return hslToHex(h, s, l); } const namedColors = { 'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF', 'black': '#000000', 'white': '#FFFFFF' }; if (colorStr.toLowerCase() in namedColors) { return namedColors[colorStr.toLowerCase()]; } return null; } function hslToHex(h, s, l) { l /= 100; const a = s * Math.min(l, 1 - l) / 100; const f = n => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color).toString(16).padStart(2, '0'); }; return `#${f(0)}${f(8)}${f(4)}`.toUpperCase(); } function hexToRgb(hex) { hex = hex.replace('#', ''); if (hex.length === 3) { hex = hex.split('').map(c => c + c).join(''); } return { r: parseInt(hex.substring(0, 2), 16), g: parseInt(hex.substring(2, 4), 16), b: parseInt(hex.substring(4, 6), 16) }; } function getLuminance(rgb) { return (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; } function computeTextColor(primary, secondary) { const rgb1 = hexToRgb(primary); const rgb2 = hexToRgb(secondary); const lum1 = getLuminance(rgb1); const lum2 = getLuminance(rgb2); const avgLum = (lum1 + lum2) / 2; return avgLum > 0.5 ? '#000' : '#fff'; } function uniq(a) { return [...new Set(a)]; } function getCsrf() { console.log('[Genius Gradient Assistant] Searching CSRF token...'); const cookie = document.cookie.split("; ").find(x => x.startsWith("_csrf_token=")); if (cookie) { try { const token = decodeURIComponent(cookie.split("=")[1]); console.log(`[Genius Gradient Assistant] CSRF token found: ${token}`); return token; } catch (e) { console.warn('[Genius Gradient Assistant] Error decoding CSRF token:', e); } } console.warn('[Genius Gradient Assistant] CSRF token not found in cookies. Available cookies:', document.cookie.split("; ")); return ""; } function isInsideHotSongs(el) { let n = el; for (let i = 0; i < 8 && n; i++) { if (n.textContent && /^\s*hot songs\s*:?\s*$/i.test(n.textContent.trim())) return true; if (n.getAttribute && /hot[-_\s]?songs/i.test(n.getAttribute("aria-label") || "")) return true; n = n.parentElement; } return false; } function findAlbumTracklistContainer() { const a = document.querySelector('[data-test="album_tracklist"]'); if (a) return a; const c = [...document.querySelectorAll('section,div,ol,ul')].find(n => /album.*tracklist/i.test(n.className) || /tracklist/i.test(n.getAttribute("data-test") || "")); if (c) return c; return null; } function collectAlbumAnchors() { const container = findAlbumTracklistContainer(); let anchors = []; if (container) anchors = [...container.querySelectorAll('a[href*="-lyrics"]')]; if (!anchors.length) { anchors = [...document.querySelectorAll('a[href*="-lyrics"]')].filter(a => !isInsideHotSongs(a)); } anchors = anchors.filter(a => /https?:\/\/genius\.com\/[^?#]+-lyrics/i.test(a.href)); anchors = anchors.filter(a => !isInsideHotSongs(a)); return anchors.map(a => { const txt = (a && a.textContent || "").trim(); let title = txt || decodeURIComponent(a.href.split("/").pop().replace(/-lyrics.*/i, "").replace(/-/g, " ")); return { url: a.href, title }; }); } async function extractSongIdFromHtml(html) { const tries = [/"song"\s*:\s*{[^}]*"id"\s*:\s*(\d+)/i, /"song_id"\s*:\s*(\d+)/i, /data-song-id="(\d+)"/i, /rg_embed_link_(\d+)/i, /"pusher_channel"\s*:\s*"song-(\d+)"/i]; for (const re of tries) { const m = html.match(re); if (m && m[1]) return m[1]; } return null; } async function fetchSongIdByLyricUrl(url) { const res = await fetch(url, { credentials: "include" }); const html = await res.text(); return extractSongIdFromHtml(html); } async function putSongColors(id, primary, secondary, text, csrf) { const payload = { text_format: "html,markdown", song: { song_art_primary_color: primary, song_art_secondary_color: secondary, song_art_text_color: text, valid_song_art_contrast: true } }; const res = await fetch(`https://genius.com/api/songs/${id}`, { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf, "Accept": "*/*" }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error(String(res.status)); return res.json(); } function hex(v) { if (!v) return v; const x = v.trim().toLowerCase(); if (/^#[0-9a-f]{6}$/.test(x)) return x; if (/^#[0-9a-f]{3}$/.test(x)) return "#" + x.slice(1).split("").map(c => c + c).join(""); return x; } const state = { rows: [], data: [] }; function renderList(items, list) { list.innerHTML = ""; state.rows = []; state.data = items; items.forEach((it, idx) => { const row = document.createElement("label"); row.style.display = "grid"; row.style.gridTemplateColumns = "20px 1fr"; row.style.gap = "8px"; row.style.alignItems = "center"; row.style.padding = "6px"; row.style.borderRadius = "8px"; row.style.cursor = "pointer"; row.onmouseenter = () => row.style.background = "rgba(0,0,0,0.05)"; row.onmouseleave = () => row.style.background = "transparent"; const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = true; cb.dataset.index = String(idx); const tt = document.createElement("div"); tt.style.fontSize = "13px"; tt.style.whiteSpace = "nowrap"; tt.style.overflow = "hidden"; tt.style.textOverflow = "ellipsis"; tt.textContent = it.title || it.url; row.appendChild(cb); row.appendChild(tt); list.appendChild(row); state.rows.push({ row, cb }); }); } function getSelected() { const out = []; state.rows.forEach((r, i) => { if (r.cb.checked) out.push(state.data[i]) }); return out; } function setAll(val) { state.rows.forEach(r => r.cb.checked = val); } async function loadTracks(list, status) { status.textContent = "Scanning album tracklist…"; const items = collectAlbumAnchors(); renderList(items, list); status.textContent = `Loaded ${items.length} track(s).`; } let isProcessing = false; window.addEventListener('beforeunload', (e) => { if (isProcessing) { e.preventDefault(); e.returnValue = 'Changes are being applied. Are you sure you want to leave?'; } }); const editBtn = document.createElement("button"); editBtn.id = "gradient-assistant-toggle"; editBtn.type = "button"; editBtn.textContent = "Gradient Assistant"; document.body.appendChild(editBtn); editBtn.onclick = async () => { const { ui, copyBtn, gradientInput, list, status, fill, reloadBtn, allBtn, noneBtn, applyBtn } = createUI(); try { const gradient = await findCurrentGradient(); if (gradient) { const colors = parseGradient(gradient); const gradientText = `linear-gradient(135deg, ${colors.primary}, ${colors.secondary})`; gradientInput.value = gradientText; await navigator.clipboard.writeText(gradientText); copyBtn.textContent = "Done!"; setTimeout(() => copyBtn.textContent = "Copy Gradient", 2000); } } catch (error) { console.error('Error copying gradient:', error); status.textContent = `Error: ${error.message}`; } copyBtn.onclick = async () => { try { const gradient = await findCurrentGradient(); if (!gradient) { throw new Error('No visible gradient detected'); } const colors = parseGradient(gradient); if (!colors) { throw new Error('The found gradient is not in a valid format'); } const gradientText = `linear-gradient(135deg, ${colors.primary}, ${colors.secondary})`; await navigator.clipboard.writeText(gradientText); copyBtn.textContent = "Done!"; setTimeout(() => copyBtn.textContent = "Copy Gradient", 2000); } catch (error) { console.error('Error copying gradient:', error); status.textContent = `Error: ${error.message}`; } }; reloadBtn.onclick = () => loadTracks(list, status); allBtn.onclick = () => setAll(true); noneBtn.onclick = () => setAll(false); applyBtn.onclick = async () => { const csrf = getCsrf(); if (!csrf) { status.textContent = "Missing CSRF token."; return; } const gradientText = gradientInput.value.trim(); if (!gradientText) { status.textContent = "Please paste a gradient."; return; } try { isProcessing = true; const { primary, secondary } = parseGradient(gradientText); const P = hex(primary); const S = hex(secondary); const T = computeTextColor(P, S); const sel = getSelected(); if (!sel.length) { status.textContent = "No tracks selected."; return; } applyBtn.disabled = true; reloadBtn.disabled = true; allBtn.disabled = true; noneBtn.disabled = true; status.textContent = "Processing…"; let done = 0, fail = 0; for (const it of sel) { status.textContent = `Resolving ID…`; let id = null; try { id = await fetchSongIdByLyricUrl(it.url); } catch (e) { console.warn(`Failed to fetch song ID for ${it.url}:`, e); } if (!id) { fail++; done++; fill.style.width = ((done / sel.length) * 100).toFixed(1) + "%"; continue; } status.textContent = `Updating #${id}…`; try { await putSongColors(id, P, S, T, csrf); } catch (e) { console.warn(`Failed to update colors for song #${id}:`, e); fail++; } done++; fill.style.width = ((done / sel.length) * 100).toFixed(1) + "%"; await new Promise(r => setTimeout(r, 400)); } status.textContent = `Done: ${sel.length - fail}/${sel.length}`; } catch (e) { status.textContent = `Error: ${e.message}`; } finally { applyBtn.disabled = false; reloadBtn.disabled = false; allBtn.disabled = false; noneBtn.disabled = false; isProcessing = false; } }; loadTracks(list, status); }; })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址