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.
227 lines
8.1 KiB
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');
|