You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
586 lines
19 KiB
586 lines
19 KiB
/**
|
|
* El catálogo, leído de PostgreSQL.
|
|
*
|
|
* Devuelve **exactamente** los tipos que ya usaba el sitio cuando el catálogo
|
|
* vivía en `src/content`: `Cancion`, `Album`, `Estilo`. Es deliberado. Cambiar
|
|
* a la vez de dónde salen los datos y qué forma tienen habría dejado veinte
|
|
* archivos sin poder comprobar cuál de las dos cosas rompió; así las pruebas
|
|
* que ya existen valen de red tal cual.
|
|
*
|
|
* ## Por qué se carga entero
|
|
*
|
|
* Son dieciocho temas. Hasta ahora viajaban en el paquete de JavaScript —los
|
|
* cargaba `import.meta.glob` al construir—, así que el sitio ya contaba con
|
|
* tenerlos todos a mano: el carrito resuelve sus slugs por su cuenta, el
|
|
* reproductor arma la cola, las tarjetas miran si el tema está comprado. Leer
|
|
* el catálogo entero y guardarlo en memoria mantiene eso funcionando y no pide
|
|
* una consulta por tarjeta.
|
|
*
|
|
* Lo que **no** se carga aquí es la letra completa, que es lo único pesado: de
|
|
* ella solo sale el extracto de las tarjetas. La letra entera la pide la ficha
|
|
* del tema, y solo la suya.
|
|
*
|
|
* La caché se tira sola cada minuto. No hay administración todavía; cuando la
|
|
* haya, esto tendrá que invalidarse al guardar, no por reloj.
|
|
*/
|
|
|
|
import { and, asc, eq, isNull, or } from 'drizzle-orm';
|
|
import { alias } from 'drizzle-orm/pg-core';
|
|
import { db } from './db';
|
|
import {
|
|
album,
|
|
albumEnlace,
|
|
artista,
|
|
autoria,
|
|
cancion,
|
|
cancionAudio,
|
|
cancionEnlace,
|
|
cancionEtiqueta,
|
|
cancionGenero,
|
|
cancionInterprete,
|
|
cancionPersona,
|
|
creditoAlbum,
|
|
estilo,
|
|
etiqueta,
|
|
genero,
|
|
medio,
|
|
pais,
|
|
persona,
|
|
registroObra,
|
|
traduccion,
|
|
video
|
|
} from './db/schema';
|
|
import { extraerLineas } from '$lib/content/extracto';
|
|
import { componer } from './markdown';
|
|
import * as q from '$lib/catalogo/consultas';
|
|
import type { Catalogo } from '$lib/catalogo/consultas';
|
|
import type {
|
|
Album,
|
|
Cancion,
|
|
Enlaces,
|
|
Estilo,
|
|
FormatoDeEdicion,
|
|
Plataforma,
|
|
RegistroDeObra,
|
|
VersionMusical,
|
|
VideoDeCancion
|
|
} from '$lib/types';
|
|
|
|
export type { Catalogo } from '$lib/catalogo/consultas';
|
|
|
|
const VIDA_MS = 60_000;
|
|
|
|
let guardado: { catalogo: Catalogo; hasta: number } | null = null;
|
|
|
|
/** Tira la caché. La llamará la administración el día que exista. */
|
|
export function olvidarCatalogo(): void {
|
|
guardado = null;
|
|
}
|
|
|
|
export async function leerCatalogo(): Promise<Catalogo> {
|
|
if (guardado && Date.now() < guardado.hasta) return guardado.catalogo;
|
|
|
|
const catalogo = await cargar();
|
|
guardado = { catalogo, hasta: Date.now() + VIDA_MS };
|
|
return catalogo;
|
|
}
|
|
|
|
/** Agrupa filas por una clave, que es lo que se hace ocho veces aquí abajo. */
|
|
function porClave<T, K>(filas: T[], clave: (f: T) => K): Map<K, T[]> {
|
|
const mapa = new Map<K, T[]>();
|
|
for (const fila of filas) {
|
|
const k = clave(fila);
|
|
const lista = mapa.get(k);
|
|
if (lista) lista.push(fila);
|
|
else mapa.set(k, [fila]);
|
|
}
|
|
return mapa;
|
|
}
|
|
|
|
/** Los enlaces llegan como filas y el sitio los espera como objeto. */
|
|
function comoEnlaces(filas: { plataforma: string; url: string }[]): Enlaces {
|
|
const enlaces: Enlaces = {};
|
|
for (const f of filas) enlaces[f.plataforma as Plataforma] = f.url;
|
|
return enlaces;
|
|
}
|
|
|
|
/**
|
|
* De clave de almacén a ruta pública.
|
|
*
|
|
* Las imágenes se sirven de `/static`, así que su clave —`images/…`— es ya la
|
|
* ruta con una barra delante. El audio no: ese vive en un almacén privado y no
|
|
* tiene URL pública, por eso no pasa por aquí.
|
|
*/
|
|
function rutaPublica(clave: string | null): string | undefined {
|
|
return clave ? `/${clave}` : undefined;
|
|
}
|
|
|
|
/**
|
|
* La onda es un derivado estático de la escucha y comparte su ruta relativa.
|
|
* No es una columna editorial: si cambia el audio, `scripts/medios.mjs` vuelve
|
|
* a calcularla y conserva esta dirección estable.
|
|
*/
|
|
function rutaDeOnda(clave: string | null, huella: string | null): string | undefined {
|
|
if (!clave?.startsWith('escucha/')) return undefined;
|
|
const ruta = `/${clave.replace(/^escucha\//, 'ondas/').replace(/\.[^/.]+$/, '.json')}`;
|
|
// La dirección lógica es estable, pero la huella evita que el navegador
|
|
// conserve la onda anterior cuando se sustituye el archivo de escucha.
|
|
return huella ? `${ruta}?v=${huella.slice(0, 12)}` : ruta;
|
|
}
|
|
|
|
async function cargar(): Promise<Catalogo> {
|
|
/*
|
|
* Alias de verdad y no la misma tabla dos veces: una canción se une a
|
|
* `medio` por su audio y otra vez por su portada, y sin alias Postgres ve
|
|
* un solo `medio` y devuelve la misma fila para los dos.
|
|
*/
|
|
const portadaCancion = alias(medio, 'portada_cancion');
|
|
const portadaAlbum = alias(medio, 'portada_album');
|
|
const imagenArtista = alias(medio, 'imagen_artista');
|
|
|
|
const [
|
|
filasEstilo,
|
|
filasAlbum,
|
|
filasCancion,
|
|
personas,
|
|
interpretes,
|
|
audiosDeEscucha,
|
|
etiquetas,
|
|
enlacesCancion,
|
|
enlacesAlbum,
|
|
videos,
|
|
traducciones,
|
|
registros,
|
|
partes,
|
|
creditos,
|
|
generosDeCancion
|
|
] = await Promise.all([
|
|
db
|
|
.select()
|
|
.from(estilo)
|
|
.where(eq(estilo.estado, 'publicado'))
|
|
.orderBy(asc(estilo.orden), asc(estilo.nombre)),
|
|
|
|
db
|
|
.select({
|
|
fila: album,
|
|
portadaClave: portadaAlbum.clave,
|
|
paisNombre: pais.nombre
|
|
})
|
|
.from(album)
|
|
.innerJoin(estilo, eq(estilo.id, album.estiloId))
|
|
.leftJoin(portadaAlbum, eq(portadaAlbum.id, album.portadaId))
|
|
// El archivo decía «España» y la base guarda «ES»: el nombre sale de
|
|
// la tabla de países, que es donde vive una sola vez.
|
|
.leftJoin(pais, eq(pais.codigo, album.paisEdicionCodigo))
|
|
.where(and(eq(album.estado, 'publicado'), eq(estilo.estado, 'publicado')))
|
|
.orderBy(asc(album.fecha)),
|
|
|
|
db
|
|
.select({
|
|
fila: cancion,
|
|
estiloSlug: estilo.slug,
|
|
albumSlug: album.slug,
|
|
portadaClave: portadaCancion.clave
|
|
})
|
|
.from(cancion)
|
|
.innerJoin(estilo, eq(estilo.id, cancion.estiloId))
|
|
.leftJoin(album, eq(album.id, cancion.albumId))
|
|
.leftJoin(portadaCancion, eq(portadaCancion.id, cancion.portadaId))
|
|
.where(
|
|
and(
|
|
eq(cancion.estado, 'publicado'),
|
|
eq(estilo.estado, 'publicado'),
|
|
or(isNull(cancion.albumId), eq(album.estado, 'publicado'))
|
|
)
|
|
)
|
|
.orderBy(asc(cancion.numero), asc(cancion.titulo)),
|
|
|
|
db
|
|
.select({
|
|
cancionId: cancionPersona.cancionId,
|
|
rol: cancionPersona.rol,
|
|
nombre: persona.nombre,
|
|
orden: cancionPersona.orden
|
|
})
|
|
.from(cancionPersona)
|
|
.innerJoin(persona, eq(persona.id, cancionPersona.personaId))
|
|
.orderBy(asc(cancionPersona.orden)),
|
|
|
|
db
|
|
.select({
|
|
cancionId: cancionInterprete.cancionId,
|
|
id: artista.id,
|
|
codigo: artista.codigo,
|
|
slug: artista.slug,
|
|
nombre: artista.nombre,
|
|
tipo: artista.tipo,
|
|
estado: artista.estado,
|
|
rol: cancionInterprete.rol,
|
|
instrumento: cancionInterprete.instrumento,
|
|
acreditadoComo: cancionInterprete.acreditadoComo,
|
|
imagenClave: imagenArtista.clave
|
|
})
|
|
.from(cancionInterprete)
|
|
.innerJoin(artista, eq(artista.id, cancionInterprete.artistaId))
|
|
.leftJoin(imagenArtista, eq(imagenArtista.id, artista.imagenId))
|
|
.orderBy(asc(cancionInterprete.orden), asc(artista.nombre)),
|
|
|
|
db
|
|
.select({
|
|
cancionId: cancionAudio.cancionId,
|
|
clave: medio.clave,
|
|
huella: medio.sha256,
|
|
duracion: medio.duracionS
|
|
})
|
|
.from(cancionAudio)
|
|
.innerJoin(medio, eq(medio.id, cancionAudio.medioId))
|
|
.where(and(eq(cancionAudio.uso, 'stream'), eq(medio.publico, true)))
|
|
.orderBy(asc(cancionAudio.orden)),
|
|
|
|
db
|
|
.select({ cancionId: cancionEtiqueta.cancionId, nombre: etiqueta.nombre })
|
|
.from(cancionEtiqueta)
|
|
.innerJoin(etiqueta, eq(etiqueta.id, cancionEtiqueta.etiquetaId))
|
|
// Por `orden` y no alfabéticamente: la primera etiqueta de un tema
|
|
// dice de qué va, y las de después matizan.
|
|
.orderBy(asc(cancionEtiqueta.orden)),
|
|
|
|
db
|
|
.select({
|
|
cancionId: cancionEnlace.cancionId,
|
|
plataforma: cancionEnlace.plataforma,
|
|
url: cancionEnlace.url
|
|
})
|
|
.from(cancionEnlace),
|
|
|
|
db
|
|
.select({
|
|
albumId: albumEnlace.albumId,
|
|
plataforma: albumEnlace.plataforma,
|
|
url: albumEnlace.url
|
|
})
|
|
.from(albumEnlace),
|
|
|
|
db
|
|
.select({ cancionId: video.cancionId, youtube: video.youtubeId, titulo: video.titulo })
|
|
.from(video)
|
|
.orderBy(asc(video.orden)),
|
|
|
|
db
|
|
.select({ cancionId: traduccion.cancionId, texto: traduccion.texto })
|
|
.from(traduccion)
|
|
.where(eq(traduccion.idioma, 'es')),
|
|
|
|
db.select().from(registroObra),
|
|
|
|
db
|
|
.select({
|
|
cancionId: autoria.cancionId,
|
|
rol: autoria.rol,
|
|
porcentaje: autoria.porcentaje,
|
|
nombre: persona.nombre
|
|
})
|
|
.from(autoria)
|
|
.innerJoin(persona, eq(persona.id, autoria.personaId)),
|
|
|
|
db
|
|
.select({
|
|
albumId: creditoAlbum.albumId,
|
|
rol: creditoAlbum.rol,
|
|
nombre: creditoAlbum.nombre,
|
|
personaNombre: persona.nombre
|
|
})
|
|
.from(creditoAlbum)
|
|
.leftJoin(persona, eq(persona.id, creditoAlbum.personaId))
|
|
.orderBy(asc(creditoAlbum.orden)),
|
|
|
|
/*
|
|
* El género musical del tema —bolero, bachata—, que no es lo mismo que el
|
|
* estilo: el estilo es cómo se ordena la obra en esta web y es uno solo;
|
|
* los géneros son de qué está hecha la canción y pueden ser varios.
|
|
*/
|
|
db
|
|
.select({ cancionId: cancionGenero.cancionId, nombre: genero.nombre, slug: genero.slug })
|
|
.from(cancionGenero)
|
|
.innerJoin(genero, eq(genero.id, cancionGenero.generoId))
|
|
.orderBy(asc(genero.nombre))
|
|
]);
|
|
|
|
/* --- Índices para no recorrer listas dentro de bucles ------------------ */
|
|
|
|
const porCancion = {
|
|
personas: porClave(personas, (p) => p.cancionId),
|
|
interpretes: porClave(interpretes, (i) => i.cancionId),
|
|
etiquetas: porClave(etiquetas, (e) => e.cancionId),
|
|
generos: porClave(generosDeCancion, (g) => g.cancionId),
|
|
enlaces: porClave(enlacesCancion, (e) => e.cancionId),
|
|
videos: porClave(videos, (v) => v.cancionId),
|
|
partes: porClave(partes, (p) => p.cancionId)
|
|
};
|
|
const audioDeEscucha = new Map(audiosDeEscucha.map((a) => [a.cancionId, a]));
|
|
const traduccionDe = new Map(traducciones.map((t) => [t.cancionId, t.texto]));
|
|
const registroDe = new Map(registros.map((r) => [r.cancionId, r]));
|
|
const enlacesDeAlbum = porClave(enlacesAlbum, (e) => e.albumId);
|
|
const creditosDeAlbum = porClave(creditos, (c) => c.albumId);
|
|
|
|
/* --- Estilos ----------------------------------------------------------- */
|
|
|
|
const estilos: Estilo[] = filasEstilo.map((e) => ({
|
|
slug: e.slug,
|
|
nombre: e.nombre,
|
|
resumen: e.resumen,
|
|
orden: e.orden,
|
|
principal: e.principal,
|
|
imagen: undefined
|
|
}));
|
|
|
|
/* --- Álbumes ----------------------------------------------------------- */
|
|
|
|
const slugDeEstilo = new Map(filasEstilo.map((e) => [e.id, e.slug]));
|
|
|
|
const albumes: Album[] = filasAlbum.map(({ fila, portadaClave, paisNombre }) => ({
|
|
slug: fila.slug,
|
|
titulo: fila.titulo,
|
|
estilo: slugDeEstilo.get(fila.estiloId) ?? '',
|
|
fecha: fila.fecha,
|
|
tipo: fila.tipo,
|
|
portada: rutaPublica(portadaClave) ?? '',
|
|
sello: fila.sello ?? undefined,
|
|
resumen: fila.resumen,
|
|
destacado: fila.destacado,
|
|
enlaces: comoEnlaces(enlacesDeAlbum.get(fila.id) ?? []),
|
|
// El álbum sintético de singles no existe en la base: lo compone quien
|
|
// lo necesita, igual que antes lo componía el módulo de contenido.
|
|
sintetico: false,
|
|
edicion: {
|
|
numeroCatalogo: fila.numeroCatalogo ?? undefined,
|
|
formatos: (fila.formatos ?? []) as FormatoDeEdicion[],
|
|
pais: paisNombre ?? undefined,
|
|
creditos: (creditosDeAlbum.get(fila.id) ?? []).map((c) => ({
|
|
rol: c.rol,
|
|
nombre: c.personaNombre ?? c.nombre ?? ''
|
|
}))
|
|
}
|
|
}));
|
|
|
|
/* --- Canciones ---------------------------------------------------------- */
|
|
|
|
/*
|
|
* Las versiones son filas de `cancion` con `version_de_id`, no una tabla
|
|
* aparte: se decidió así porque una versión puede cambiar de estilo, de
|
|
* etiquetas y de todo lo demás. Aquí se separan las principales de sus
|
|
* versiones y cada una vuelve con las suyas colgando.
|
|
*/
|
|
const versionesDe = porClave(
|
|
filasCancion.filter((f) => f.fila.versionDeId !== null),
|
|
(f) => f.fila.versionDeId!
|
|
);
|
|
|
|
const canciones: Cancion[] = filasCancion
|
|
.filter((f) => f.fila.versionDeId === null)
|
|
.map(({ fila, estiloSlug, albumSlug, portadaClave }) => {
|
|
const suyas = porCancion.personas.get(fila.id) ?? [];
|
|
const susInterpretes = porCancion.interpretes.get(fila.id) ?? [];
|
|
const escucha = audioDeEscucha.get(fila.id);
|
|
const registro = registroDe.get(fila.id);
|
|
const misPartes = porCancion.partes.get(fila.id) ?? [];
|
|
|
|
const versiones: VersionMusical[] = (versionesDe.get(fila.id) ?? [])
|
|
.sort((a, b) => a.fila.versionOrden - b.fila.versionOrden)
|
|
.map((v) => {
|
|
const escuchaVersion = audioDeEscucha.get(v.fila.id);
|
|
const interpretesVersion = porCancion.interpretes.get(v.fila.id) ?? [];
|
|
return {
|
|
id: v.fila.versionCodigo ?? v.fila.slug.replace(`${fila.slug}-`, ''),
|
|
codigo: v.fila.codigo,
|
|
slug: v.fila.slug,
|
|
nombre: v.fila.versionNombre ?? '',
|
|
audio: rutaPublica(escuchaVersion?.clave ?? null),
|
|
onda: rutaDeOnda(escuchaVersion?.clave ?? null, escuchaVersion?.huella ?? null),
|
|
duracion: v.fila.duracionS ?? escuchaVersion?.duracion ?? undefined,
|
|
nota: v.fila.versionNota ?? undefined,
|
|
interpretes: interpretesVersion.map((i) => ({
|
|
id: i.id,
|
|
codigo: i.codigo,
|
|
slug: i.estado === 'publicado' ? i.slug : undefined,
|
|
nombre: i.acreditadoComo ?? i.nombre,
|
|
tipo: i.tipo,
|
|
rol: i.rol,
|
|
instrumento: i.instrumento ?? undefined,
|
|
acreditadoComo: i.acreditadoComo ?? undefined,
|
|
imagen: i.estado === 'publicado' ? rutaPublica(i.imagenClave) : undefined
|
|
}))
|
|
};
|
|
});
|
|
|
|
const registroDeObra: RegistroDeObra = {
|
|
iswc: registro?.iswc ?? undefined,
|
|
isrc: registro?.isrc ?? undefined,
|
|
entidad: registro?.entidad ?? undefined,
|
|
numero: registro?.numero ?? undefined,
|
|
editorial: registro?.editorial ?? undefined,
|
|
reparto: misPartes.map((p) => ({
|
|
nombre: p.nombre,
|
|
rol: p.rol,
|
|
porcentaje: Number(p.porcentaje)
|
|
}))
|
|
};
|
|
|
|
const videosDe: VideoDeCancion[] = (porCancion.videos.get(fila.id) ?? []).map((v) => ({
|
|
youtube: v.youtube,
|
|
titulo: v.titulo
|
|
}));
|
|
|
|
return {
|
|
codigo: fila.codigo,
|
|
slug: fila.slug,
|
|
titulo: fila.titulo,
|
|
estilo: estiloSlug,
|
|
album: albumSlug ?? null,
|
|
numero: fila.numero,
|
|
audio: rutaPublica(escucha?.clave ?? null),
|
|
onda: rutaDeOnda(escucha?.clave ?? null, escucha?.huella ?? null),
|
|
duracion: fila.duracionS ?? escucha?.duracion ?? undefined,
|
|
ano: fila.ano ?? undefined,
|
|
letra: suyas.filter((p) => p.rol === 'letra').map((p) => p.nombre),
|
|
musica: suyas.filter((p) => p.rol === 'musica').map((p) => p.nombre),
|
|
interpretes: susInterpretes.map((i) => ({
|
|
id: i.id,
|
|
codigo: i.codigo,
|
|
slug: i.estado === 'publicado' ? i.slug : undefined,
|
|
nombre: i.acreditadoComo ?? i.nombre,
|
|
tipo: i.tipo,
|
|
rol: i.rol,
|
|
instrumento: i.instrumento ?? undefined,
|
|
acreditadoComo: i.acreditadoComo ?? undefined,
|
|
imagen: i.estado === 'publicado' ? rutaPublica(i.imagenClave) : undefined
|
|
})),
|
|
destacada: fila.destacada,
|
|
etiquetas: (porCancion.etiquetas.get(fila.id) ?? []).map((e) => e.nombre),
|
|
generos: (porCancion.generos.get(fila.id) ?? []).map((g) => ({
|
|
slug: g.slug,
|
|
nombre: g.nombre
|
|
})),
|
|
enlaces: comoEnlaces(porCancion.enlaces.get(fila.id) ?? []),
|
|
instrumental: fila.instrumental,
|
|
idioma: fila.idioma,
|
|
traduccion: traduccionDe.get(fila.id) ?? undefined,
|
|
portada: rutaPublica(portadaClave),
|
|
inspiracion: fila.inspiracion ? fila.inspiracion.split('\n\n').filter(Boolean) : [],
|
|
// El mismo extracto que inyectaba el preprocesador, con la misma
|
|
// función: son los primeros versos, no un resumen.
|
|
extracto: extraerLineas(fila.letra ?? ''),
|
|
versiones,
|
|
videos: videosDe,
|
|
alaVenta: fila.alaVenta,
|
|
registro: registroDeObra
|
|
} satisfies Cancion;
|
|
});
|
|
|
|
return { estilos, albumes, canciones };
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Las consultas
|
|
*
|
|
* Las mismas que usa el módulo de contenido —viven en `$lib/catalogo/consultas`
|
|
* y no saben de dónde salen los datos—, aquí atadas a la base. Es lo que hace
|
|
* que una ruta pueda cambiar de fuente sin cambiar de resultado.
|
|
*
|
|
* Son asíncronas porque leer el catálogo lo es; lo demás es idéntico.
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
export async function obtenerCancion(slug: string) {
|
|
return q.obtenerCancion(await leerCatalogo(), slug);
|
|
}
|
|
|
|
export async function obtenerAlbum(slug: string) {
|
|
return q.obtenerAlbum(await leerCatalogo(), slug);
|
|
}
|
|
|
|
export async function obtenerEstilo(slug: string) {
|
|
return q.obtenerEstilo(await leerCatalogo(), slug);
|
|
}
|
|
|
|
export async function estiloPrincipal() {
|
|
return q.estiloPrincipal(await leerCatalogo());
|
|
}
|
|
|
|
export async function albumesDeEstilo(estiloSlug: string) {
|
|
return q.albumesDeEstilo(await leerCatalogo(), estiloSlug);
|
|
}
|
|
|
|
export async function slugsDeAlbum() {
|
|
return q.slugsDeAlbum(await leerCatalogo());
|
|
}
|
|
|
|
export async function estilosConObra() {
|
|
return q.estilosConObra(await leerCatalogo());
|
|
}
|
|
|
|
export async function obtenerEstiloConObra(estiloSlug: string) {
|
|
return q.obtenerEstiloConObra(await leerCatalogo(), estiloSlug);
|
|
}
|
|
|
|
export async function etiquetas() {
|
|
return q.etiquetas(await leerCatalogo());
|
|
}
|
|
|
|
export async function etiquetasDeEstilo(estiloSlug: string, cuantas = 4) {
|
|
return q.etiquetasDeEstilo(await leerCatalogo(), estiloSlug, cuantas);
|
|
}
|
|
|
|
export async function obtenerEtiqueta(slug: string) {
|
|
return q.obtenerEtiqueta(await leerCatalogo(), slug);
|
|
}
|
|
|
|
export async function portadaDeCancion(cancion: Cancion) {
|
|
return q.portadaDeCancion(await leerCatalogo(), cancion);
|
|
}
|
|
|
|
/* No necesita catálogo: sale de la propia canción. */
|
|
export const albumDeCancion = q.albumDeCancion;
|
|
|
|
/**
|
|
* La letra completa de un tema, que no viaja con el catálogo.
|
|
*
|
|
* Es lo único pesado que hay: dieciocho letras enteras en cada petición para
|
|
* pintar una lista sería mandar cien veces lo que hace falta. La ficha pide la
|
|
* suya y nada más.
|
|
*/
|
|
export async function letraDeCancion(slug: string): Promise<string | null> {
|
|
const [fila] = await db
|
|
.select({ letra: cancion.letra })
|
|
.from(cancion)
|
|
.where(and(eq(cancion.slug, slug), eq(cancion.estado, 'publicado')))
|
|
.limit(1);
|
|
return fila?.letra ?? null;
|
|
}
|
|
|
|
/**
|
|
* La introducción de un estilo, ya compuesta.
|
|
*
|
|
* Va aparte del catálogo por lo mismo que la letra: solo la usa la página de
|
|
* ese estilo, y mandarla en cada petición sería repartir cuatro textos para
|
|
* pintar una tarjeta.
|
|
*/
|
|
export async function textoDeEstilo(slug: string): Promise<string> {
|
|
const [fila] = await db
|
|
.select({ cuerpo: estilo.cuerpo })
|
|
.from(estilo)
|
|
.where(and(eq(estilo.slug, slug), eq(estilo.estado, 'publicado')))
|
|
.limit(1);
|
|
return componer(fila?.cuerpo);
|
|
}
|
|
|
|
/** Las notas de un disco, ya compuestas. Un álbum de singles no tiene. */
|
|
export async function notasDeAlbum(slug: string): Promise<string> {
|
|
const [fila] = await db
|
|
.select({ cuerpo: album.cuerpo })
|
|
.from(album)
|
|
.where(and(eq(album.slug, slug), eq(album.estado, 'publicado')))
|
|
.limit(1);
|
|
return componer(fila?.cuerpo);
|
|
}
|