El catálogo se sirve de la base: música, canciones, discos y etiquetas

Lo que bloqueaba la fase 4 era el cliente, no las rutas: el catálogo
viajaba en el paquete de JavaScript y las tarjetas, el carrito, la cuenta
y el reproductor lo consultaban por su cuenta. Baja por `page.data` desde
el layout —veintiún kilobytes, medidos, menos de lo que ocupaba en el
paquete— y `$lib/catalogo/cliente.svelte` lo ata a las mismas consultas
que usa el servidor. La otra opción era pasar por props lo que cada
componente busca solo, y eso obligaba a tocar la compra, el carrito y el
reproductor a la vez.

Siete rutas dejan de ser cargadores universales: música, estilo, álbum,
canción, portada y las dos de etiquetas. La ficha del tema se resuelve
entera en el servidor, incluida la letra, que se pide sola y no viaja con
el catálogo.

Faltaba dónde guardar dos textos: la introducción de cada estilo y las
notas de cada disco eran el cuerpo de su Markdown y el volcado se quedó
solo con el frontmatter. Migración 0010 y `npm run db:completar` los
rellena.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
master
dev 4 weeks ago
parent 771e97c4e5
commit 0b823d44b4

@ -0,0 +1,9 @@
-- El texto de los estilos y de los discos.
--
-- La introducción de cada estilo y las notas de cada disco eran el cuerpo de
-- su Markdown, y el volcado se quedó solo con el frontmatter: no había columna
-- donde ponerlas. Se vio al ir a cambiar las rutas, que las pedían como
-- componente compilado.
ALTER TABLE "album" ADD COLUMN "cuerpo" text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE "estilo" ADD COLUMN "cuerpo" text DEFAULT '' NOT NULL;

File diff suppressed because it is too large Load Diff

@ -71,6 +71,13 @@
"when": 1788890122515, "when": 1788890122515,
"tag": "0009_nota_de_version_y_orden_de_etiquetas", "tag": "0009_nota_de_version_y_orden_de_etiquetas",
"breakpoints": true "breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1788898884661,
"tag": "0010_texto_de_estilos_y_discos",
"breakpoints": true
} }
] ]
} }

