Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions api/music.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ module.exports = async (req, res) => {
res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=3600, stale-while-revalidate=1800");

const {
username,
theme,
hide_border,
bg_color,
Expand All @@ -27,18 +26,15 @@ module.exports = async (req, res) => {
gif_url
} = req.query;

// username is not strictly required for this card as it's purely decorative/user-defined music,
// but we keep it for consistency with other endpoints if needed.

try {
let colors = getTheme(theme);
colors = applyColorOverrides(colors, { bg_color, title_color, text_color, border_color });

const svg = generateMusicSVG({
player: player || "90s",
title: title || "Not Playing",
artist: artist || "Unknown Artist",
track_id: track_id || "",
title: title,
artist: artist,
track_id: track_id,
gif_url: gif_url || "",
colors,
hideBorder: hide_border === "true",
Expand Down
246 changes: 167 additions & 79 deletions src/svg-music.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,148 +2,236 @@
* Music Player Card SVG generation
*/

const DEFAULT_SONGS = [
{
title: "Lofi Hip Hop",
artist: "Lofi Girl",
track_id: "4cOdK2wG6S999UI9797", // Example ID
url: "https://www.youtube.com/watch?v=jfKfPfyJRdk"
},
{
title: "Midnight City",
artist: "M83",
track_id: "16X79m77pD8pY0pX8pY8",
url: "https://open.spotify.com/track/16X79m77pD8pY0pX8pY8"
},
{
title: "Blinding Lights",
artist: "The Weeknd",
track_id: "0VjIj9S91pI9pX8pY8pY",
url: "https://open.spotify.com/track/0VjIj9S91pI9pX8pY8pY"
}
];

function generateMusicSVG(options) {
const {
player = "90s",
title = "Unknown Title",
artist = "Unknown Artist",
track_id = "",
gif_url = "",
colors,
hideBorder,
} = options;

let { title, artist, track_id, gif_url } = options;

// Use default song if none provided
if (!title && !artist && !track_id) {
const defaultSong = DEFAULT_SONGS[0];
Comment on lines +33 to +37
title = defaultSong.title;
artist = defaultSong.artist;
track_id = defaultSong.track_id;
}

const cardWidth = 400;
const cardHeight = 120;
const hba = hideBorder ? `rx="8"` : `rx="8" stroke="#30363d" stroke-width="1"`;
const accentColor = (colors && colors.accent_color) || "58a6ff";
const bgColor = (colors && colors.bg_color) || "0d1117";
const titleColor = (colors && colors.title_color) || accentColor;
const textColor = (colors && colors.text_color) || "8b949e";

const trackUrl = track_id ? `https://open.spotify.com/track/${track_id}` : "#";

let playerContent = "";
let styleTags = `
.title { font: 600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; fill: #${titleColor}; }
.title { font: 700 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; fill: #${titleColor}; }
.artist { font: 400 12px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; fill: #${textColor}; }
.controls { cursor: pointer; }
@keyframes bar {
0% { height: 2px; }
50% { height: 15px; }
100% { height: 2px; }
0%, 100% { height: 4px; }
50% { height: 20px; }
}
.bar { fill: #${accentColor}; animation: bar 1.2s infinite ease-in-out; }
.bar { fill: #${accentColor}; animation: bar 1s infinite ease-in-out; }
.bar1 { animation-delay: 0s; }
.bar2 { animation-delay: 0.2s; }
.bar3 { animation-delay: 0.4s; }
.bar4 { animation-delay: 0.6s; }
.bar5 { animation-delay: 0.8s; }
.bar2 { animation-delay: 0.15s; }
.bar3 { animation-delay: 0.3s; }
.bar4 { animation-delay: 0.45s; }
.bar5 { animation-delay: 0.6s; }
.glow { filter: drop-shadow(0 0 3px #${accentColor}); }
`;

if (player === "ipod") {
playerContent = `
<rect width="${cardWidth}" height="${cardHeight}" fill="#e0e0e0" ${hba}/>
<rect x="20" y="15" width="120" height="90" fill="#333" rx="4"/>
${gif_url ? `<image href="${gif_url}" x="25" y="20" width="110" height="80" preserveAspectRatio="xMidYMid slice" clip-path="inset(0% round 4px)"/>` : `
<g transform="translate(45, 100)">
<rect class="bar bar1" x="0" y="0" width="10" height="20" transform="scale(1,-1)"/>
<rect class="bar bar2" x="15" y="0" width="10" height="30" transform="scale(1,-1)"/>
<rect class="bar bar3" x="30" y="0" width="10" height="25" transform="scale(1,-1)"/>
<rect class="bar bar4" x="45" y="0" width="10" height="35" transform="scale(1,-1)"/>
<defs>
<linearGradient id="ipodGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f0f0f0;stop-opacity:1" />
<stop offset="100%" style="stop-color:#c0c0c0;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="${cardWidth}" height="${cardHeight}" fill="url(#ipodGrad)" ${hba}/>
<rect x="20" y="15" width="130" height="90" fill="#222" rx="6" stroke="#444" stroke-width="2"/>
${gif_url ? `<image href="${gif_url}" x="25" y="20" width="120" height="80" preserveAspectRatio="xMidYMid slice" clip-path="inset(0% round 4px)"/>` : `
<g transform="translate(45, 95)">
<rect class="bar bar1" x="0" y="0" width="12" height="20" transform="scale(1,-1)" rx="1"/>
<rect class="bar bar2" x="18" y="0" width="12" height="30" transform="scale(1,-1)" rx="1"/>
<rect class="bar bar3" x="36" y="0" width="12" height="25" transform="scale(1,-1)" rx="1"/>
<rect class="bar bar4" x="54" y="0" width="12" height="35" transform="scale(1,-1)" rx="1"/>
</g>
`}
<g transform="translate(160, 35)">
<g transform="translate(170, 40)">
<text class="title" y="0">${escapeXml(title)}</text>
<text class="artist" y="20">${escapeXml(artist)}</text>
<text class="artist" y="22" opacity="0.8">${escapeXml(artist)}</text>
</g>
<g transform="translate(290, 65)">
<circle r="40" fill="#fff" stroke="#bbb" stroke-width="1.5"/>
<circle r="15" fill="#eee" stroke="#ccc" stroke-width="1"/>
<text y="-25" font-size="8" fill="#888" text-anchor="middle" font-weight="bold">MENU</text>
<a href="${trackUrl}" target="_blank">
<path d="M-5 -5 L8 0 L-5 5 Z" fill="#555" transform="translate(0,0)"/>
<circle r="40" fill="transparent"/>
</a>
</g>
Comment on lines +95 to 99
<circle cx="280" cy="70" r="35" fill="#fff" stroke="#ccc" stroke-width="1"/>
<circle cx="280" cy="70" r="12" fill="#eee" stroke="#ccc" stroke-width="1"/>
<a href="${trackUrl}" target="_blank">
<path d="M275 65 L288 70 L275 75 Z" fill="#555"/>
</a>
`;
} else if (player === "cassette") {
styleTags += `
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.reel { transform-origin: center; animation: spin 3s linear infinite; }
.reel { transform-origin: center; animation: spin 4s linear infinite; }
`;
playerContent = `
<rect width="${cardWidth}" height="${cardHeight}" fill="#333" ${hba}/>
<rect x="20" y="20" width="360" height="80" fill="#555" rx="10"/>
<rect x="100" y="35" width="200" height="50" fill="#111" rx="5"/>
<g transform="translate(135, 60)">
<circle class="reel" r="15" fill="none" stroke="#777" stroke-width="2" stroke-dasharray="5,3"/>
<defs>
<linearGradient id="cassetteGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#333;stop-opacity:1" />
<stop offset="100%" style="stop-color:#111;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="${cardWidth}" height="${cardHeight}" fill="url(#cassetteGrad)" ${hba}/>
<rect x="25" y="20" width="350" height="80" fill="#d4af37" rx="8" opacity="0.9"/>
<rect x="35" y="30" width="330" height="60" fill="#222" rx="5"/>
<rect x="110" y="40" width="180" height="40" fill="#111" rx="4" stroke="#444"/>

<g transform="translate(145, 60)">
<circle class="reel" r="12" fill="none" stroke="#666" stroke-width="3" stroke-dasharray="4,4"/>
<circle r="4" fill="#666"/>
</g>
<g transform="translate(265, 60)">
<circle class="reel" r="15" fill="none" stroke="#777" stroke-width="2" stroke-dasharray="5,3"/>
<g transform="translate(255, 60)">
<circle class="reel" r="12" fill="none" stroke="#666" stroke-width="3" stroke-dasharray="4,4"/>
<circle r="4" fill="#666"/>
</g>
<g transform="translate(200, 55)" text-anchor="middle">
<text class="title" y="-5" font-size="10" fill="#eee">${escapeXml(title)}</text>
<text class="artist" y="12" font-size="8" fill="#aaa">${escapeXml(artist)}</text>

<g transform="translate(200, 35)" text-anchor="middle">
<text class="title" y="0" font-size="11" fill="#fff">${escapeXml(title)}</text>
</g>
<g transform="translate(200, 95)" text-anchor="middle">
<text class="artist" y="0" font-size="9" fill="#d4af37">${escapeXml(artist)}</text>
</g>
${gif_url ? `<image href="${gif_url}" x="150" y="40" width="100" height="40" opacity="0.6"/>` : ""}

${gif_url ? `<image href="${gif_url}" x="150" y="45" width="100" height="30" opacity="0.4"/>` : ""}
<a href="${trackUrl}" target="_blank">
<rect x="20" y="20" width="360" height="80" fill="transparent"/>
<rect x="0" y="0" width="${cardWidth}" height="${cardHeight}" fill="transparent"/>
</a>
`;
} else if (player === "boombox") {
playerContent = `
<rect width="${cardWidth}" height="${cardHeight}" fill="#444" ${hba}/>
<circle cx="60" cy="60" r="35" fill="#222" stroke="#666" stroke-width="3"/>
<circle cx="60" cy="60" r="25" fill="#111" stroke="#333" stroke-width="1"/>
<circle cx="340" cy="60" r="35" fill="#222" stroke="#666" stroke-width="3"/>
<circle cx="340" cy="60" r="25" fill="#111" stroke="#333" stroke-width="1"/>
<rect x="110" y="30" width="180" height="60" fill="#222" rx="4"/>
<g transform="translate(120, 50)">
<text class="title" y="0" font-size="12" fill="#0f0">${escapeXml(title)}</text>
<text class="artist" y="20" font-size="10" fill="#0f0" opacity="0.8">${escapeXml(artist)}</text>
<defs>
<radialGradient id="speakerGrad">
<stop offset="70%" stop-color="#111"/>
<stop offset="100%" stop-color="#333"/>
</radialGradient>
</defs>
<rect width="${cardWidth}" height="${cardHeight}" fill="#2a2a2a" ${hba}/>
<rect x="10" y="10" width="380" height="100" fill="#333" rx="10" stroke="#111" stroke-width="2"/>

<!-- Speakers -->
<circle cx="65" cy="60" r="40" fill="url(#speakerGrad)" stroke="#222" stroke-width="2"/>
<circle cx="65" cy="60" r="30" fill="none" stroke="#444" stroke-width="1" stroke-dasharray="2,2"/>

<circle cx="335" cy="60" r="40" fill="url(#speakerGrad)" stroke="#222" stroke-width="2"/>
<circle cx="335" cy="60" r="30" fill="none" stroke="#444" stroke-width="1" stroke-dasharray="2,2"/>

<!-- Display -->
<rect x="120" y="30" width="160" height="60" fill="#000" rx="4" stroke="#0f0" stroke-width="1" stroke-opacity="0.3"/>
<g transform="translate(130, 50)">
<text class="title glow" y="0" font-size="13" fill="#0f0" font-family="monospace">${escapeXml(title)}</text>
<text class="artist" y="22" font-size="10" fill="#0f0" opacity="0.7" font-family="monospace">${escapeXml(artist)}</text>
</g>
<g transform="translate(250, 75)">
<rect class="bar bar1" x="0" y="0" width="4" height="15" transform="scale(1,-1)"/>
<rect class="bar bar2" x="6" y="0" width="4" height="15" transform="scale(1,-1)"/>
<rect class="bar bar3" x="12" y="0" width="4" height="15" transform="scale(1,-1)"/>

<!-- Viz in display -->
<g transform="translate(240, 80)">
<rect class="bar bar1" x="0" y="0" width="5" height="20" transform="scale(1,-1)"/>
<rect class="bar bar2" x="7" y="0" width="5" height="25" transform="scale(1,-1)"/>
<rect class="bar bar3" x="14" y="0" width="5" height="18" transform="scale(1,-1)"/>
</g>

<a href="${trackUrl}" target="_blank">
<rect x="110" y="30" width="180" height="60" fill="transparent"/>
<rect x="0" y="0" width="${cardWidth}" height="${cardHeight}" fill="transparent"/>
</a>
`;
} else {
// Default 90s Winamp style
// Default 90s Winamp style - Enhanced
playerContent = `
<rect width="${cardWidth}" height="${cardHeight}" fill="#2c2c2c" ${hba}/>
<rect x="0" y="0" width="${cardWidth}" height="20" fill="#1a1a1a"/>
<text x="10" y="14" font-family="monospace" font-size="10" fill="#ccc">WINAMP 2.91</text>
<defs>
<linearGradient id="winampGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#3a3a3a;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1a1a1a;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="${cardWidth}" height="${cardHeight}" fill="url(#winampGrad)" ${hba}/>

<!-- Title Bar -->
<rect x="2" y="2" width="${cardWidth - 4}" height="18" fill="#000080" rx="2"/>
<text x="10" y="15" font-family="monospace" font-size="11" font-weight="bold" fill="#fff">WINAMP 2.91 - ${escapeXml(title)}</text>

<rect x="10" y="30" width="70" height="50" fill="#000"/>
${gif_url ? `<image href="${gif_url}" x="10" y="30" width="70" height="50" preserveAspectRatio="xMidYMid slice"/>` : `
<!-- Visualization Area -->
<rect x="10" y="25" width="80" height="55" fill="#000" stroke="#444"/>
${gif_url ? `<image href="${gif_url}" x="11" y="26" width="78" height="53" preserveAspectRatio="xMidYMid slice"/>` : `
<g transform="translate(15, 75)">
<rect class="bar bar1" x="0" y="0" width="8" height="30" transform="scale(1,-1)"/>
<rect class="bar bar2" x="10" y="0" width="8" height="40" transform="scale(1,-1)"/>
<rect class="bar bar3" x="20" y="0" width="8" height="35" transform="scale(1,-1)"/>
<rect class="bar bar4" x="30" y="0" width="8" height="45" transform="scale(1,-1)"/>
<rect class="bar bar5" x="40" y="0" width="8" height="30" transform="scale(1,-1)"/>
<rect class="bar bar1" x="0" y="0" width="10" height="30" transform="scale(1,-1)"/>
<rect class="bar bar2" x="12" y="0" width="10" height="45" transform="scale(1,-1)"/>
<rect class="bar bar3" x="24" y="0" width="10" height="35" transform="scale(1,-1)"/>
<rect class="bar bar4" x="36" y="0" width="10" height="50" transform="scale(1,-1)"/>
<rect class="bar bar5" x="48" y="0" width="10" height="40" transform="scale(1,-1)"/>
</g>
`}

<g transform="translate(90, 45)">
<text class="title" y="0" font-family="monospace">${escapeXml(title)}</text>
<text class="artist" y="20" font-family="monospace">${escapeXml(artist)}</text>
<!-- Song Info -->
<g transform="translate(100, 45)">
<text class="title" y="0" font-family="monospace" font-size="16" fill="#0f0" style="filter: drop-shadow(0 0 2px #0f0)">${escapeXml(title)}</text>
<text class="artist" y="25" font-family="monospace" font-size="12" fill="#0f0" opacity="0.8">${escapeXml(artist)}</text>
</g>

<g transform="translate(10, 95)" class="controls">
<!-- Controls -->
<g transform="translate(10, 90)">
<a href="${trackUrl}" target="_blank">
<g transform="translate(0,0)">
<rect width="35" height="20" fill="#444" rx="2" stroke="#666"/>
<path d="M12 5 L25 10 L12 15 Z" fill="#ccc"/>
</g>
</a>
<a href="${trackUrl}" target="_blank">
<rect x="0" y="0" width="30" height="15" fill="#444" rx="2"/>
<path d="M10 4 L20 7.5 L10 11 Z" fill="#eee"/>
<g transform="translate(40,0)">
<rect width="35" height="20" fill="#444" rx="2" stroke="#666"/>
<rect x="11" y="5" width="4" height="10" fill="#ccc"/>
<rect x="20" y="5" width="4" height="10" fill="#ccc"/>
</g>
</a>
<a href="${trackUrl}" target="_blank">
<rect x="35" y="0" width="30" height="15" fill="#444" rx="2"/>
<rect x="42" y="4" width="3" height="7" fill="#eee"/>
<rect x="48" y="4" width="3" height="7" fill="#eee"/>
<g transform="translate(80,0)">
<rect width="35" height="20" fill="#444" rx="2" stroke="#666"/>
<rect x="10" y="5" width="15" height="10" fill="#ccc"/>
</g>
</a>
<a href="${trackUrl}" target="_blank">
<rect x="70" y="0" width="30" height="15" fill="#444" rx="2"/>
<rect x="78" y="4" width="14" height="7" fill="#eee"/>
<g transform="translate(120,0)">
<rect width="70" height="20" fill="#222" rx="10" stroke="#444"/>
<circle cx="15" cy="10" r="6" fill="#${accentColor}"/>
<text x="30" y="14" font-size="9" fill="#888" font-family="monospace">VOLUME</text>
</g>
</a>
</g>
`;
Comment on lines 236 to 237
Expand Down
Loading