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.
148 lines
4.9 KiB
148 lines
4.9 KiB
/**
|
|
* Carga los ejemplos de versos y los enlaces de las obras de referencia.
|
|
*
|
|
* npm run db:versos
|
|
*
|
|
* Los versos son originales, escritos para enseñar, y entran marcados como tal
|
|
* —`fuente = 'ejemplo'`, `origen = 'importado'`, sin revisar—. Ninguno es cita
|
|
* de obra ajena: para eso está `obra_referencia.cita`, y la tabla lo obliga
|
|
* (un verso con `fuente = 'cita'` sin `obra_id` no entra).
|
|
*
|
|
* ## La comprobación de la escansión
|
|
*
|
|
* Antes de escribir nada se comprueba que cada verso cuadre con la ley del
|
|
* acento final. La comprobación vive en `escansion.mjs`, para poder probarla
|
|
* sin una base delante; si algo no cuadra, esto aborta en vez de avisar: un
|
|
* ejemplo mal escandido enseña a contar mal.
|
|
*
|
|
* ## Los enlaces
|
|
*
|
|
* Son búsquedas en Spotify y en YouTube, no enlaces a una grabación concreta.
|
|
* Es deliberado: una URL de canción hay que verificarla una a una, y una
|
|
* inventada manda a un 404 con pinta de dato bueno. La búsqueda lleva a la obra
|
|
* siempre, y cuando lleguen los enlaces exactos —en el JSON de la ficha, como
|
|
* se decidió— sustituyen a estos con un `ON CONFLICT DO UPDATE`.
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import pg from 'pg';
|
|
import { revisarSemilla } from './escansion.mjs';
|
|
|
|
const SEMILLA = JSON.parse(fs.readFileSync('scripts/db/semilla-versos.json', 'utf8'));
|
|
const MODELO = 'claude-opus-5';
|
|
|
|
/* --- La escansión, antes de tocar la base -------------------------------- */
|
|
|
|
const problemas = revisarSemilla(SEMILLA);
|
|
|
|
if (problemas.length > 0) {
|
|
console.error('La escansión no cuadra. No se ha tocado la base:');
|
|
for (const p of problemas) console.error(` ${p}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
/* --- A la base ------------------------------------------------------------ */
|
|
|
|
const c = new pg.Client({ connectionString: process.env.DATABASE_URL, query_timeout: 60_000 });
|
|
await c.connect();
|
|
await c.query('BEGIN');
|
|
|
|
const contado = { verso: 0, linea: 0, enlace: 0 };
|
|
const avisos = [];
|
|
|
|
try {
|
|
const { rows: generos } = await c.query('SELECT id, slug FROM genero');
|
|
const idGenero = new Map(generos.map((g) => [g.slug, g.id]));
|
|
|
|
for (const [slug, ejemplos] of Object.entries(SEMILLA.generos)) {
|
|
const generoId = idGenero.get(slug);
|
|
if (!generoId) {
|
|
avisos.push(`no conozco el género «${slug}»`);
|
|
continue;
|
|
}
|
|
|
|
// Se rehacen enteros: un ejemplo es su texto y su escansión juntos, y
|
|
// actualizar por partes dejaría versos de una versión con la cuenta de
|
|
// otra. Las líneas caen solas, por la clave foránea en cascada.
|
|
await c.query('DELETE FROM verso WHERE genero_id = $1 AND fuente = $2', [generoId, 'ejemplo']);
|
|
|
|
for (const [i, ejemplo] of ejemplos.entries()) {
|
|
const { rows } = await c.query(
|
|
`INSERT INTO verso (id, genero_id, titulo, ilustra, fuente, analisis, medida, rima,
|
|
orden, estado, origen, modelo_origen, importado_en)
|
|
VALUES ($1,$2,$3,$4,'ejemplo',$5,$6,$7,$8,'publicado','importado',$9,now())
|
|
RETURNING id`,
|
|
[
|
|
crypto.randomUUID(),
|
|
generoId,
|
|
ejemplo.titulo,
|
|
ejemplo.ilustra,
|
|
ejemplo.analisis ?? null,
|
|
ejemplo.medida ?? null,
|
|
ejemplo.rima ?? null,
|
|
i,
|
|
MODELO
|
|
]
|
|
);
|
|
contado.verso++;
|
|
|
|
for (const [j, linea] of ejemplo.lineas.entries()) {
|
|
await c.query(
|
|
`INSERT INTO verso_linea (verso_id, numero, texto, silabas, tonicas, medida, rima)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
|
[
|
|
rows[0].id,
|
|
j + 1,
|
|
linea.texto,
|
|
linea.silabas,
|
|
linea.tonicas,
|
|
linea.medida ?? null,
|
|
linea.rima ?? null
|
|
]
|
|
);
|
|
contado.linea++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --- Dónde escucharlas ------------------------------------------------- */
|
|
|
|
const { rows: obras } = await c.query(
|
|
`SELECT o.id, o.titulo, o.ano,
|
|
(SELECT p.nombre FROM obra_referencia_persona op
|
|
JOIN persona p ON p.id = op.persona_id
|
|
WHERE op.obra_id = o.id ORDER BY op.orden LIMIT 1) AS autor
|
|
FROM obra_referencia o`
|
|
);
|
|
|
|
for (const obra of obras) {
|
|
const busqueda = [obra.titulo, obra.autor].filter(Boolean).join(' ');
|
|
const enlaces = {
|
|
spotify: `https://open.spotify.com/search/${encodeURIComponent(busqueda)}`,
|
|
youtube: `https://www.youtube.com/results?search_query=${encodeURIComponent(busqueda)}`
|
|
};
|
|
|
|
for (const [plataforma, url] of Object.entries(enlaces)) {
|
|
await c.query(
|
|
`INSERT INTO obra_referencia_enlace (obra_id, plataforma, url) VALUES ($1,$2,$3)
|
|
ON CONFLICT (obra_id, plataforma) DO UPDATE SET url = EXCLUDED.url`,
|
|
[obra.id, plataforma, url]
|
|
);
|
|
contado.enlace++;
|
|
}
|
|
}
|
|
|
|
await c.query('COMMIT');
|
|
} catch (fallo) {
|
|
await c.query('ROLLBACK').catch(() => {});
|
|
console.error('\nFalló, no se ha escrito nada:\n ' + fallo.message);
|
|
await c.end();
|
|
process.exit(1);
|
|
}
|
|
|
|
await c.end();
|
|
|
|
for (const [que, n] of Object.entries(contado)) console.log(` ${que.padEnd(10)} ${n}`);
|
|
for (const aviso of avisos) console.log(` aviso: ${aviso}`);
|
|
console.log('\nSembrado. Los versos son ejemplos originales, sin revisar.');
|