Created
July 23, 2018 12:00
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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