music-metadata-search
Version:
Search in your local music library using quick filters on metadata tags
107 lines (98 loc) • 4.37 kB
JavaScript
// @ts-check
import { and, between, eq, like, or, sql } from "drizzle-orm";
import { DEFAULT_AUDIO_EXT } from "./constants.mjs";
import { db } from "./drizzle/database.mjs";
import { ScansTracks, Tracks } from "./drizzle/schema.mjs";
import { logger } from "./logger.mjs";
import { scanAudioFiles } from "./scan.mjs";
/**
* @typedef {[from: number, to: number]} Range
*
* @typedef Options
* @property {string} [album] Filter album of the track using `LIKE` operator
* @property {string} [artist] Filter artist of the track using `LIKE` operator
* @property {string} [genre] Filter genre of the track using `LIKE` operator
* @property {string} [comment] Filter comment of the track using `LIKE` operator
* @property {string} [title] Filter by title of the track to search using `LIKE` operator
* @property {number | Range} [bpm] Filter by BPM of the track using `=` operator or `BETWEEN` if a range is given
* @property {number | Range} [year] Filter by year of the track using `=` operator or `BETWEEN` if a range is given
* @property {number | Range} [bitrate] Filter by bitrate (in bits/seconds) of the track using `=` operator or `BETWEEN` if a range is given
* @property {number | Range} [sampleRate] Filter by sample rate (in Hz) of the track using `=` operator or `BETWEEN` if a range is given
* @property {number} [bpm] Filter by BPM of the track using `=` operator or `BETWEEN` if a range is given
* @property {number | Range} [duration] Filter by duration of the track (in seconds), using `=` operator or `BETWEEN` if a range is given
*
* @property {string} [query] Search the term everywhere
* @property {string} [sort] SQL ORDER BY expression
* @property {string} [where] SQL WHERE expression
*
* @property {number} [cacheScanTtl]
* @property {string[]} [ext] Extensions of Audio files to scan
* @property {number} [limit] Limit the number of tracks returned
* @property {string} [logLevel] Log level for [pino](https://www.npmjs.com/package/pino) (default to `'silent'`)
*
* @param {string} path
* @param {Options} opts
*/
export async function search(path, opts = {}) {
logger.level = opts.logLevel ?? "silent";
const scanId = await scanAudioFiles(path, {
cacheScanTtl: opts.cacheScanTtl ?? 3_600,
ext: opts.ext ?? [...DEFAULT_AUDIO_EXT],
});
/** @type {import("drizzle-orm").SQLWrapper[]} */
const wheres = [];
/**
* @param {number | Range} value
*/
const addWhereRange = (col, value) => {
if (Array.isArray(value)) {
wheres.push(between(col, value[0], value[1]));
} else {
wheres.push(eq(col, opts.year));
}
};
if (opts.artist) wheres.push(like(Tracks.artist, `%${opts.artist}%`));
if (opts.album) wheres.push(like(Tracks.album, `%${opts.album}%`));
if (opts.title) wheres.push(like(Tracks.title, `%${opts.title}%`));
if (opts.genre) wheres.push(like(Tracks.genre, `%${opts.genre}%`));
if (opts.comment) wheres.push(like(Tracks.comment, `%${opts.comment}%`));
if (opts.where) wheres.push(sql.raw(opts.where));
if (opts.year) addWhereRange(Tracks.year, opts.year);
if (opts.duration) addWhereRange(Tracks.year, opts.duration);
if (opts.bpm) addWhereRange(Tracks.year, opts.bpm);
if (opts.bitrate) addWhereRange(Tracks.year, opts.bitrate);
if (opts.sampleRate) addWhereRange(Tracks.year, opts.sampleRate);
if (opts.query) {
const cond = or(
like(Tracks.album, `%${opts.query}%`),
like(Tracks.artist, `%${opts.query}%`),
like(Tracks.title, `%${opts.query}%`),
like(Tracks.genre, `%${opts.query}%`),
);
if (cond) wheres.push(cond);
}
const query = db
.select({
path: Tracks.path,
title: Tracks.title,
genre: Tracks.genre,
artist: Tracks.artist,
album: Tracks.album,
year: Tracks.year,
bitrate: Tracks.bitrate,
sampleRate: Tracks.sampleRate,
bpm: Tracks.bpm,
duration: Tracks.duration,
musicbrainzArtistId: Tracks.musicbrainzArtistId,
comment: Tracks.comment,
})
.from(Tracks)
.innerJoin(ScansTracks, eq(ScansTracks.trackId, Tracks.id))
.where(and(eq(ScansTracks.scanId, scanId), ...wheres))
.orderBy(opts.sort ? sql.raw(opts.sort) : Tracks.id);
if (opts.limit) query.limit(Number(opts.limit));
logger.debug(
`${query.toSQL().sql} -- ${JSON.stringify(query.toSQL().params)}`,
);
return await query;
}