Skip to content

Commit 70437f6

Browse files
committed
First pass at new album art
1 parent 85a3b25 commit 70437f6

File tree

4 files changed

+229
-36
lines changed

4 files changed

+229
-36
lines changed

src/app/album-art/route.tsx

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/* eslint-disable react/no-unknown-property */
2+
import 'server-only';
3+
4+
import { fetchArtists, fetchShowByUUID } from '@/app/queries';
5+
import { ImageResponse } from 'next/og';
6+
import { NextRequest } from 'next/server';
7+
import React from 'react';
8+
9+
export const runtime = 'edge';
10+
11+
const notFound = () =>
12+
new Response('Not Found', {
13+
status: 404,
14+
});
15+
16+
// Function to generate a color based on artist UUID
17+
const getArtistColor = (uuid: string) => {
18+
// Generate a consistent color based on the UUID
19+
const hash = Array.from(uuid).reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0);
20+
21+
// Map to a curated set of Tailwind colors
22+
const tailwindColors = [
23+
'#3b82f6', // blue-500
24+
'#10b981', // emerald-500
25+
'#8b5cf6', // violet-500
26+
'#ef4444', // red-500
27+
'#f59e0b', // amber-500
28+
'#06b6d4', // cyan-500
29+
'#f97316', // orange-500
30+
'#8b5cf6', // violet-500
31+
'#0ea5e9', // sky-500
32+
'#22c55e', // green-500
33+
'#dc2626', // red-600
34+
'#0d9488', // teal-600
35+
];
36+
37+
return tailwindColors[Math.abs(hash) % tailwindColors.length];
38+
};
39+
40+
// Function to generate a gradient based on artist UUID
41+
const getArtistGradient = (uuid: string) => {
42+
const baseColor = getArtistColor(uuid);
43+
44+
// Create complementary color pairs for beautiful gradients using Tailwind colors
45+
const gradientPairs = {
46+
'#3b82f6': '#8b5cf6', // blue-500 to violet-500
47+
'#10b981': '#06b6d4', // emerald-500 to cyan-500
48+
'#8b5cf6': '#3b82f6', // violet-500 to blue-500
49+
'#ef4444': '#f97316', // red-500 to orange-500
50+
'#f59e0b': '#ef4444', // amber-500 to red-500
51+
'#06b6d4': '#10b981', // cyan-500 to emerald-500
52+
'#f97316': '#f59e0b', // orange-500 to amber-500
53+
// '#8b5cf6': '#a855f7', // violet-500 to purple-500
54+
'#0ea5e9': '#3b82f6', // sky-500 to blue-500
55+
'#22c55e': '#10b981', // green-500 to emerald-500
56+
'#dc2626': '#ef4444', // red-600 to red-500
57+
'#0d9488': '#06b6d4', // teal-600 to cyan-500
58+
};
59+
60+
const secondColor = gradientPairs[baseColor] || '#8b5cf6'; // Default to violet-500
61+
62+
return `linear-gradient(135deg, ${baseColor}, ${secondColor})`;
63+
};
64+
65+
export async function GET(request: NextRequest) {
66+
try {
67+
const { searchParams } = new URL(request.url);
68+
69+
const showUuid = searchParams.get('showUuid');
70+
if (!showUuid) return notFound();
71+
72+
const artists = await fetchArtists();
73+
const show = await fetchShowByUUID(showUuid);
74+
75+
if (!show || !show.sources?.length) return notFound();
76+
77+
// Get params
78+
const artist = artists.find((artist) => artist.uuid === show.artist_uuid);
79+
const artistName = artist?.name ?? 'Unknown Artist';
80+
81+
// Generate dynamic background color and pattern based on artist UUID
82+
const bgGradient = getArtistGradient(show.artist_uuid || '');
83+
84+
// Generate a pattern of circles for the background based on artist UUID
85+
const patternSeed = parseInt(show.artist_uuid?.replace(/\D/g, '').slice(0, 8) || '0', 10);
86+
const patternElements: React.ReactNode[] = [];
87+
88+
for (let i = 0; i < 5; i++) {
89+
const size = 100 + (patternSeed % 200);
90+
const x = (patternSeed * (i + 1)) % 900;
91+
const y = (patternSeed * (i + 2)) % 900;
92+
const opacity = 0.1 + i * 0.05;
93+
94+
patternElements.push(
95+
<div
96+
key={i}
97+
tw="absolute rounded-full"
98+
style={{
99+
width: size,
100+
height: size,
101+
left: x,
102+
top: y,
103+
background: 'white',
104+
opacity: opacity,
105+
filter: 'blur(40px)',
106+
}}
107+
/>
108+
);
109+
}
110+
111+
// Fetch Roboto font
112+
const fontReg = await fetch(
113+
new URL('https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.ttf')
114+
).then((res) => res.arrayBuffer());
115+
116+
const fontBold = await fetch(
117+
new URL('https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-700-normal.ttf')
118+
).then((res) => res.arrayBuffer());
119+
120+
const fontMegaBold = await fetch(
121+
new URL('https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-900-normal.ttf')
122+
).then((res) => res.arrayBuffer());
123+
124+
return new ImageResponse(
125+
(
126+
<div
127+
tw="flex h-full w-full flex-col items-center justify-center p-10 text-white relative overflow-hidden"
128+
style={{ background: bgGradient }}
129+
>
130+
<div tw="flex w-full max-w-[800px] flex-col items-center justify-center rounded-xl bg-black/10 p-10">
131+
<div tw="mb-2 text-center text-7xl font-extrabold">{artistName}</div>
132+
<div tw="mb-2 text-center text-6xl font-bold">{show.display_date}</div>
133+
<div tw="flex items-center justify-center" style={{ gap: 4 }}>
134+
{show.venue?.name && (
135+
<div tw="rounded-lg bg-white/20 px-4 py-2 text-4xl flex">
136+
{show.venue?.name} {show.venue?.location ?? ''}
137+
</div>
138+
)}
139+
</div>
140+
</div>
141+
<div tw="absolute bottom-5 right-5 text-4xl opacity-80 font-bold">Relisten.net</div>
142+
{patternElements}
143+
</div>
144+
),
145+
{
146+
width: 1024,
147+
height: 1024,
148+
fonts: [
149+
{
150+
name: 'Roboto',
151+
data: fontReg,
152+
weight: 400,
153+
style: 'normal',
154+
},
155+
{
156+
name: 'Roboto',
157+
data: fontBold,
158+
weight: 700,
159+
style: 'normal',
160+
},
161+
{
162+
name: 'Roboto',
163+
data: fontMegaBold,
164+
weight: 900,
165+
style: 'normal',
166+
},
167+
],
168+
}
169+
);
170+
} catch (error) {
171+
console.error('Error generating album art:', error);
172+
return new Response('Error generating album art', { status: 500 });
173+
}
174+
}

