Initial release 0.1.0
This commit is contained in:
commit
9f97d8407a
52 files changed
+6774
No files matched your search
@@ -0,0 +1,12 @@
|
||||
import path from 'node:path';
|
||||
|
||||
function configuredPath(value, fallback) {
|
||||
return path.resolve(process.cwd(), value || fallback);
|
||||
}
|
||||
|
||||
export function getPaths() {
|
||||
return {
|
||||
database: configuredPath(process.env.SENDEWERK_DB_PATH, '.data/sendewerk.sqlite'),
|
||||
uploads: configuredPath(process.env.SENDEWERK_UPLOAD_DIR, '.data/uploads')
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getPaths } from './config.mjs';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
export function migrate(databasePath = getPaths().database) {
|
||||
mkdirSync(path.dirname(databasePath), { recursive: true });
|
||||
mkdirSync(getPaths().uploads, { recursive: true });
|
||||
|
||||
const database = new Database(databasePath);
|
||||
database.pragma('foreign_keys = ON');
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at_utc TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
const applied = new Set(
|
||||
database
|
||||
.prepare('SELECT name FROM schema_migrations')
|
||||
.all()
|
||||
.map((row) => row.name)
|
||||
);
|
||||
const migrations = readdirSync(path.join(projectRoot, 'migrations'))
|
||||
.filter((name) => /^\d+.+\.sql$/.test(name))
|
||||
.sort();
|
||||
|
||||
for (const name of migrations) {
|
||||
if (applied.has(name)) continue;
|
||||
|
||||
const sql = readFileSync(path.join(projectRoot, 'migrations', name), 'utf8');
|
||||
database.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
database.exec(sql);
|
||||
database
|
||||
.prepare('INSERT INTO schema_migrations (name, applied_at_utc) VALUES (?, ?)')
|
||||
.run(name, new Date().toISOString());
|
||||
database.exec('COMMIT');
|
||||
console.log(`Migration applied: ${name}`);
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK');
|
||||
database.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
database.close();
|
||||
console.log(`Database ready: ${databasePath}`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
migrate();
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getPaths } from './config.mjs';
|
||||
|
||||
export function seed(databasePath = getPaths().database) {
|
||||
mkdirSync(path.dirname(databasePath), { recursive: true });
|
||||
const database = new Database(databasePath);
|
||||
database.pragma('foreign_keys = ON');
|
||||
|
||||
const seedDatabase = database.transaction(() => {
|
||||
const replace = (table, columns, rows) => {
|
||||
const placeholders = columns.map(() => '?').join(', ');
|
||||
const update = columns
|
||||
.filter((column) => column !== 'id')
|
||||
.map((column) => `${column} = excluded.${column}`)
|
||||
.join(', ');
|
||||
const statement = database.prepare(
|
||||
`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})
|
||||
ON CONFLICT(id) DO UPDATE SET ${update}`
|
||||
);
|
||||
for (const row of rows) statement.run(...row);
|
||||
};
|
||||
|
||||
replace(
|
||||
'section_categories',
|
||||
['id', 'name', 'color', 'sort_order'],
|
||||
[
|
||||
['cat-jc', 'JC', '#67DDE8', 0],
|
||||
['cat-reaction', 'Reaction', '#E7B957', 1],
|
||||
['cat-game', 'Spiel', '#9B8AFB', 2]
|
||||
]
|
||||
);
|
||||
replace(
|
||||
'formats',
|
||||
['id', 'name'],
|
||||
[
|
||||
['format-find', 'Fundstück'],
|
||||
['format-talk', 'Gespräch'],
|
||||
['format-quiz', 'Quiz']
|
||||
]
|
||||
);
|
||||
replace(
|
||||
'topics',
|
||||
['id', 'name'],
|
||||
[
|
||||
['topic-gaming', 'Gaming'],
|
||||
['topic-film', 'Film & Serien'],
|
||||
['topic-internet', 'Netzkultur']
|
||||
]
|
||||
);
|
||||
|
||||
const timestamp = '2026-10-21T10:00:00.000Z';
|
||||
replace(
|
||||
'pieces',
|
||||
[
|
||||
'id',
|
||||
'title',
|
||||
'summary',
|
||||
'format_id',
|
||||
'status',
|
||||
'duration_seconds',
|
||||
'intro',
|
||||
'moderation_notes',
|
||||
'transition_text',
|
||||
'presentation_text',
|
||||
'created_at_utc',
|
||||
'updated_at_utc'
|
||||
],
|
||||
[
|
||||
[
|
||||
'piece-console',
|
||||
'Die kleine Konsole mit dem großen Dreh',
|
||||
'Ein ungewöhnliches Handheld-Konzept sorgt für Diskussionsstoff.',
|
||||
'format-find',
|
||||
'ready',
|
||||
480,
|
||||
'Eine Kurbel, ein gelber Knopf und sehr viel Charme: Was macht kleine Hardware spannend?',
|
||||
'Erst Formfaktor zeigen, dann über bewusst gesetzte Grenzen sprechen.',
|
||||
'Von der kleinen Konsole geht es zur ganz großen Leinwand.',
|
||||
'Weniger Leistung, mehr Idee.',
|
||||
timestamp,
|
||||
timestamp
|
||||
],
|
||||
[
|
||||
'piece-effects',
|
||||
'Warum praktische Effekte wieder auffallen',
|
||||
'Neue Produktionen mischen Miniaturen, Masken und digitale Nacharbeit.',
|
||||
'format-talk',
|
||||
'ready',
|
||||
720,
|
||||
'Man sieht manchmal gerade deshalb weniger Trick, weil wieder mehr vor der Kamera passiert.',
|
||||
'Beispiele vergleichen, nicht analog gegen digital ausspielen.',
|
||||
'Nach so viel Handarbeit wird es spielerisch.',
|
||||
'Wenn das Licht wirklich auf ein Modell fällt.',
|
||||
timestamp,
|
||||
timestamp
|
||||
],
|
||||
[
|
||||
'piece-quiz',
|
||||
'Drei Sounds, ein Spiel',
|
||||
'Kurzes Audioquiz mit bekannten Inventar- und Menüklängen.',
|
||||
'format-quiz',
|
||||
'preparing',
|
||||
600,
|
||||
'Augen zu: Erkennt ihr das Spiel nur an seinem Inventar?',
|
||||
'Drei Runden, Auflösung jeweils erst nach den Tipps.',
|
||||
'Damit sind die Ohren warm für unser erstes Spiel.',
|
||||
'Welches Spiel steckt hinter diesem Klang?',
|
||||
timestamp,
|
||||
timestamp
|
||||
],
|
||||
[
|
||||
'piece-pixels',
|
||||
'Pixelart, die nicht nostalgisch sein will',
|
||||
'Wie moderne Spiele Pixel als Material statt als Rückblick einsetzen.',
|
||||
'format-find',
|
||||
'ready',
|
||||
540,
|
||||
'Pixel sind längst keine technische Notlösung mehr.',
|
||||
'Großaufnahmen vorbereiten, drei Stilmittel benennen.',
|
||||
'Genug Theorie, jetzt spielen wir selbst.',
|
||||
'Pixel als bewusste Entscheidung.',
|
||||
timestamp,
|
||||
timestamp
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
database.prepare('DELETE FROM piece_topics WHERE piece_id LIKE ?').run('piece-%');
|
||||
const addTopic = database.prepare(
|
||||
'INSERT INTO piece_topics (piece_id, topic_id) VALUES (?, ?)'
|
||||
);
|
||||
for (const values of [
|
||||
['piece-console', 'topic-gaming'],
|
||||
['piece-effects', 'topic-film'],
|
||||
['piece-quiz', 'topic-gaming'],
|
||||
['piece-pixels', 'topic-gaming'],
|
||||
['piece-pixels', 'topic-internet']
|
||||
])
|
||||
addTopic.run(...values);
|
||||
|
||||
database.prepare('DELETE FROM piece_category_suggestions WHERE piece_id LIKE ?').run('piece-%');
|
||||
const addCategory = database.prepare(
|
||||
'INSERT INTO piece_category_suggestions (piece_id, category_id) VALUES (?, ?)'
|
||||
);
|
||||
for (const values of [
|
||||
['piece-console', 'cat-jc'],
|
||||
['piece-effects', 'cat-reaction'],
|
||||
['piece-quiz', 'cat-jc'],
|
||||
['piece-pixels', 'cat-reaction']
|
||||
])
|
||||
addCategory.run(...values);
|
||||
|
||||
replace(
|
||||
'piece_sources',
|
||||
['id', 'piece_id', 'label', 'url', 'timecode_seconds', 'position'],
|
||||
[
|
||||
[
|
||||
'source-console',
|
||||
'piece-console',
|
||||
'Herstellerseite',
|
||||
'https://example.com/console',
|
||||
null,
|
||||
0
|
||||
],
|
||||
[
|
||||
'source-effects',
|
||||
'piece-effects',
|
||||
'Werkstattbericht',
|
||||
'https://example.com/effects',
|
||||
95,
|
||||
0
|
||||
],
|
||||
['source-pixels', 'piece-pixels', 'Galerie', 'https://example.com/pixels', null, 0]
|
||||
]
|
||||
);
|
||||
|
||||
replace(
|
||||
'shows',
|
||||
[
|
||||
'id',
|
||||
'title',
|
||||
'starts_at_utc',
|
||||
'twitch_title',
|
||||
'announcement_text',
|
||||
'status',
|
||||
'created_at_utc',
|
||||
'updated_at_utc'
|
||||
],
|
||||
[
|
||||
[
|
||||
'show-october',
|
||||
'Herbstsendung: Kleine Welten',
|
||||
'2026-10-23T18:00:00.000Z',
|
||||
'Kleine Welten, große Ideen | heinzlive',
|
||||
'Heute schauen wir auf Hardware mit Kurbel, handgemachte Filmtricks und Pixel mit Haltung.',
|
||||
'ready',
|
||||
timestamp,
|
||||
timestamp
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
replace(
|
||||
'show_sections',
|
||||
['id', 'show_id', 'name', 'category_id', 'position', 'explicit_duration_seconds'],
|
||||
[
|
||||
['section-start', 'show-october', 'Ankommen & Warmwerden', 'cat-jc', 0, 300],
|
||||
['section-stories', 'show-october', 'Kleine Welten', 'cat-reaction', 1, null],
|
||||
['section-game', 'show-october', 'Spiel 1', 'cat-game', 2, 3600]
|
||||
]
|
||||
);
|
||||
|
||||
replace(
|
||||
'piece_usages',
|
||||
[
|
||||
'id',
|
||||
'show_section_id',
|
||||
'original_piece_id',
|
||||
'position',
|
||||
'status',
|
||||
'duration_seconds',
|
||||
'title_snapshot',
|
||||
'intro_snapshot',
|
||||
'moderation_snapshot',
|
||||
'transition_snapshot',
|
||||
'presentation_snapshot',
|
||||
'is_reserve'
|
||||
],
|
||||
[
|
||||
[
|
||||
'usage-console',
|
||||
'section-stories',
|
||||
'piece-console',
|
||||
0,
|
||||
'current',
|
||||
480,
|
||||
'Die kleine Konsole mit dem großen Dreh',
|
||||
'Eine Kurbel, ein gelber Knopf und sehr viel Charme: Was macht kleine Hardware spannend?',
|
||||
'Erst Formfaktor zeigen, dann über bewusst gesetzte Grenzen sprechen.',
|
||||
'Von der kleinen Konsole geht es zur ganz großen Leinwand.',
|
||||
'Weniger Leistung, mehr Idee.',
|
||||
0
|
||||
],
|
||||
[
|
||||
'usage-effects',
|
||||
'section-stories',
|
||||
'piece-effects',
|
||||
1,
|
||||
'open',
|
||||
720,
|
||||
'Warum praktische Effekte wieder auffallen',
|
||||
'Man sieht manchmal gerade deshalb weniger Trick, weil wieder mehr vor der Kamera passiert.',
|
||||
'Beispiele vergleichen, nicht analog gegen digital ausspielen.',
|
||||
'Nach so viel Handarbeit wird es spielerisch.',
|
||||
'Wenn das Licht wirklich auf ein Modell fällt.',
|
||||
0
|
||||
],
|
||||
[
|
||||
'usage-reserve',
|
||||
'section-stories',
|
||||
'piece-pixels',
|
||||
2,
|
||||
'open',
|
||||
540,
|
||||
'Pixelart, die nicht nostalgisch sein will',
|
||||
'Pixel sind längst keine technische Notlösung mehr.',
|
||||
'Großaufnahmen vorbereiten, drei Stilmittel benennen.',
|
||||
'Genug Theorie, jetzt spielen wir selbst.',
|
||||
'Pixel als bewusste Entscheidung.',
|
||||
1
|
||||
]
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
seedDatabase();
|
||||
database.close();
|
||||
console.log(`Example data ready: ${databasePath}`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
seed();
|
||||
}
|
||||
Reference in new issue
Block a user