Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
5.1 kB · 146 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147import { loggerNoop } from "../MaybeLogger.ts";import * as path from 'path';import { childLogger, type Logger } from "@foxxmd/logging";import { Migrator } from 'sqlite-up';import { projectRootDir, type MigrationStatus } from "../infrastructure/Atomic.ts";import * as fs from "fs/promises";import { backupDb, getDbBackupPath, MEMORY_DB_NAME } from "./Database.ts";import { fileExists, fileOrDirectoryIsWriteable } from "../../utils/FSUtils.ts";import { getDbMigrationStatus, getDb, type DbConcrete, migrateDb } from "./drizzle/drizzleUtils.ts";
export interface MigrateBaseContext { db: DbConcrete logger: Logger}
const migrationsTable = '__app_migrations';
export const getAppMigrationStatus = async (db: DbConcrete, opts: {logger?: Logger, migrationsAppFolder?: string} = {}): Promise<MigrationStatus> => { const { logger: parentLogger = loggerNoop, migrationsAppFolder = path.resolve(projectRootDir, 'src/backend/common/database/appMigrations') } = opts; const logger = childLogger(parentLogger, 'App Migrations');
const migrator = new Migrator({ db: db.$client, migrationsDir: migrationsAppFolder, migrationsTable: migrationsTable });
const status = await migrator.status();
if(status.pending === 0) { return {backupRequired: false, pending: [], log: 'No pending app migrations.'}; }
const plan = await migrator.plan(); return {backupRequired: true, pending: plan.pendingMigrations, log: `${plan.pendingMigrations.length} pending app migrations:\n${plan.pendingMigrations.join('\n')}`};}
export const migrateApp = async (db: DbConcrete, opts: {logger?: Logger, migrationsAppFolder?: string} = {}): Promise<string[]> => { const { logger: parentLogger = loggerNoop, migrationsAppFolder = path.resolve(projectRootDir, 'src/backend/common/database/appMigrations') } = opts; const logger = childLogger(parentLogger, ['App', 'Migrations']);
const migrator = new Migrator<MigrateBaseContext>({ db: db.$client, migrationsDir: migrationsAppFolder, migrationsTable: migrationsTable });
migrator.on('migration:applied', function (name: string, batch: number): void { logger.verbose(`Migration Applied: "${name}" in batch ${batch}`); });
logger.info('Applying any app migrations...'); const result = await migrator.apply({db, logger});
if (!result.success) { throw new Error('App migration failed', {cause: result.error}); } else { if(result.appliedMigrations.length === 0) { logger.info('No app migrations required.'); } else { logger.info('App migrations applied!'); } }
return result.appliedMigrations;}
export const getMigratedDb = async (dbPath: string, opts: { logger?: Logger, migrationsFolder?: string, migrationsAppFolder?: string, backupPath?: string } = {}): Promise<[DbConcrete, boolean]> => { const { logger: parentLogger = loggerNoop } = opts; const logger = childLogger(parentLogger, ['Migrations']); let db: DbConcrete, isNew = false, isMemory = dbPath === MEMORY_DB_NAME, dbMigrationStatus: MigrationStatus, appMigrationStatus: MigrationStatus, backedUp = false; if (!isMemory) { try { fileOrDirectoryIsWriteable(dbPath); } catch (e) { throw new Error('Database directory is not accessible', { cause: e }); }
const backupPath = getDbBackupPath(dbPath);
if(!fileExists(dbPath) && fileExists(backupPath)) { logger.info(`Detected no database, making a copy of backup to use as new db. Backup file: ${backupPath}`); await fs.copyFile(backupPath, dbPath); } if (fileExists(dbPath)) { db = await getDb(dbPath, opts); } else { logger.info('Detected no database, creating a new one...'); db = await getDb(dbPath, opts); isNew = true; } } else { logger.info('Detected in-memory database'); db = await getDb(dbPath, opts); isNew = true; }
dbMigrationStatus = await getDbMigrationStatus(db, opts); if(dbMigrationStatus.error !== undefined) { logger.warn(dbMigrationStatus.error); } else if(!isMemory && dbMigrationStatus.log !== undefined) { logger.info({labels: 'DB'}, dbMigrationStatus.log); } if (dbMigrationStatus.backupRequired && !isNew && !isMemory) { await backupDb(db.$client, dbPath, { logger: opts.logger }); backedUp = true; }
if(backedUp) { logger.info('TIP: Migrations may take some time, depending on the size of your database'); } await migrateDb(db, opts);
appMigrationStatus = await getAppMigrationStatus(db, opts); if(!isMemory && appMigrationStatus.log !== undefined) { logger.info({labels: 'App'}, appMigrationStatus.log); } if(appMigrationStatus.pending.length > 0) { if(appMigrationStatus.backupRequired && !isNew && !isMemory && !backedUp) { logger.info(`Database not yet backed up, backing up before app migrations`); await backupDb(db.$client, dbPath, { logger: opts.logger }); } await migrateApp(db, opts); }
return [db, isNew];}