src/app/queries.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { notFound } from 'next/navigation';
22

3-
import { sortSources } from '@/redux/modules/tapes';
4-
import { Artist, Tape } from '@/types';
53
import ky from 'ky-universal';
4+
import { Artist, Tape } from '@/types';
65
import { API_DOMAIN } from '../lib/constants';
6+
import { sortSources } from '@/lib/sortSources';
77

88
export const fetchArtists = async (): Promise<Artist[]> => {
99
const parsed = await ky(`${API_DOMAIN}/api/v2/artists`, {
@@ -40,3 +40,21 @@ export const fetchShow = async (
4040
notFound();
4141
}
4242
};
43+
44+
export const fetchShowByUUID = async (showUuid: string): Promise<Partial<Tape> | undefined> => {
45+
if (!showUuid) return { sources: [] };
46+
47+
try {
48+
const parsed = (await ky(`${API_DOMAIN}/api/v3/shows/${showUuid}`, {
49+
cache: 'no-cache',
50+
}).json()) as Tape;
51+
52+
if (parsed) {
53+
parsed.sources = sortSources(parsed.sources);
54+
55+
return parsed;
56+
}
57+
} catch (err) {
58+
notFound();
59+
}
60+
};

src/lib/sortSources.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { firstBy } from 'thenby';
2+
3+
const getEtreeId = (s = '') =>
4+
Number(
5+
s
6+
.split('.')
7+
.reverse()
8+
.find((x) => /^[0-9]+$/.test(x))
9+
);
10+
11+
// tapes: TODO: GD sort (charlie miller, sbd + etree id, weighted average), sbd + etree id, weighted avg, asc, desc
12+
// for now, hardcode sort: sbd, charlie miller, etree id, weighted average
13+
export const sortSources = (sources) => {
14+
const sortedSources = sources
15+
? [...sources].sort(
16+
firstBy((t) => t.is_soundboard, 'desc')
17+
// Charlie for GD, Pete for JRAD
18+
.thenBy(
19+
(t) =>
20+
/(charlie miller)|(peter costello)/i.test(
21+
[t.taper, t.transferrer, t.source].join('')
22+
),
23+
'desc'
24+
)
25+
.thenBy(
26+
(t1, t2) => getEtreeId(t1.upstream_identifier) - getEtreeId(t2.upstream_identifier),
27+
'desc'
28+
)
29+
.thenBy((t) => t.avg_rating_weighted, 'desc')
30+
)
31+
: [];
32+
33+
return sortedSources;
34+
};

src/redux/modules/tapes.js

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HYDRATE } from 'next-redux-wrapper';
2-
import { firstBy } from 'thenby';
32

43
import { API_DOMAIN } from '../../lib/constants';
4+
import { sortSources } from '@/lib/sortSources';
55

66
const REQUEST_TAPES = 'years/REQUEST_TAPES';
77
const RECEIVE_TAPES = 'years/RECEIVE_TAPES';
@@ -50,39 +50,6 @@ export default function counter(state = defaultState, action) {
5050
}
5151
}
5252

53-
const getEtreeId = (s = '') =>
54-
Number(
55-
s
56-
.split('.')
57-
.reverse()
58-
.find((x) => /^[0-9]+$/.test(x))
59-
);
60-
61-
// tapes: TODO: GD sort (charlie miller, sbd + etree id, weighted average), sbd + etree id, weighted avg, asc, desc
62-
// for now, hardcode sort: sbd, charlie miller, etree id, weighted average
63-
export const sortSources = (sources) => {
64-
const sortedSources = sources
65-
? [...sources].sort(
66-
firstBy((t) => t.is_soundboard, 'desc')
67-
// Charlie for GD, Pete for JRAD
68-
.thenBy(
69-
(t) =>
70-
/(charlie miller)|(peter costello)/i.test(
71-
[t.taper, t.transferrer, t.source].join('')
72-
),
73-
'desc'
74-
)
75-
.thenBy(
76-
(t1, t2) => getEtreeId(t1.upstream_identifier) - getEtreeId(t2.upstream_identifier),
77-
'desc'
78-
)
79-
.thenBy((t) => t.avg_rating_weighted, 'desc')
80-
)
81-
: [];
82-
83-
return sortedSources;
84-
};
85-
8653
export function requestTapes(artistSlug, year, showDate) {
8754
return {
8855
type: REQUEST_TAPES,

0 commit comments

Comments
 (0)