@ -34,7 +34,13 @@ function partir(texto) {
if (lineas[0]?.trim() !== '---') return { datos: {}, cuerpo: texto.trim() }; if (lineas[0]?.trim() !== '---') return { datos: {}, cuerpo: texto.trim() };
const cierre = lineas.indexOf('---', 1); const cierre = lineas.indexOf('---', 1);
if (cierre === -1) return { datos: {}, cuerpo: texto.trim() }; if (cierre === -1) return { datos: {}, cuerpo: texto.trim() };
return { datos: leerYaml(lineas.slice(1, cierre).join(SALTO)) ?? {}, cuerpo: '' }; return {
datos: leerYaml(lineas.slice(1, cierre).join(SALTO)) ?? {},
cuerpo: lineas
.slice(cierre + 1)
.join(SALTO)
.trim()
};
} }
function leerCarpeta(carpeta) { function leerCarpeta(carpeta) {
@ -44,10 +50,10 @@ function leerCarpeta(carpeta) {
.readdirSync(dir) .readdirSync(dir)
.filter((n) => n.endsWith('.md')) .filter((n) => n.endsWith('.md'))
.sort() .sort()
.map((nombre) => ({ .map((nombre) => {
slug: nombre.replace(new RegExp(String.raw`\.md$`), ''), const { datos, cuerpo } = partir(fs.readFileSync(path.join(dir, nombre), 'utf8'));
datos: partir(fs.readFileSync(path.join(dir, nombre), 'utf8')).datos return { slug: nombre.replace(new RegExp(String.raw`\.md$`), ''), datos, cuerpo };
})); });
} }
const c = new pg.Client({ connectionString: process.env.DATABASE_URL, query_timeout: 60_000 }); const c = new pg.Client({ connectionString: process.env.DATABASE_URL, query_timeout: 60_000 });
@ -60,6 +66,8 @@ const contado = {
portadaCancion: 0, portadaCancion: 0,
portadaAlbum: 0, portadaAlbum: 0,
paisAlbum: 0, paisAlbum: 0,
textoEstilo: 0,
textoAlbum: 0,
nota: 0, nota: 0,
orden: 0 orden: 0
}; };
@ -182,9 +190,25 @@ try {
} }
} }
/* --- Estilos: su introducción ------------------------------------------ */
const { rows: estilos } = await c.query('SELECT id, slug FROM estilo');
const idDeEstilo = new Map(estilos.map((f) => [f.slug, f.id]));
for (const { slug, cuerpo } of leerCarpeta('estilos')) {
const estiloId = idDeEstilo.get(slug);
if (!estiloId) {
avisos.push(`el estilo «${slug}» no está en la base`);
continue;
}
if (!cuerpo) continue;
await c.query('UPDATE estilo SET cuerpo = $2 WHERE id = $1', [estiloId, cuerpo]);
contado.textoEstilo++;
}
/* --- Álbumes ------------------------------------------------------------ */ /* --- Álbumes ------------------------------------------------------------ */
for (const { slug, datos } of leerCarpeta('albumes')) { for (const { slug, datos, cuerpo } of leerCarpeta('albumes')) {
const albumId = idDeAlbum.get(slug); const albumId = idDeAlbum.get(slug);
if (!albumId) { if (!albumId) {
avisos.push(`el álbum «${slug}» no está en la base`); avisos.push(`el álbum «${slug}» no está en la base`);
@ -202,9 +226,11 @@ try {
await c.query( await c.query(
`UPDATE album SET portada_id = COALESCE($2, portada_id), `UPDATE album SET portada_id = COALESCE($2, portada_id),
pais_edicion_codigo = COALESCE($3, pais_edicion_codigo) WHERE id = $1`, pais_edicion_codigo = COALESCE($3, pais_edicion_codigo),
[albumId, portada ?? null, codigoPais ?? null] cuerpo = COALESCE($4, cuerpo) WHERE id = $1`,
[albumId, portada ?? null, codigoPais ?? null, cuerpo || null]
); );
if (cuerpo) contado.textoAlbum++;
if (portada) contado.portadaAlbum++; if (portada) contado.portadaAlbum++;
if (codigoPais) contado.paisAlbum++; if (codigoPais) contado.paisAlbum++;
} }

@ -0,0 +1,55 @@
/**
* El catálogo en el navegador.
*
* Las mismas consultas de `./consultas`, atadas a lo que baja por
* `page.data.catalogo`. Antes venían de `$lib/content`, que cargaba los
* archivos al construir y por eso podía consultarse desde cualquier parte sin
* pedir nada: el catálogo entero viajaba en el paquete de JavaScript.
*
* Con el catálogo en la base eso deja de valer, y hay dos formas de arreglarlo:
* que cada componente reciba por props lo que hoy busca solo, o que el catálogo
* baje una vez y siga estando a mano. Se eligió lo segundo porque son
* veintiún kilobytes —menos de lo que ocupaba en el paquete— y porque lo
* primero obligaba a tocar la compra, el carrito y el reproductor a la vez.
*
* Es `.svelte.ts` porque `page` es estado con runes: lo que se lee aquí dentro
* es reactivo donde se use.
*/
import { page } from '$app/state';
import * as q from './consultas';
import type { Catalogo } from './consultas';
import type { Cancion } from '$lib/types';
const VACIO: Catalogo = { estilos: [], albumes: [], canciones: [] };
/**
* Lo que haya bajado, o nada.
*
* El vacío no es un caso hipotético: la página de espera no carga catálogo, y
* antes de que resuelva el `load` de la primera navegación tampoco hay. Sin
* este respaldo, una tarjeta reventaba en vez de pintarse sin portada.
*/
function cat(): Catalogo {
return (page.data.catalogo as Catalogo | undefined) ?? VACIO;
}
export const estilos = () => cat().estilos;
export const albumes = () => cat().albumes;
export const canciones = () => cat().canciones;
export const obtenerEstilo = (slug: string) => q.obtenerEstilo(cat(), slug);
export const estiloPrincipal = () => q.estiloPrincipal(cat());
export const obtenerAlbum = (slug: string) => q.obtenerAlbum(cat(), slug);
export const obtenerCancion = (slug: string) => q.obtenerCancion(cat(), slug);
export const albumesDeEstilo = (estiloSlug: string) => q.albumesDeEstilo(cat(), estiloSlug);
export const estilosConObra = () => q.estilosConObra(cat());
export const etiquetas = () => q.etiquetas(cat());
export const etiquetasDeEstilo = (estiloSlug: string, cuantas = 4) =>
q.etiquetasDeEstilo(cat(), estiloSlug, cuantas);
export const obtenerEtiqueta = (slug: string) => q.obtenerEtiqueta(cat(), slug);
export const portadaDeCancion = (cancion: Cancion) => q.portadaDeCancion(cat(), cancion);
/* Estas dos no necesitan catálogo: son de la propia canción. */
export const albumDeCancion = q.albumDeCancion;
export const slugSingles = q.slugSingles;

@ -13,7 +13,7 @@
*/ */
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import Valoracion from './Valoracion.svelte'; import Valoracion from './Valoracion.svelte';
import { slugDeEtiqueta } from '$lib/content'; import { aSlug as slugDeEtiqueta } from '$lib/content/schema';
import type { ResumenDeValoracion } from '$lib/server/valoraciones'; import type { ResumenDeValoracion } from '$lib/server/valoraciones';
import type { AlbumConCanciones, Cancion, Estilo } from '$lib/types'; import type { AlbumConCanciones, Cancion, Estilo } from '$lib/types';
import { listaDeNombres, segundosADuracion } from '$lib/utils/format'; import { listaDeNombres, segundosADuracion } from '$lib/utils/format';

@ -16,7 +16,12 @@
import BotonReproducir from './BotonReproducir.svelte'; import BotonReproducir from './BotonReproducir.svelte';
import { brillo } from '$lib/acciones/brillo'; import { brillo } from '$lib/acciones/brillo';
import { revelar } from '$lib/acciones/revelar'; import { revelar } from '$lib/acciones/revelar';
import { albumDeCancion, obtenerAlbum, obtenerEstilo, portadaDeCancion } from '$lib/content'; import {
albumDeCancion,
obtenerAlbum,
obtenerEstilo,
portadaDeCancion
} from '$lib/catalogo/cliente.svelte';
import type { PistaReproductor } from '$lib/player/player.svelte'; import type { PistaReproductor } from '$lib/player/player.svelte';
import type { Cancion } from '$lib/types'; import type { Cancion } from '$lib/types';

@ -15,7 +15,7 @@
import { brillo } from '$lib/acciones/brillo'; import { brillo } from '$lib/acciones/brillo';
import { revelar } from '$lib/acciones/revelar'; import { revelar } from '$lib/acciones/revelar';
import { aPistas } from '$lib/player/pista'; import { aPistas } from '$lib/player/pista';
import { etiquetasDeEstilo } from '$lib/content'; import { etiquetasDeEstilo } from '$lib/catalogo/cliente.svelte';
import { PORTADA_POR_DEFECTO } from '$lib/content/schema'; import { PORTADA_POR_DEFECTO } from '$lib/content/schema';
import type { EstiloConObra } from '$lib/types'; import type { EstiloConObra } from '$lib/types';

@ -10,13 +10,24 @@
*/ */
import { urlMuestra } from '$lib/audio'; import { urlMuestra } from '$lib/audio';
import { albumDeCancion, estilos, obtenerAlbum, portadaDeCancion } from '$lib/content'; /*
* Del catálogo que baja por `page.data`, no de los archivos. `aPistas` se
* llama siempre dentro de un `$derived` de un componente, así que la lectura
* de `page` ocurre en contexto reactivo y la cola se rehace sola si el
* catálogo cambia.
*/
import {
albumDeCancion,
estilos,
obtenerAlbum,
portadaDeCancion
} from '$lib/catalogo/cliente.svelte';
import type { Cancion, VersionMusical } from '$lib/types'; import type { Cancion, VersionMusical } from '$lib/types';
import type { PistaReproductor } from './player.svelte'; import type { PistaReproductor } from './player.svelte';
function contexto(cancion: Cancion) { function contexto(cancion: Cancion) {
const albumSlug = albumDeCancion(cancion); const albumSlug = albumDeCancion(cancion);
const estilo = estilos.find((e) => e.slug === cancion.estilo); const estilo = estilos().find((e) => e.slug === cancion.estilo);
return { return {
albumSlug, albumSlug,
albumTitulo: obtenerAlbum(albumSlug)?.titulo ?? '', albumTitulo: obtenerAlbum(albumSlug)?.titulo ?? '',

@ -46,6 +46,7 @@ import {
video video
} from './db/schema'; } from './db/schema';
import { extraerLineas } from '$lib/content/extracto'; import { extraerLineas } from '$lib/content/extracto';
import { componer } from './markdown';
import * as q from '$lib/catalogo/consultas'; import * as q from '$lib/catalogo/consultas';
import type { Catalogo } from '$lib/catalogo/consultas'; import type { Catalogo } from '$lib/catalogo/consultas';
import type { import type {
@ -422,6 +423,13 @@ export async function obtenerEtiqueta(slug: string) {
return q.obtenerEtiqueta(await leerCatalogo(), slug); 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. * La letra completa de un tema, que no viaja con el catálogo.
* *
@ -437,3 +445,29 @@ export async function letraDeCancion(slug: string): Promise<string | null> {
.limit(1); .limit(1);
return fila?.letra ?? null; 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(eq(estilo.slug, slug))
.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(eq(album.slug, slug))
.limit(1);
return componer(fila?.cuerpo);
}

@ -38,6 +38,8 @@ export const estilo = pgTable(
slug: text('slug').notNull().unique(), slug: text('slug').notNull().unique(),
nombre: text('nombre').notNull(), nombre: text('nombre').notNull(),
resumen: text('resumen').notNull().default(''), resumen: text('resumen').notNull().default(''),
/** La introducción del bloque, en Markdown. Era el cuerpo de su `.md`. */
cuerpo: text('cuerpo').notNull().default(''),
/** Menor, más arriba. */ /** Menor, más arriba. */
orden: integer('orden').notNull().default(0), orden: integer('orden').notNull().default(0),
/** El estilo principal del artista. Solo puede haber uno: ver el índice. */ /** El estilo principal del artista. Solo puede haber uno: ver el índice. */
@ -81,6 +83,8 @@ export const album = pgTable(
portadaId: text('portada_id').references(() => medio.id, { onDelete: 'set null' }), portadaId: text('portada_id').references(() => medio.id, { onDelete: 'set null' }),
sello: text('sello'), sello: text('sello'),
resumen: text('resumen').notNull().default(''), resumen: text('resumen').notNull().default(''),
/** Las notas del disco, en Markdown. Era el cuerpo de su `.md`. */
cuerpo: text('cuerpo').notNull().default(''),
destacado: boolean('destacado').notNull().default(false), destacado: boolean('destacado').notNull().default(false),
/* --- Datos de edición: lo que iría en la contraportada --------------- */ /* --- Datos de edición: lo que iría en la contraportada --------------- */

@ -3,6 +3,7 @@ import { favoritosDeUsuario } from '$lib/server/favoritos';
import { comprasDeUsuario } from '$lib/server/pedidos'; import { comprasDeUsuario } from '$lib/server/pedidos';
import { proveedoresDisponibles } from '$lib/server/oauth'; import { proveedoresDisponibles } from '$lib/server/oauth';
import { indiceLegal } from '$lib/server/legal'; import { indiceLegal } from '$lib/server/legal';
import { leerCatalogo } from '$lib/server/catalogo';
import { RUTA_ESPERA } from '$lib/rutas'; import { RUTA_ESPERA } from '$lib/rutas';
/** /**
@ -30,7 +31,8 @@ export async function load({ locals, cookies, url }) {
compras: [] as string[], compras: [] as string[],
favoritos: [] as string[], favoritos: [] as string[],
proveedores: [], proveedores: [],
legales: [] legales: [],
catalogo: { estilos: [], albumes: [], canciones: [] }
}; };
} }
@ -58,6 +60,18 @@ export async function load({ locals, cookies, url }) {
* viven en la base tienen que bajar por aquí, porque un componente no * viven en la base tienen que bajar por aquí, porque un componente no
* puede consultar. * puede consultar.
*/ */
legales: await indiceLegal() legales: await indiceLegal(),
/*
* El catálogo entero, una vez. Hasta ahora viajaba en el paquete de
* JavaScript —lo cargaba `import.meta.glob` al construir— y por eso
* cualquier componente podía consultarlo sin pedir nada: las tarjetas
* buscan su portada, el carrito resuelve sus slugs, el reproductor arma
* la cola. Con el catálogo en la base eso deja de valer, y bajarlo aquí
* es lo que evita tocar la compra y el reproductor a la vez.
*
* Son veintiún kilobytes, menos de lo que ocupaba en el paquete. La
* letra completa no va: esa la pide la ficha de cada tema.
*/
catalogo: await leerCatalogo()
}; };
} }

@ -0,0 +1,36 @@
import {
albumesDeEstilo,
estiloPrincipal,
estilosConObra,
etiquetasDeEstilo,
leerCatalogo,
obtenerAlbum,
obtenerEstiloConObra
} from '$lib/server/catalogo';
import { heroe } from '$lib/site';
/**
* La portada cuenta el catálogo en tres pasos: el estilo principal, sus discos
* y el resto de estilos. No lleva selecciones ni novedades: el catálogo es
* pequeño y repetir las mismas tarjetas tres veces lo hace parecer aún más
* corto de lo que es.
*/
export async function load() {
const principal = await estiloPrincipal();
const { albumes } = await leerCatalogo();
return {
/** El disco que se pone en el tocadiscos de la cabecera. */
heroe: (await obtenerAlbum(heroe.album)) ?? (await obtenerAlbum(albumes[0]?.slug ?? '')),
bloquePrincipal: principal ? await obtenerEstiloConObra(principal.slug) : undefined,
/** De qué se habla en el estilo principal, sacado de sus temas. */
etiquetasPrincipal: principal ? await etiquetasDeEstilo(principal.slug, 6) : [],
/** Lo demás, como tarjetas: cuánto hay en cada uno y de qué va. */
otrosBloques: (await estilosConObra()).filter((b) => b.estilo.slug !== principal?.slug)
};
}
// Se importa para que quede claro que no hace falta: la portada no lo usa.
void albumesDeEstilo;

@ -10,7 +10,7 @@
import TarjetaEstilo from '$lib/components/TarjetaEstilo.svelte'; import TarjetaEstilo from '$lib/components/TarjetaEstilo.svelte';
import { revelar } from '$lib/acciones/revelar'; import { revelar } from '$lib/acciones/revelar';
import { aPistas } from '$lib/player/pista'; import { aPistas } from '$lib/player/pista';
import { slugDeEtiqueta } from '$lib/content'; import { aSlug as slugDeEtiqueta } from '$lib/content/schema';
import { heroe, site } from '$lib/site'; import { heroe, site } from '$lib/site';
import { CANCIONES_POR_PACK, formatearPrecio, PRECIO_PACK_CENTIMOS } from '$lib/tienda'; import { CANCIONES_POR_PACK, formatearPrecio, PRECIO_PACK_CENTIMOS } from '$lib/tienda';

@ -1,32 +0,0 @@
import {
albumes,
estiloPrincipal,
estilosConObra,
etiquetasDeEstilo,
obtenerAlbum,
obtenerEstiloConObra
} from '$lib/content';
import { heroe } from '$lib/site';
/**
* La portada cuenta el catalogo en tres pasos: el estilo principal, sus discos
* y el resto de estilos. No lleva selecciones ni novedades: el catalogo es
* pequeno y repetir las mismas tarjetas tres veces lo hace parecer aun mas
* corto de lo que es.
*/
export function load() {
const principal = estiloPrincipal();
const bloquePrincipal = principal ? obtenerEstiloConObra(principal.slug) : undefined;
return {
/** El disco que se pone en el tocadiscos de la cabecera. */
heroe: obtenerAlbum(heroe.album) ?? obtenerAlbum(albumes[0]?.slug ?? ''),
bloquePrincipal,
/** De que se habla en el estilo principal, sacado de sus temas. */
etiquetasPrincipal: principal ? etiquetasDeEstilo(principal.slug, 6) : [],
/** Lo demas, como tarjetas: cuanto hay en cada uno y de que va. */
otrosBloques: estilosConObra().filter((b) => b.estilo.slug !== principal?.slug)
};
}

@ -0,0 +1,14 @@
import { error } from '@sveltejs/kit';
import { notasDeAlbum, obtenerAlbum, obtenerEstilo } from '$lib/server/catalogo';
export async function load({ params }) {
const album = await obtenerAlbum(params.slug);
if (!album) error(404, 'Ese album no existe.');
return {
album,
estilo: await obtenerEstilo(album.estilo),
// Un álbum de singles no existe como fila, así que tampoco tiene notas.
notas: album.sintetico ? '' : await notasDeAlbum(params.slug)
};
}

@ -14,7 +14,7 @@
let { data } = $props(); let { data } = $props();
const album = $derived(data.album); const album = $derived(data.album);
const Notas = $derived(data.Notas); const notas = $derived(data.notas);
const pistas = $derived(aPistas(album.canciones)); const pistas = $derived(aPistas(album.canciones));
const hayEdicion = $derived( const hayEdicion = $derived(
@ -157,10 +157,12 @@
{/if} {/if}
{/if} {/if}
{#if Notas} {#if notas}
<section class="notas"> <section class="notas">
<h2>Sobre el disco</h2> <h2>Sobre el disco</h2>
<Notas /> <!-- Compuesto en el servidor; ver `$lib/server/markdown`. -->
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
<div>{@html notas}</div>
</section> </section>
{/if} {/if}
</div> </div>

@ -1,14 +0,0 @@
import { error } from '@sveltejs/kit';
import { cuerpoAlbum, obtenerAlbum, obtenerEstilo } from '$lib/content';
export function load({ params }) {
const album = obtenerAlbum(params.slug);
if (!album) error(404, 'Ese album no existe.');
return {
album,
estilo: obtenerEstilo(album.estilo),
// Un album de singles no tiene archivo propio, asi que tampoco notas.
Notas: album.sintetico ? undefined : cuerpoAlbum(params.slug)
};
}

@ -2,7 +2,7 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import Meta from '$lib/components/Meta.svelte'; import Meta from '$lib/components/Meta.svelte';
import RejillaCanciones from '$lib/components/RejillaCanciones.svelte'; import RejillaCanciones from '$lib/components/RejillaCanciones.svelte';
import { slugDeEtiqueta } from '$lib/content'; import { aSlug as slugDeEtiqueta } from '$lib/content/schema';
import { formatearFecha } from '$lib/utils/date'; import { formatearFecha } from '$lib/utils/date';
import Migas from '$lib/components/Migas.svelte'; import Migas from '$lib/components/Migas.svelte';

@ -1,5 +1,13 @@
import { fail } from '@sveltejs/kit'; import { error, fail } from '@sveltejs/kit';
import { obtenerCancion } from '$lib/content'; import {
albumDeCancion,
letraDeCancion,
obtenerAlbum,
obtenerCancion,
obtenerEstilo,
portadaDeCancion
} from '$lib/server/catalogo';
import { componer } from '$lib/server/markdown';
import { import {
aPuntuacion, aPuntuacion,
resumenDeCancion, resumenDeCancion,
@ -9,15 +17,45 @@ import {
} from '$lib/server/valoraciones'; } from '$lib/server/valoraciones';
/** /**
* Lo unico que la ficha necesita del servidor son las valoraciones: el resto * La ficha entera sale de aquí desde que el catálogo está en la base. Antes
* del tema es contenido compilado y no toca la base de datos. * este archivo solo traía las valoraciones y el resto lo ponía un cargador
* universal, que leía el catálogo compilado; ese ya no existe.
*/ */
export async function load({ params, locals }) { export async function load({ params, locals }) {
const usuarioId = locals.usuario?.id; const usuarioId = locals.usuario?.id;
const cancion = await obtenerCancion(params.slug);
if (!cancion) error(404, 'Ese tema no existe.');
/*
* El álbum completo hace de contexto de reproducción: darle al play aquí
* deja sonando el disco entero, no solo este tema. En un single suelto, el
* contexto es el álbum de singles de su estilo.
*/
const album = await obtenerAlbum(albumDeCancion(cancion));
const hermanas = album?.canciones ?? [cancion];
const posicion = hermanas.findIndex((c) => c.slug === cancion.slug);
const [valoracion, miValoracion, estilo, portada, letra] = await Promise.all([
resumenDeCancion(params.slug),
usuarioId ? valoracionDe(usuarioId, params.slug) : undefined,
obtenerEstilo(cancion.estilo),
portadaDeCancion(cancion),
letraDeCancion(params.slug)
]);
return { return {
valoracion: await resumenDeCancion(params.slug), cancion,
miValoracion: usuarioId ? await valoracionDe(usuarioId, params.slug) : undefined album,
estilo,
portada,
hermanas,
anterior: posicion > 0 ? hermanas[posicion - 1] : undefined,
siguiente: posicion < hermanas.length - 1 ? hermanas[posicion + 1] : undefined,
/** La letra, o las notas si el tema es instrumental. */
letra: componer(letra),
valoracion,
miValoracion
}; };
} }
@ -28,9 +66,10 @@ export const actions = {
if (!locals.usuario) { if (!locals.usuario) {
return fail(401, { valoracion: { error: 'Entra con tu cuenta para valorar.' } }); return fail(401, { valoracion: { error: 'Entra con tu cuenta para valorar.' } });
} }
// Un slug que no existe no se valora: la tabla no tiene clave foranea // Un slug que no existe no se valora. Desde la fase 3 la tabla tiene
// contra el catalogo, asi que esto es lo unico que lo impide. // clave foránea contra `cancion`, así que esto es lo que convierte un
if (!obtenerCancion(params.slug)) { // error de base de datos en un 404 con su mensaje.
if (!(await obtenerCancion(params.slug))) {
return fail(404, { valoracion: { error: 'Ese tema no existe.' } }); return fail(404, { valoracion: { error: 'Ese tema no existe.' } });
} }

@ -16,7 +16,7 @@
let { data } = $props(); let { data } = $props();
const cancion = $derived(data.cancion); const cancion = $derived(data.cancion);
const Cuerpo = $derived(data.Cuerpo); const letra = $derived(data.letra);
/** El contexto es el álbum entero; el índice, la posición del tema en él. */ /** El contexto es el álbum entero; el índice, la posición del tema en él. */
const pistas = $derived(aPistas(data.hermanas)); const pistas = $derived(aPistas(data.hermanas));
@ -160,13 +160,15 @@
<Pestanas {pestanas} etiqueta="Contenido del tema"> <Pestanas {pestanas} etiqueta="Contenido del tema">
{#snippet panel(id)} {#snippet panel(id)}
{#if id === 'letra'} {#if id === 'letra'}
{#if Cuerpo} {#if letra}
<div class="letra" class:pareada={aDosColumnas}> <div class="letra" class:pareada={aDosColumnas}>
<div class="texto" class:versos={!cancion.instrumental} lang={cancion.idioma}> <div class="texto" class:versos={!cancion.instrumental} lang={cancion.idioma}>
{#if aDosColumnas} {#if aDosColumnas}
<p class="rotulo-columna muted">Letra original · {nombreIdioma}</p> <p class="rotulo-columna muted">Letra original · {nombreIdioma}</p>
{/if} {/if}
<Cuerpo /> <!-- Compuesta en el servidor; ver `$lib/server/markdown`. -->
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html letra}
</div> </div>
{#if aDosColumnas} {#if aDosColumnas}

@ -1,35 +0,0 @@
import { error } from '@sveltejs/kit';
import {
albumDeCancion,
cuerpoCancion,
obtenerAlbum,
obtenerCancion,
obtenerEstilo,
portadaDeCancion
} from '$lib/content';
export function load({ params, data }) {
const cancion = obtenerCancion(params.slug);
if (!cancion) error(404, 'Ese tema no existe.');
// El album completo hace de contexto de reproduccion: darle al play aqui
// deja sonando el disco entero, no solo este tema. En un single suelto, el
// contexto es el album de singles de su estilo.
const album = obtenerAlbum(albumDeCancion(cancion));
const hermanas = album?.canciones ?? [cancion];
const posicion = hermanas.findIndex((c) => c.slug === cancion.slug);
return {
// Las valoraciones vienen de `+page.server.ts`; lo demas, del catalogo.
...data,
cancion,
album,
estilo: obtenerEstilo(cancion.estilo),
portada: portadaDeCancion(cancion),
hermanas,
anterior: posicion > 0 ? hermanas[posicion - 1] : undefined,
siguiente: posicion < hermanas.length - 1 ? hermanas[posicion + 1] : undefined,
/** Cuerpo del Markdown: la letra, o las notas si es instrumental. */
Cuerpo: cuerpoCancion(params.slug)
};
}

@ -8,7 +8,7 @@
obtenerAlbum, obtenerAlbum,
obtenerCancion, obtenerCancion,
portadaDeCancion portadaDeCancion
} from '$lib/content'; } from '$lib/catalogo/cliente.svelte';
import { import {
CANCIONES_POR_PACK, CANCIONES_POR_PACK,
formatearPrecio, formatearPrecio,
@ -35,7 +35,7 @@
const sugerencias = $derived( const sugerencias = $derived(
resumen.faltan === 0 resumen.faltan === 0
? [] ? []
: canciones : canciones()
.filter( .filter(
(c) => c.alaVenta && !data.carrito.includes(c.slug) && !data.compras.includes(c.slug) (c) => c.alaVenta && !data.carrito.includes(c.slug) && !data.compras.includes(c.slug)
) )

@ -3,7 +3,12 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import Meta from '$lib/components/Meta.svelte'; import Meta from '$lib/components/Meta.svelte';
import { urlDescarga } from '$lib/audio'; import { urlDescarga } from '$lib/audio';
import { albumDeCancion, obtenerAlbum, obtenerCancion, portadaDeCancion } from '$lib/content'; import {
albumDeCancion,
obtenerAlbum,
obtenerCancion,
portadaDeCancion
} from '$lib/catalogo/cliente.svelte';
import { formatearPrecio } from '$lib/tienda'; import { formatearPrecio } from '$lib/tienda';
import type { Cancion } from '$lib/types'; import type { Cancion } from '$lib/types';
import { formatearFecha } from '$lib/utils/date'; import { formatearFecha } from '$lib/utils/date';

@ -0,0 +1,5 @@
import { etiquetas } from '$lib/server/catalogo';
export async function load() {
return { etiquetas: await etiquetas() };
}

@ -1,5 +0,0 @@
import { etiquetas } from '$lib/content';
export function load() {
return { etiquetas: etiquetas() };
}

@ -0,0 +1,9 @@
import { error } from '@sveltejs/kit';
import { obtenerEtiqueta } from '$lib/server/catalogo';
export async function load({ params }) {
const etiqueta = await obtenerEtiqueta(params.etiqueta);
if (!etiqueta) error(404, 'Esa etiqueta no existe.');
return { etiqueta };
}

@ -1,9 +0,0 @@
import { error } from '@sveltejs/kit';
import { obtenerEtiqueta } from '$lib/content';
export function load({ params }) {
const etiqueta = obtenerEtiqueta(params.etiqueta);
if (!etiqueta) error(404, 'Esa etiqueta no existe.');
return { etiqueta };
}

@ -14,7 +14,7 @@
import { page } from '$app/state'; import { page } from '$app/state';
import Meta from '$lib/components/Meta.svelte'; import Meta from '$lib/components/Meta.svelte';
import RejillaCanciones from '$lib/components/RejillaCanciones.svelte'; import RejillaCanciones from '$lib/components/RejillaCanciones.svelte';
import { obtenerCancion } from '$lib/content'; import { obtenerCancion } from '$lib/catalogo/cliente.svelte';
let { data } = $props(); let { data } = $props();

@ -0,0 +1,10 @@
import { estilosConObra } from '$lib/server/catalogo';
/**
* Los estilos se cargan una vez para toda la sección: la portada de música los
* necesita para pintar los bloques y cada página de estilo, para la barra
* lateral. Al resolverlo aquí, cambiar de estilo no vuelve a calcularlos.
*/
export async function load() {
return { bloques: await estilosConObra() };
}

@ -1,10 +0,0 @@
import { estilosConObra } from '$lib/content';
/**
* Los estilos se cargan una vez para toda la seccion: la portada de musica los
* necesita para pintar los bloques y cada pagina de estilo, para la barra
* lateral. Al resolverlo aqui, cambiar de estilo no vuelve a calcularlos.
*/
export function load() {
return { bloques: estilosConObra() };
}

@ -1,5 +0,0 @@
import { estilosConObra } from '$lib/content';
export function load() {
return { bloques: estilosConObra() };
}

@ -0,0 +1,10 @@
import { error } from '@sveltejs/kit';
import { obtenerEstiloConObra, textoDeEstilo } from '$lib/server/catalogo';
export async function load({ params }) {
const bloque = await obtenerEstiloConObra(params.estilo);
if (!bloque) error(404, 'Ese estilo no existe.');
// El texto llega compuesto: antes era un componente que compilaba mdsvex.
return { bloque, introduccion: await textoDeEstilo(params.estilo) };
}

@ -7,7 +7,7 @@
let { data } = $props(); let { data } = $props();
const estilo = $derived(data.bloque.estilo); const estilo = $derived(data.bloque.estilo);
const Introduccion = $derived(data.Introduccion); const introduccion = $derived(data.introduccion);
const migas = $derived([{ texto: 'Música', href: resolve('/musica') }, { texto: estilo.nombre }]); const migas = $derived([{ texto: 'Música', href: resolve('/musica') }, { texto: estilo.nombre }]);
</script> </script>
@ -20,10 +20,10 @@
<Migas {migas} /> <Migas {migas} />
{#if Introduccion} {#if introduccion}
<div class="introduccion"> <!-- Compuesto en el servidor; ver `$lib/server/markdown`. -->
<Introduccion /> <!-- eslint-disable-next-line svelte/no-at-html-tags -->
</div> <div class="introduccion">{@html introduccion}</div>
{/if} {/if}
<BloqueDeEstilo bloque={data.bloque} contexto="pagina" /> <BloqueDeEstilo bloque={data.bloque} contexto="pagina" />

@ -1,13 +0,0 @@
import { error } from '@sveltejs/kit';
import { cuerpoEstilo, obtenerEstiloConObra } from '$lib/content';
export function load({ params }) {
const bloque = obtenerEstiloConObra(params.estilo);
if (!bloque) error(404, 'Ese estilo no existe.');
return {
bloque,
/** Texto introductorio del estilo; el cuerpo del .md, si lo tiene. */
Introduccion: cuerpoEstilo(params.estilo)
};
}
Loading…
Cancel
Save

Powered by TurnKey Linux.