diff --git a/.vscode/launch.json b/.vscode/launch.json index 69228d9..5eb672a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,4 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { @@ -19,7 +16,14 @@ "id": "command", "type": "pickString", "description": "Which command do you want to run?", - "options": ["smart", "relocate", "import-ext", "help"], + "options": [ + // + "import-ext", + "relocate", + "smart", + "strip-accents", + "help" + ], "default": "smart" } ] diff --git a/img/enjinn-square.png b/img/enjinn-square.png new file mode 100644 index 0000000..78f28a4 Binary files /dev/null and b/img/enjinn-square.png differ diff --git a/package-lock.json b/package-lock.json index 8ca8446..f79f150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "node-fetch": "2.6.1", "node-rsa": "1.1.1", "ora": "5.4.0", + "pluralize": "8.0.0", "prompts": "2.4.1", "sqlite3": "5.0.2", "terminal-link": "2.1.1", @@ -46,6 +47,7 @@ "@types/node": "14.14.41", "@types/node-fetch": "2.5.10", "@types/node-rsa": "1.1.0", + "@types/pluralize": "0.0.29", "@types/prompts": "2.0.11", "aws-sdk": "2.903.0", "execa": "0.10.0", @@ -678,6 +680,12 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@types/pluralize": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz", + "integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==", + "dev": true + }, "node_modules/@types/prompts": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.11.tgz", @@ -7242,6 +7250,14 @@ "node": ">= 10.0.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, "node_modules/prebuild-install": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.0.1.tgz", @@ -10009,6 +10025,12 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/pluralize": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz", + "integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==", + "dev": true + }, "@types/prompts": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.11.tgz", @@ -14999,6 +15021,11 @@ "find-up": "^3.0.0" } }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==" + }, "prebuild-install": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.0.1.tgz", diff --git a/package.json b/package.json index 094cbd6..3f0ed9c 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "node-fetch": "2.6.1", "node-rsa": "1.1.1", "ora": "5.4.0", + "pluralize": "8.0.0", "prompts": "2.4.1", "sqlite3": "5.0.2", "terminal-link": "2.1.1", @@ -113,6 +114,7 @@ "@types/node": "14.14.41", "@types/node-fetch": "2.5.10", "@types/node-rsa": "1.1.0", + "@types/pluralize": "0.0.29", "@types/prompts": "2.0.11", "aws-sdk": "2.903.0", "execa": "0.10.0", diff --git a/src/base-commands/base-engine-command.ts b/src/base-commands/base-engine-command.ts index a314b53..ef0cc2e 100644 --- a/src/base-commands/base-engine-command.ts +++ b/src/base-commands/base-engine-command.ts @@ -9,12 +9,40 @@ export abstract class BaseEngineCommand extends Command { protected libraryFolder!: string; protected engineDb!: engine.EngineDB; + log(message = '', { indent = 0 }: { indent?: number } = {}) { + const indentStr = ' '.repeat(indent); + super.log(indentStr + message); + } + + logBlock(message: string, opts?: { indent?: number }) { + super.log(); + this.log(message, opts); + super.log(); + } + + warn(message: string, opts?: { indent?: number }) { + this.log(chalk`{yellow Warning} ${message}`, opts); + } + + warnBlock(message: string, { indent = 0 }: { indent?: number } = {}) { + super.log(); + this.warn('', { indent }); + this.log(message, { indent: indent + 2 }); + super.log(); + } + protected async init() { this.libraryFolder = appConf().get(AppConfKey.EngineLibraryFolder); } protected async finally() { - await this.engineDb?.disconnect(); + if (this.engineDb) { + await spinner({ + text: 'Disconnect from Engine DB', + successMessage: 'Disconnected from Engine DB', + run: async () => this.engineDb.disconnect(), + }); + } } protected async connectToEngine() { @@ -35,25 +63,24 @@ export abstract class BaseEngineCommand extends Command { color?: string; } = {}, ) { - const indentStr = ' '.repeat(indent); - tracks.forEach(t => - this.log(`${indentStr}${(chalk as any)[color](t.path)}`), - ); + tracks.forEach(t => { + this.log((chalk as any)[color](t.path), { indent }); + }); } protected logPlaylistsWithTrackCount( playlists: engine.PlaylistInput[], { indent = 4 }: { indent?: number } = {}, ) { - const indentStr = ' '.repeat(indent); playlists.forEach(({ title, tracks }) => { const numTracks = tracks.length; if (numTracks) { this.log( - chalk`${indentStr}{blue ${title}} [{green ${numTracks.toString()}} tracks]`, + chalk`{blue ${title}} [{green ${numTracks.toString()}} tracks]`, + { indent }, ); } else { - this.log(chalk`${indentStr}{blue ${title}} [{yellow 0} tracks]`); + this.log(chalk`{blue ${title}} [{yellow 0} tracks]`, { indent }); } }); } diff --git a/src/commands/import-ext.ts b/src/commands/import-ext.ts index 68f35d7..b5a57ba 100644 --- a/src/commands/import-ext.ts +++ b/src/commands/import-ext.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import * as path from 'path'; +import pluralize from 'pluralize'; import prompts from 'prompts'; import { BaseEngineCommand } from '../base-commands'; @@ -22,7 +23,7 @@ export default class ImportExt extends BaseEngineCommand { await this.connectToEngine(); if (this.engineDb.version === engine.Version.V2_0) { - this.error(`import-ext doesn't support Engine 2.0`); + this.error(`${ImportExt.name} doesn't support Engine 2.0`); } const extLibraries = await spinner({ @@ -32,9 +33,10 @@ export default class ImportExt extends BaseEngineCommand { const length = libraries.length; if (length) { ctx.succeed( - chalk`Found {blue ${length.toString()}} external ${ - length > 1 ? 'libraries' : 'library' - }`, + chalk`Found {blue ${length.toString()}} external ${pluralize( + 'library', + length, + )}`, ); } else { ctx.warn(`Didn't find any external Engine libraries`); @@ -67,7 +69,10 @@ export default class ImportExt extends BaseEngineCommand { const length = extPlaylists.length; if (length) { ctx.succeed( - chalk`Found {blue ${length.toString()}} external playlists`, + chalk`Found {blue ${length.toString()}} external ${pluralize( + 'playlist', + length, + )}`, ); } else { ctx.warn(`Didn't find any external playlists`); @@ -86,7 +91,10 @@ export default class ImportExt extends BaseEngineCommand { const imported = await this.importPlaylists(selectedPlaylists); ctx.succeed( - chalk`Imported {blue ${imported.length.toString()}} external playlists`, + chalk`Imported {blue ${imported.length.toString()}} external ${pluralize( + 'playlist', + imported.length, + )}`, ); this.logPlaylistsWithTrackCount(imported); @@ -116,8 +124,8 @@ export default class ImportExt extends BaseEngineCommand { private checkLicense(): boolean { if (!isLicensed()) { - this.log( - chalk`{yellow Warning} The import-ext command isn't included in the free version.`, + this.warnBlock( + chalk`The {cyan ${ImportExt.name}} command isn't included in the free version.`, ); return false; } diff --git a/src/commands/relocate.ts b/src/commands/relocate.ts index a3ebf00..f07bf55 100644 --- a/src/commands/relocate.ts +++ b/src/commands/relocate.ts @@ -1,6 +1,7 @@ import chalk from 'chalk'; import { keyBy, partition } from 'lodash'; import path from 'path'; +import pluralize from 'pluralize'; import prompts from 'prompts'; import { trueCasePath } from 'true-case-path'; @@ -25,9 +26,13 @@ export default class Relocate extends BaseEngineCommand { text: 'Find missing tracks', run: async ctx => { const missing = await this.findMissingTracks(); - if (missing.length) { + const length = missing.length; + if (length) { ctx.succeed( - chalk`Found {red ${missing.length.toString()}} missing tracks`, + chalk`Found {red ${length.toString()}} missing ${pluralize( + 'track', + length, + )}`, ); this.logTracks(missing); } else { @@ -36,12 +41,12 @@ export default class Relocate extends BaseEngineCommand { return missing; }, }); + this.log(); if (!missingTracks.length) { return; } - this.log(); const searchFolder = await this.promptForSearchFolder(); this.log(); @@ -53,17 +58,23 @@ export default class Relocate extends BaseEngineCommand { searchFolder, }); - const numRelocated = relocated.length.toString(); - const numMissing = stillMissing.length.toString(); + const relocatedLength = relocated.length; + const missingLength = stillMissing.length; + const relocatedTrackWord = pluralize('track', relocatedLength); + const missingTrackWord = pluralize('track', missingLength); if (!relocated.length) { - ctx.fail(chalk`Couldn't find {red ${numMissing}} tracks`); - } else if (stillMissing.length) { + ctx.fail( + chalk`Couldn't find {red ${missingLength.toString()}} ${missingTrackWord}`, + ); + } else if (missingLength) { ctx.warn( - chalk`Relocated {green ${numRelocated}} tracks, couldn't find {red ${numMissing}} tracks`, + chalk`Relocated {green ${relocatedLength.toString()}} ${relocatedTrackWord}, couldn't find {red ${missingLength.toString()}} ${missingTrackWord}`, ); } else { - ctx.succeed(chalk`Relocated {green ${numRelocated}} tracks`); + ctx.succeed( + chalk`Relocated {green ${relocatedLength.toString()}} ${relocatedTrackWord}`, + ); } this.logTracks(relocated); if (stillMissing.length) { @@ -85,7 +96,13 @@ export default class Relocate extends BaseEngineCommand { await spinner({ text: 'Save relocated tracks to Engine', successMessage: 'Saved relocated tracks to Engine', - run: async () => this.engineDb.updateTrackPaths(relocated), + run: async () => + this.engineDb.updateTracks( + relocated.map(track => ({ + id: track.id, + path: track.path, + })), + ), }); } diff --git a/src/commands/smart.ts b/src/commands/smart.ts index 2223398..d484b5e 100644 --- a/src/commands/smart.ts +++ b/src/commands/smart.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import { every, some } from 'lodash'; +import pluralize from 'pluralize'; import { BaseEngineCommand } from '../base-commands'; import * as engine from '../engine'; @@ -41,15 +42,17 @@ export default class Smart extends BaseEngineCommand { tracks: filterTracks({ tracks, playlistConfig }), }), ); + const length = inputs.length; const numEmpty = inputs.filter(x => !x.tracks.length).length; + const playlistWord = pluralize('playlist', length); if (numEmpty) { ctx.warn( - chalk`Built {blue ${inputs.length.toString()}} smart playlists, but {yellow ${numEmpty.toString()}} didn't match any tracks`, + chalk`Built {blue ${length.toString()}} smart ${playlistWord}, but {yellow ${numEmpty.toString()}} didn't match any tracks`, ); } else { ctx.succeed( - chalk`Built {blue ${inputs.length.toString()}} smart playlists`, + chalk`Built {blue ${length.toString()}} smart ${playlistWord}`, ); } this.logPlaylistsWithTrackCount(inputs); @@ -81,8 +84,8 @@ export default class Smart extends BaseEngineCommand { const numPlaylists = this.smartPlaylists.length; if (numPlaylists > MAX_FREE_PLAYLISTS) { - this.log( - chalk`{yellow Warning} The free version only supports up to {green ${MAX_FREE_PLAYLISTS.toString()}}, but you have {yellow ${numPlaylists.toString()}}.`, + this.warnBlock( + chalk`The free version only supports up to {green ${MAX_FREE_PLAYLISTS.toString()}} smart playlists, but you have {yellow ${numPlaylists.toString()}}.`, ); return false; } @@ -90,10 +93,10 @@ export default class Smart extends BaseEngineCommand { normalizeRuleGroup(p.rules).nodes.some(isNodeGroup), ); if (withNestedRules.length) { - this.log( - chalk`{yellow Warning} The free version doesn't support nested rules. The following playlists contain nested rules:`, + this.warnBlock( + `The free version doesn't support nested rules. The following playlists contain nested rules:`, ); - withNestedRules.forEach(p => this.log(` ${p.name}`)); + withNestedRules.forEach(p => this.log(p.name, { indent: 4 })); return false; } diff --git a/src/commands/strip-accents.ts b/src/commands/strip-accents.ts new file mode 100644 index 0000000..b88dba7 --- /dev/null +++ b/src/commands/strip-accents.ts @@ -0,0 +1,159 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import { compact, pick } from 'lodash'; +import path from 'path'; +import pluralize from 'pluralize'; + +import { BaseEngineCommand } from '../base-commands'; +import * as engine from '../engine'; +import { isLicensed } from '../licensing'; +import { asyncSeries, spinner } from '../utils'; + +export default class StripAccents extends BaseEngineCommand { + static readonly description = 'strip accents from tags and filenames'; + + async run() { + if (!this.checkLicense()) { + return; + } + + this.warnBlock( + `This will NOT update tags in your files. It will only update tags in the Engine DB.`, + ); + + await this.connectToEngine(); + + this.log(); + const accented = await spinner({ + text: 'Find accented tracks', + run: async ctx => { + const accented = await this.findTracksWithAccents(); + const length = accented.length; + + const msgSuffix = `${pluralize( + 'track', + length, + )} with accents in their filename or tags`; + + if (length) { + ctx.succeed(chalk`Found {yellow ${length.toString()}} ${msgSuffix}`); + this.logTracks(accented); + } else { + ctx.warn(`Didn't find any ${msgSuffix}`); + } + + return accented; + }, + }); + this.log(); + + if (!accented.length) { + return; + } + + await spinner({ + text: 'Strip accents from tracks and save to Engine DB', + successMessage: 'Stripped accents from tracks and saved to Engine DB', + run: async () => + asyncSeries( + accented.map( + track => async () => this.stripAccentsAndSaveTrack(track), + ), + ), + }); + } + + private checkLicense(): boolean { + if (!isLicensed()) { + this.warnBlock( + chalk`The {cyan ${StripAccents.id}} command isn't included in the free version.`, + ); + return false; + } + + return true; + } + + private async findTracksWithAccents(): Promise { + const tracks = await this.engineDb.getTracks(); + + return tracks.filter(track => + ACCENTS_REGEX.test( + compact(Object.values(pick(track, TRACK_ACCENT_FIELDS))).join(''), + ), + ); + } + + private async stripAccentsAndSaveTrack(track: engine.Track) { + const origFilename = track.filename; + + TRACK_ACCENT_FIELDS.forEach(field => { + const value = track[field]; + + if (value) { + track[field] = ACCENT_REPLACE_FUNCS.reduce((v, func) => func(v), value); + } + }); + + if (track.filename !== origFilename) { + const oldPath = track.path; + const parsedPath = path.parse(oldPath); + track.path = path.join(parsedPath.dir, track.filename); + + const oldFile = path.resolve(this.libraryFolder, oldPath); + const newFile = path.resolve(this.libraryFolder, track.path); + + await fs.promises.rename(oldFile, newFile); + } + + await this.engineDb.updateTracks([track]); + } +} + +const TRACK_ACCENT_FIELDS = [ + 'album', + 'artist', + 'comment', + 'composer', + 'filename', + 'genre', + 'label', + 'remixer', + 'title', +] as const; + +// spell-checker: disable +const ACCENT_MAP = { + ÀÁÂÃÄÅ: 'A', + àáâãäå: 'a', + ÈÉÊË: 'E', + èéêë: 'e', + ÌÍÎÏ: 'I', + ìíîï: 'i', + ÒÓÔÕÖØ: 'O', + òóôõöø: 'o', + ÙÚÛÜ: 'U', + ùúûüµ: 'u', + ß: 'B', + Ç: 'C', + ç: 'c', + Ææ: 'AE', + Œœ: 'OE', + Ðð: 'DH', + Þþ: 'TH', + '¢£€¤¥': '$', + '©': 'c', + '¡': '!', + '×': 'x', + '·': '.', + '¿÷«»': '_', +} as const; +// spell-checker: enable + +const ALL_ACCENTS = Object.keys(ACCENT_MAP).join(''); +const ACCENTS_REGEX = new RegExp(`[${ALL_ACCENTS}]`, 'g'); +const ACCENT_REPLACE_FUNCS = Object.entries(ACCENT_MAP).map(([key, value]) => { + const regex = new RegExp(`[${key}]`, 'g'); + + return (v: string) => v.replace(regex, value); +}); diff --git a/src/engine/1.6/engine-db-1_6.ts b/src/engine/1.6/engine-db-1_6.ts index 089491f..3982efa 100644 --- a/src/engine/1.6/engine-db-1_6.ts +++ b/src/engine/1.6/engine-db-1_6.ts @@ -1,12 +1,13 @@ import { Knex } from 'knex'; import { - camelCase, Dictionary, fromPairs, groupBy, + pick, pullAt, transform, } from 'lodash'; +import { ConditionalKeys } from 'type-fest'; import { asyncSeries } from '../../utils'; import { EngineDB } from '../engine-db'; @@ -289,7 +290,7 @@ export class EngineDB_1_6 extends EngineDB { ...transform( textMetaMap[track.id] ?? [], (result, meta) => { - const key = camelCase(schema.MetaDataType[meta.type]); + const key = schema.MetaDataType[meta.type]; result[key] = meta.text; }, {} as any, @@ -297,7 +298,7 @@ export class EngineDB_1_6 extends EngineDB { ...transform( intMetaMap[track.id] ?? [], (result, meta) => { - const key = camelCase(schema.MetaDataIntegerType[meta.type]); + const key = schema.MetaDataIntegerType[meta.type]; result[key] = meta.value; }, {} as any, @@ -316,16 +317,47 @@ export class EngineDB_1_6 extends EngineDB { }); } - async updateTrackPaths(tracks: publicSchema.Track[]) { - await this.knex.transaction(async trx => { - await asyncSeries( - tracks.map(track => async () => { + async updateTracks(updates: publicSchema.UpdateTrackInput[]) { + await this.knex.transaction(async trx => + asyncSeries( + updates.map(trackUpdates => async () => { + const updateKeys: (keyof publicSchema.UpdateTrackInput)[] = [ + 'filename', + 'path', + 'year', + ]; await this.table('Track', trx) - .where('id', track.id) - .update({ path: track.path }); + .where('id', trackUpdates.id) + .update(pick(trackUpdates, updateKeys)); + + const stringMetaUpdateKeys: ConditionalKeys< + publicSchema.UpdateTrackInput, + string | undefined + >[] = [ + 'album', + 'artist', + 'comment', + 'composer', + 'genre', + 'label', + 'title', + ]; + + await asyncSeries( + stringMetaUpdateKeys + .filter(key => trackUpdates[key]) + .map(key => async () => { + const metaType = schema.MetaDataType[key as any]; + + await this.table('MetaData', trx) + .where('id', trackUpdates.id) + .andWhere('type', metaType) + .update({ text: trackUpdates[key] }); + }), + ); }), - ); - }); + ), + ); } async getExtTrackMapping( diff --git a/src/engine/1.6/schema-1_6.ts b/src/engine/1.6/schema-1_6.ts index 2cd12ee..0f15f83 100644 --- a/src/engine/1.6/schema-1_6.ts +++ b/src/engine/1.6/schema-1_6.ts @@ -111,14 +111,14 @@ export interface TrackWithMeta extends Track { } export enum MetaDataType { - Album = 3, - Artist = 2, - Comment = 5, - Composer = 7, - FileType = 13, - Genre = 4, - Label = 6, - Title = 1, + album = 3, + artist = 2, + comment = 5, + composer = 7, + fileType = 13, + genre = 4, + label = 6, + title = 1, } export interface MetaData { @@ -128,9 +128,10 @@ export interface MetaData { } export enum MetaDataIntegerType { - DateAdded = 2, - DateCreated = 3, - Key = 4, + dateLastPlayed = 1, + dateAdded = 2, + dateCreated = 3, + key = 4, } export interface MetaDataInteger { diff --git a/src/engine/2.0/engine-db-2_0.ts b/src/engine/2.0/engine-db-2_0.ts index 3e371ed..1c595dc 100644 --- a/src/engine/2.0/engine-db-2_0.ts +++ b/src/engine/2.0/engine-db-2_0.ts @@ -1,4 +1,5 @@ import { Knex } from 'knex'; +import { pick } from 'lodash'; import { asyncSeries } from '../../utils'; import { EngineDB } from '../engine-db'; @@ -161,15 +162,29 @@ export class EngineDB_2_0 extends EngineDB { throw new Error('Not implemented'); } - async updateTrackPaths(tracks: publicSchema.Track[]) { - await this.knex.transaction(async trx => { - await asyncSeries( + async updateTracks(tracks: publicSchema.UpdateTrackInput[]) { + await this.knex.transaction(async trx => + asyncSeries( tracks.map(track => async () => { + const updateKeys: (keyof publicSchema.UpdateTrackInput)[] = [ + 'album', + 'artist', + 'comment', + 'composer', + 'filename', + 'genre', + 'label', + 'path', + 'rating', + 'remixer', + 'title', + 'year', + ]; await this.table('Track', trx) .where('id', track.id) - .update({ path: track.path }); + .update(pick(track, updateKeys)); }), - ); - }); + ), + ); } } diff --git a/src/engine/engine-db.ts b/src/engine/engine-db.ts index 8a5716d..7a064b7 100644 --- a/src/engine/engine-db.ts +++ b/src/engine/engine-db.ts @@ -71,7 +71,7 @@ export abstract class EngineDB { abstract getPlaylistTracks(playlistId: number): Promise; - abstract updateTrackPaths(tracks: schema.Track[]): Promise; + abstract updateTracks(tracks: schema.UpdateTrackInput[]): Promise; protected async getSchemaInfo(): Promise { const results = await this.knex('Information') // diff --git a/src/engine/public-schema.ts b/src/engine/public-schema.ts index 6fc1388..9e9284a 100644 --- a/src/engine/public-schema.ts +++ b/src/engine/public-schema.ts @@ -1,4 +1,4 @@ -import { Opaque } from 'type-fest'; +import { Except, Opaque, SetRequired } from 'type-fest'; export interface Information { id: number; @@ -45,4 +45,17 @@ export interface Track { year?: number; } +export type UpdateTrackInput = Except< + SetRequired, 'id'>, + | 'bitrate' + | 'bpmAnalyzed' + | 'dateAdded' + | 'dateCreated' + | 'fileType' + | 'isBeatGridLocked' + | 'key' + | 'length' + | 'timeLastPlayed' +>; + export type CamelotKeyId = Opaque<'CamelotKeyId', number>; diff --git a/webpack.config.js b/webpack.config.js index b8f9356..e325bce 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -35,6 +35,7 @@ module.exports = { ...commandEntry('import-ext'), ...commandEntry('relocate'), ...commandEntry('smart'), + ...commandEntry('strip-accents'), ...hookEntry('init', 'conf'), ...hookEntry('prerun', 'engine-library'), },