Skip to content

Instantly share code, notes, and snippets.

@crucialfelix
Created July 23, 2018 12:00
Show Gist options
  • Save crucialfelix/4f07c35710ed8368ca8dff61b8c31fbe to your computer and use it in GitHub Desktop.
Save crucialfelix/4f07c35710ed8368ca8dff61b8c31fbe to your computer and use it in GitHub Desktop.
Scan a directory of music files (mp3 aif wav flac etc.) and collect data grouped by artist, release, tracks
import readdir from 'fs-readdir-recursive';
import { List } from 'linqts';
import _ from 'lodash';
import { IAudioMetadata, parseFile } from 'music-metadata';
import path from 'path';
type Common = IAudioMetadata["common"];
export interface Track {
path: string;
ext: string;
metadata: IAudioMetadata;
}
export type Release = Track[];
export interface ArtistReleases {
[slug: string]: Release;
}
// export type Artist = ArtistReleases[];
export interface Artists {
[slug: string]: ArtistReleases;
}
// internal type for Artists, each with one bag of all tracks
interface ArtistsTracks {
[slug: string]: Track[];
}
/**
* Scan music files, group by Artist, Album, Track
*
* Read soundfile tags ID3, FLAC etc
*/
export async function scan(source: string): Promise<Artists> {
let tracks = await scanTracks(source);
let artists = groupByArtist(tracks);
let artistsReleases: Artists = {};
for (let slug in artists) {
let artist = artists[slug];
artistsReleases[slug] = groupByAlbum(artist);
}
return artistsReleases;
}
export async function scanTracks(source: string) {
let s = path.resolve(source);
let files = readdir(s);
return Promise.all(files.filter(isMusicFile).map(metaData));
}
function isMusicFile(p: string) {
let ext = path.extname(p);
// skip these for now
let match = ext.match(/\.(mp3|aiff|aif|flac)$/);
let ok = !!match;
return ok;
}
const metaDataParseOptions = {
native: false,
skipCovers: true
};
/**
* Parse audio file for metadata (id3 tags etc)
*/
async function metaData(filename: string): Promise<Track> {
let s = path.resolve(filename);
let ext = path.extname(filename);
if (ext) {
// remove .
ext = ext.substr(1);
}
let ret = {
path: filename,
ext
};
return parseFile(s, metaDataParseOptions).then(
metadata => {
return { ...ret, metadata };
},
error => {
// cannot read a .wav
console.error(filename, error);
throw error;
}
);
}
/**
* Group tracks by normalized artist name
*
* {
* 'mr. artist': [ track1, track2]
* }
*/
export function groupByArtist(data: Track[]): ArtistsTracks {
let tracks = new List<Track>(data);
function key(common: Common): string {
return (common.artist || common.albumartist || "unknown").toLowerCase();
}
return tracks.GroupBy(track => key(track.metadata.common), track => track);
}
export function groupByAlbum(data: Track[]): ArtistReleases {
let tracks = new List<Track>(data);
function key(common: Common): string {
return (common.album || "unreleased").toLowerCase();
}
// {album-slug: track[]}
let albums = tracks.GroupBy(
track => key(track.metadata.common),
track => track
);
// sort each album by track number where available
return _.mapValues(albums, tracks =>
_.sortBy(tracks, track => _.get(track, "metadata.common.track.no", 100))
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment