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.
senzapaura_es/scripts/db/completar-catalogo.mjs

227 lines
8.1 KiB

/**
* Ata los cabos que el volcado dejó sueltos.
*
* npm run db:completar
*
* El volcado del catálogo y el de medios se hicieron por separado: el primero
* creó las canciones y el segundo las filas de `medio` con su sha256 y su
* duración, pero **nadie enlazó unas con otras**. Las veintitrés canciones
* tenían `audio_id`, `muestra_id` y `portada_id` a nulo, así que la web habría
* dejado de poder reproducir nada en cuanto leyera de la base.
*
* No se vio al volcar porque el volcado comprobaba las letras carácter a
* carácter —que era lo delicado— y no que las relaciones estuvieran puestas.
* Se vio al comparar el catálogo entero contra los archivos, que es lo que
* hace `npm run db:comparar`.
*
* Rellena, además, lo que no tenía dónde ir hasta la migración 0009: la nota de
* cada versión y el orden en que se escribieron las etiquetas.
*
* Es idempotente: son `UPDATE`. Se puede volver a pasar cuando haga falta.
*/
import fs from 'node:fs';
import path from 'node:path';
import { load as leerYaml } from 'js-yaml';
import pg from 'pg';
const RAIZ = 'src/content';
const SALTO = String.fromCharCode(10);
/** Separa el frontmatter del cuerpo, igual que el volcado. */
function partir(texto) {
const lineas = texto.split(new RegExp(String.raw`\r?\n`));
if (lineas[0]?.trim() !== '---') return { datos: {}, cuerpo: texto.trim() };
const cierre = lineas.indexOf('---', 1);
if (cierre === -1) return { datos: {}, cuerpo: texto.trim() };
return { datos: leerYaml(lineas.slice(1, cierre).join(SALTO)) ?? {}, cuerpo: '' };
}
function leerCarpeta(carpeta) {
const dir = path.join(RAIZ, carpeta);
if (!fs.existsSync(dir)) return [];
return fs
.readdirSync(dir)
.filter((n) => n.endsWith('.md'))
.sort()
.map((nombre) => ({
slug: nombre.replace(new RegExp(String.raw`\.md$`), ''),
datos: partir(fs.readFileSync(path.join(dir, nombre), 'utf8')).datos
}));
}
const c = new pg.Client({ connectionString: process.env.DATABASE_URL, query_timeout: 60_000 });
await c.connect();
await c.query('BEGIN');
const contado = {
audio: 0,
muestra: 0,
portadaCancion: 0,
portadaAlbum: 0,
paisAlbum: 0,
nota: 0,
orden: 0
};
const avisos = [];
try {
/* --- Índices de lo que ya hay ----------------------------------------- */
const { rows: medios } = await c.query('SELECT id, clave FROM medio');
const idDeMedio = new Map(medios.map((m) => [m.clave, m.id]));
const { rows: canciones } = await c.query(
'SELECT id, slug, version_de_id, version_nombre FROM cancion'
);
const idDeCancion = new Map(canciones.filter((f) => !f.version_de_id).map((f) => [f.slug, f.id]));
/*
* Las versiones se buscan por su nombre y no por el slug: el volcado lo
* compuso a partir del nombre —«Versión orquestal» →
* `como-si-nada-version-orquestal`— y rehacer esa cuenta aquí sería tener
* dos formas de calcular lo mismo, que es como acaban discrepando.
*/
const versionesDe = new Map();
for (const fila of canciones.filter((f) => f.version_de_id)) {
const suyas = versionesDe.get(fila.version_de_id) ?? new Map();
suyas.set(fila.version_nombre, fila.id);
versionesDe.set(fila.version_de_id, suyas);
}
const { rows: albumes } = await c.query('SELECT id, slug FROM album');
const idDeAlbum = new Map(albumes.map((f) => [f.slug, f.id]));
const { rows: etiquetas } = await c.query('SELECT id, nombre FROM etiqueta');
const idDeEtiqueta = new Map(etiquetas.map((f) => [f.nombre, f.id]));
/*
* El país de edición se escribe con su nombre —«España»— y la base guarda
* el código ISO. El volcado se lo saltó y los cuatro discos quedaron sin
* país; la ficha de edición lo enseña.
*/
const { rows: paises } = await c.query('SELECT codigo, nombre FROM pais');
const codigoDePais = new Map(paises.map((f) => [f.nombre, f.codigo]));
/**
* La clave de un medio a partir de lo que dice el archivo.
*
* En el frontmatter el audio es una ruta relativa dentro del almacén
* privado; en `medio` la clave lleva delante el almacén, porque el mismo
* nombre existe en dos —`singles/dorina.mp3` está en audio y en muestras— y
* sin el prefijo chocaban.
*/
const enAlmacen = (almacen, ruta) => {
if (!ruta) return null;
// Las portadas se escriben como ruta pública —`/images/…`— y el audio
// como ruta dentro del almacén. La clave lleva el almacén delante, una
// sola vez.
const limpia = ruta.replace(/^\//, '').replace(new RegExp(`^${almacen}/`), '');
return `${almacen}/${limpia}`;
};
/* --- Canciones ---------------------------------------------------------- */
for (const { slug, datos } of leerCarpeta('canciones')) {
const cancionId = idDeCancion.get(slug);
if (!cancionId) {
avisos.push(`la canción «${slug}» no está en la base`);
continue;
}
const audio = idDeMedio.get(enAlmacen('audio', datos.audio));
const muestra = idDeMedio.get(enAlmacen('muestras', datos.audio));
const portada = idDeMedio.get(enAlmacen('images', datos.portada));
if (datos.audio && !audio) avisos.push(`${slug}: no encuentro el audio ${datos.audio}`);
if (datos.portada && !portada) avisos.push(`${slug}: no encuentro la portada ${datos.portada}`);
await c.query(
`UPDATE cancion SET audio_id = COALESCE($2, audio_id),
muestra_id = COALESCE($3, muestra_id), portada_id = COALESCE($4, portada_id)
WHERE id = $1`,
[cancionId, audio ?? null, muestra ?? null, portada ?? null]
);
if (audio) contado.audio++;
if (muestra) contado.muestra++;
if (portada) contado.portadaCancion++;
/* Las versiones: su audio, su nota y su nombre. */
for (const version of datos.versiones ?? []) {
const versionId = versionesDe.get(cancionId)?.get(version.nombre);
if (!versionId) {
avisos.push(`${slug}: no encuentro la versión «${version.nombre}»`);
continue;
}
const audioVersion = idDeMedio.get(enAlmacen('audio', version.audio));
const muestraVersion = idDeMedio.get(enAlmacen('muestras', version.audio));
await c.query(
`UPDATE cancion SET audio_id = COALESCE($2, audio_id),
muestra_id = COALESCE($3, muestra_id), version_nota = $4 WHERE id = $1`,
[versionId, audioVersion ?? null, muestraVersion ?? null, version.nota ?? null]
);
if (audioVersion) contado.audio++;
if (muestraVersion) contado.muestra++;
if (version.nota) contado.nota++;
}
/* El orden de las etiquetas, que es el del archivo. */
for (const [i, nombre] of (datos.etiquetas ?? []).entries()) {
const etiquetaId = idDeEtiqueta.get(nombre);
if (!etiquetaId) {
avisos.push(`${slug}: no conozco la etiqueta «${nombre}»`);
continue;
}
await c.query(
'UPDATE cancion_etiqueta SET orden = $3 WHERE cancion_id = $1 AND etiqueta_id = $2',
[cancionId, etiquetaId, i]
);
contado.orden++;
}
}
/* --- Álbumes ------------------------------------------------------------ */
for (const { slug, datos } of leerCarpeta('albumes')) {
const albumId = idDeAlbum.get(slug);
if (!albumId) {
avisos.push(`el álbum «${slug}» no está en la base`);
continue;
}
const portada = idDeMedio.get(enAlmacen('images', datos.portada));
if (datos.portada && !portada) {
avisos.push(`${slug}: no encuentro la portada ${datos.portada}`);
}
const nombrePais = datos.edicion?.pais ?? datos.pais;
const codigoPais = nombrePais ? codigoDePais.get(nombrePais) : null;
if (nombrePais && !codigoPais) avisos.push(`${slug}: no conozco el país «${nombrePais}»`);
await c.query(
`UPDATE album SET portada_id = COALESCE($2, portada_id),
pais_edicion_codigo = COALESCE($3, pais_edicion_codigo) WHERE id = $1`,
[albumId, portada ?? null, codigoPais ?? null]
);
if (portada) contado.portadaAlbum++;
if (codigoPais) contado.paisAlbum++;
}
await c.query('COMMIT');
} catch (fallo) {
await c.query('ROLLBACK').catch(() => {});
console.error('Falló, no se ha escrito nada:');
console.error(' ' + fallo.message);
await c.end();
process.exit(1);
}
await c.end();
for (const [que, n] of Object.entries(contado)) console.log(` ${que.padEnd(16)} ${n}`);
for (const aviso of avisos) console.log(` aviso: ${aviso}`);
console.log('');
console.log('Completado. Comprueba con: npm run db:comparar');

Powered by TurnKey Linux.