Qué es un single, cómo se arma el álbum de sueltos, en qué orden van las etiquetas: veinte funciones que estaban atadas a los archivos y que habría que haber reescrito para la base. Dos copias «parecidas» son dos sitios donde un día discrepan, y el sitio ya tuvo ese problema con las anclas de los encabezados. Ahora viven en `$lib/catalogo/consultas`, reciben el catálogo como argumento y no leen nada por su cuenta. `$lib/content` las ata a los archivos y `$lib/server/catalogo` a la base; cambiar de fuente no puede cambiar el resultado. La letra completa se queda fuera del catálogo y se pide aparte: dieciocho letras enteras en cada petición para pintar una lista sería mandar cien veces lo que hace falta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>master
parent
715b28c3c0
commit
60f1253763
@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Las consultas del catálogo, sin saber de dónde salen los datos.
|
||||
*
|
||||
* Todas reciben el catálogo como primer argumento y no leen nada por su
|
||||
* cuenta. Es lo que permite que la web pase de los archivos a la base sin que
|
||||
* cambie el resultado: la lógica —qué es un single, cómo se arma el álbum de
|
||||
* sueltos, en qué orden van las etiquetas— se escribe una vez y las dos
|
||||
* fuentes la comparten. Dos copias «parecidas» habrían sido dos sitios donde
|
||||
* un día discrepan.
|
||||
*
|
||||
* Vive en `$lib/catalogo` y no en `$lib/server` porque no toca la base:
|
||||
* también la usa el módulo de contenido, que corre en el navegador.
|
||||
*/
|
||||
|
||||
import { PORTADA_SINGLES, aSlug } from '$lib/content/schema';
|
||||
import type {
|
||||
Album,
|
||||
AlbumConCanciones,
|
||||
Cancion,
|
||||
Estilo,
|
||||
EstiloConObra,
|
||||
Etiqueta
|
||||
} from '$lib/types';
|
||||
|
||||
/** Lo mínimo para responder a todo lo de este archivo. */
|
||||
export interface Catalogo {
|
||||
estilos: Estilo[];
|
||||
albumes: Album[];
|
||||
canciones: Cancion[];
|
||||
}
|
||||
|
||||
export const PREFIJO_SINGLES = 'singles-';
|
||||
|
||||
/** Slug del álbum de singles de un estilo. */
|
||||
export function slugSingles(estiloSlug: string): string {
|
||||
return `${PREFIJO_SINGLES}${estiloSlug}`;
|
||||
}
|
||||
|
||||
/** Álbum al que pertenece un tema; los sueltos caen en el de singles. */
|
||||
export function albumDeCancion(cancion: Cancion): string {
|
||||
return cancion.album ?? slugSingles(cancion.estilo);
|
||||
}
|
||||
|
||||
export function obtenerEstilo(cat: Catalogo, slug: string): Estilo | undefined {
|
||||
return cat.estilos.find((e) => e.slug === slug);
|
||||
}
|
||||
|
||||
/** El estilo principal del artista, o el primero. */
|
||||
export function estiloPrincipal(cat: Catalogo): Estilo | undefined {
|
||||
return cat.estilos.find((e) => e.principal) ?? cat.estilos[0];
|
||||
}
|
||||
|
||||
/** Canciones de un álbum declarado, en el orden del disco. */
|
||||
export function cancionesDeAlbum(cat: Catalogo, albumSlug: string): Cancion[] {
|
||||
return cat.canciones
|
||||
.filter((c) => c.album === albumSlug)
|
||||
.sort((a, b) => (a.numero ?? 0) - (b.numero ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Temas de un estilo que no pertenecen a ningún disco. Van de lo más reciente
|
||||
* a lo más antiguo: sin número de pista, la fecha es el único orden con
|
||||
* sentido.
|
||||
*/
|
||||
export function singlesDeEstilo(cat: Catalogo, estiloSlug: string): Cancion[] {
|
||||
return cat.canciones
|
||||
.filter((c) => c.album === null && c.estilo === estiloSlug)
|
||||
.sort((a, b) => (b.ano ?? 0) - (a.ano ?? 0) || a.titulo.localeCompare(b.titulo));
|
||||
}
|
||||
|
||||
function conDuracion(album: Album, temas: Cancion[]): AlbumConCanciones {
|
||||
return {
|
||||
...album,
|
||||
canciones: temas,
|
||||
duracionTotal: temas.reduce((total, c) => total + (c.duracion ?? 0), 0)
|
||||
};
|
||||
}
|
||||
|
||||
function completar(cat: Catalogo, album: Album): AlbumConCanciones {
|
||||
return conDuracion(album, cancionesDeAlbum(cat, album.slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Álbum «Singles» de un estilo: no existe como fila ni como archivo, se arma
|
||||
* con los temas sueltos para que tengan sitio en la web igual que los de un
|
||||
* disco. Devuelve `undefined` si ese estilo no tiene ninguno.
|
||||
*/
|
||||
export function albumSinglesDeEstilo(
|
||||
cat: Catalogo,
|
||||
estiloSlug: string
|
||||
): AlbumConCanciones | undefined {
|
||||
const temas = singlesDeEstilo(cat, estiloSlug);
|
||||
if (temas.length === 0) return undefined;
|
||||
|
||||
const estilo = obtenerEstilo(cat, estiloSlug);
|
||||
const anoMasReciente = Math.max(...temas.map((t) => t.ano ?? 0));
|
||||
|
||||
return conDuracion(
|
||||
{
|
||||
slug: slugSingles(estiloSlug),
|
||||
titulo: 'Singles',
|
||||
estilo: estiloSlug,
|
||||
// La fecha solo se usa para ordenar y para enseñar el año.
|
||||
fecha: `${anoMasReciente || new Date().getUTCFullYear()}-01-01`,
|
||||
tipo: 'coleccion',
|
||||
portada: PORTADA_SINGLES,
|
||||
resumen: `Temas de ${(estilo?.nombre ?? estiloSlug).toLowerCase()} publicados fuera de un disco.`,
|
||||
destacado: false,
|
||||
enlaces: {},
|
||||
sintetico: true,
|
||||
// No es una edición real: no hay disco que acreditar ni catalogar.
|
||||
edicion: { formatos: [], creditos: [] }
|
||||
},
|
||||
temas
|
||||
);
|
||||
}
|
||||
|
||||
/** Busca un álbum por slug, incluidos los de singles. */
|
||||
export function obtenerAlbum(cat: Catalogo, slug: string): AlbumConCanciones | undefined {
|
||||
if (slug.startsWith(PREFIJO_SINGLES)) {
|
||||
return albumSinglesDeEstilo(cat, slug.slice(PREFIJO_SINGLES.length));
|
||||
}
|
||||
const album = cat.albumes.find((a) => a.slug === slug);
|
||||
return album ? completar(cat, album) : undefined;
|
||||
}
|
||||
|
||||
export function obtenerCancion(cat: Catalogo, slug: string): Cancion | undefined {
|
||||
return cat.canciones.find((c) => c.slug === slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Los álbumes de un estilo. El de singles va el último: es un cajón de sastre,
|
||||
* no una referencia más, y encabezar la lista con él descolocaría la
|
||||
* discografía.
|
||||
*/
|
||||
export function albumesDeEstilo(cat: Catalogo, estiloSlug: string): AlbumConCanciones[] {
|
||||
const declarados = cat.albumes
|
||||
.filter((a) => a.estilo === estiloSlug)
|
||||
.map((a) => completar(cat, a));
|
||||
const singles = albumSinglesDeEstilo(cat, estiloSlug);
|
||||
return singles ? [...declarados, singles] : declarados;
|
||||
}
|
||||
|
||||
/** Todos los slugs de álbum que tienen página, incluidos los de singles. */
|
||||
export function slugsDeAlbum(cat: Catalogo): string[] {
|
||||
return [
|
||||
...cat.albumes.map((a) => a.slug),
|
||||
...cat.estilos
|
||||
.map((e) => slugSingles(e.slug))
|
||||
.filter((slug) => obtenerAlbum(cat, slug) !== undefined)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Los bloques de /musica: un estilo con todos sus álbumes y sus temas
|
||||
* destacados.
|
||||
*/
|
||||
export function estilosConObra(cat: Catalogo): EstiloConObra[] {
|
||||
return cat.estilos.map((estilo) => {
|
||||
const albumesDelEstilo = albumesDeEstilo(cat, estilo.slug);
|
||||
return {
|
||||
estilo,
|
||||
albumes: albumesDelEstilo,
|
||||
destacadas: albumesDelEstilo.flatMap((a) => a.canciones).filter((c) => c.destacada),
|
||||
totalCanciones: albumesDelEstilo.reduce((total, a) => total + a.canciones.length, 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function obtenerEstiloConObra(cat: Catalogo, estiloSlug: string): EstiloConObra | undefined {
|
||||
return estilosConObra(cat).find((s) => s.estilo.slug === estiloSlug);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Etiquetas
|
||||
*
|
||||
* Cruzan el catálogo por donde no lo hacen ni el estilo ni el disco: de qué
|
||||
* habla un tema, cómo suena, con quién se canta. El slug es lo que las agrupa
|
||||
* —«Desamor» y «desamor» son la misma— y el nombre que se lee es el primero
|
||||
* que apareció.
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
/** Todas las etiquetas, de más usada a menos y luego por nombre. */
|
||||
export function etiquetas(cat: Catalogo): Etiqueta[] {
|
||||
const porSlug = new Map<string, Etiqueta>();
|
||||
|
||||
for (const cancion of cat.canciones) {
|
||||
for (const nombre of cancion.etiquetas) {
|
||||
const slug = aSlug(nombre);
|
||||
if (!slug) continue;
|
||||
const etiqueta = porSlug.get(slug) ?? { slug, nombre, canciones: [] };
|
||||
etiqueta.canciones.push(cancion);
|
||||
porSlug.set(slug, etiqueta);
|
||||
}
|
||||
}
|
||||
|
||||
return [...porSlug.values()].sort(
|
||||
(a, b) => b.canciones.length - a.canciones.length || a.nombre.localeCompare(b.nombre, 'es')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Las etiquetas más usadas dentro de un estilo, de más a menos.
|
||||
*
|
||||
* Un estilo no las declara: salen de sus temas. Así no hay dos sitios que
|
||||
* mantener ni forma de que la etiqueta de un estilo contradiga a las de sus
|
||||
* canciones.
|
||||
*/
|
||||
export function etiquetasDeEstilo(cat: Catalogo, estiloSlug: string, cuantas = 4): string[] {
|
||||
const cuenta = new Map<string, { nombre: string; veces: number }>();
|
||||
|
||||
for (const cancion of cat.canciones) {
|
||||
if (cancion.estilo !== estiloSlug) continue;
|
||||
for (const nombre of cancion.etiquetas) {
|
||||
const slug = aSlug(nombre);
|
||||
if (!slug) continue;
|
||||
const actual = cuenta.get(slug) ?? { nombre, veces: 0 };
|
||||
actual.veces++;
|
||||
cuenta.set(slug, actual);
|
||||
}
|
||||
}
|
||||
|
||||
return [...cuenta.values()]
|
||||
.sort((a, b) => b.veces - a.veces || a.nombre.localeCompare(b.nombre, 'es'))
|
||||
.slice(0, cuantas)
|
||||
.map((e) => e.nombre);
|
||||
}
|
||||
|
||||
export function obtenerEtiqueta(cat: Catalogo, slug: string): Etiqueta | undefined {
|
||||
return etiquetas(cat).find((e) => e.slug === slug);
|
||||
}
|
||||
|
||||
/** Portada que corresponde a un tema: la suya propia o la de su álbum. */
|
||||
export function portadaDeCancion(cat: Catalogo, cancion: Cancion): string {
|
||||
return cancion.portada ?? obtenerAlbum(cat, albumDeCancion(cancion))?.portada ?? PORTADA_SINGLES;
|
||||
}
|
||||
Loading…
Reference in new issue