diff --git a/README.md b/README.md index 813fbbc..32eb633 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,25 @@ The rusty metal heart of the Basspistol release machine. +Given a folder of tracks (supports mp3, ogg, flac, and many more), it will read the track metadata and re-organise them into a Jekyll-friendly layout. Here's the example output for a single album `foo` with two tracks `bar` and `baz`: + +``` +_albums/ + foo.md +_data/ + albums.yml +_tracks/ + foo/ + 1-bar.md + 2-baz.md +assets/ + foo/ + 1-bar.mp3 + 2-baz.mp3 +``` + +All Markdown files will encode the metadata in the [Front Matter](https://jekyllrb.com/docs/frontmatter/). + ## Installation 1. Make sure `nodejs` is installed and up-tp-date: @@ -33,3 +52,61 @@ publikator organise pathToMySongs outputPath ``` Use the `--delete` flag to start with a clean output directory. + +## Jekyll Configuration + +To take advantage of the collections, add the following to your `_config.yml`: + +``` +collections: + albums: + output: true + permalink: /albums/:name + tracks: + output: true +``` + +### Albums + +To list all albums, create a file named `albums.md` in your Jekyll root with the following contents: + +``` +--- +layout: default +--- + + +``` + +Each individual album will be available at the url `/`. To create a detail page for an album, create a new layout `_layouts/album.html`: + +``` +

{{ page.album }}

+ + +``` + +### Tracks + +Each individual track will be available at the url `//`. To create a detail page for a track, create a new layout `_layouts/track.html`: + +``` +

{{ page.common.title }}

+ +``` diff --git a/package.json b/package.json index 1ee58ca..8d787e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "publikator", - "version": "0.6.0", + "version": "0.7.0", "main": "index.js", "repository": "https://github.com/aengl/publikator.git", "author": "Lynn Smeria ", diff --git a/src/cli.js b/src/cli.js index 01039e1..2523746 100644 --- a/src/cli.js +++ b/src/cli.js @@ -41,7 +41,7 @@ program const files = scan.findFilesSync(source); const taggedFiles = await scan.readTags(files); const organisedFiles = await organise.byAlbum(target, taggedFiles); - generate.generateReleaseInfo(organisedFiles); + await generate.generateReleaseInfo(target, organisedFiles); }); debug(process.argv); diff --git a/src/generate.js b/src/generate.js index 9ed5591..57a7165 100644 --- a/src/generate.js +++ b/src/generate.js @@ -20,29 +20,71 @@ const collect = (tracks, callback) => { /** * Creates release information for a single album. */ -const getAlbumInfo = tracks => ({ +const getAlbumInfo = (root, tracks) => ({ + layout: 'album', + slug: path.basename(root), + name: tracks[0].common.album || '', artists: collect(tracks, t => t.common.artists || t.common.artist), - album: collect(tracks, t => t.common.album), bitrate: collect(tracks, t => t.format.bitrate), trackCount: tracks.length, + cover: tracks[0].coverUrl || null, tracks, }); module.exports = { /** - * Generates a release YAML with data + * Generates Jekyll-compatible release data */ - generateReleaseInfo: taggedFiles => { + generateReleaseInfo: async (root, taggedFiles) => { + // Create collections + const trackCollectionRoot = path.resolve(root, '_tracks'); + await fs.ensureDir(trackCollectionRoot); + const albums = _.groupBy(taggedFiles, file => path.dirname(file.path)); - _.forEach(albums, (albumTracks, albumRoot) => { - const baseName = path.basename(albumRoot); - debug( - `generating release info for album '${baseName}' with ${ - albumTracks.length - } track(s)` - ); - const releaseInfo = yaml.safeDump(getAlbumInfo(albumTracks)); - fs.writeFileSync(path.resolve(albumRoot, `${baseName}.yml`), releaseInfo); - }); + const albumsInfo = await Promise.all( + _.map(albums, async (tracks, albumRoot) => { + const baseName = path.basename(albumRoot); + const albumCollectionRoot = path.resolve(root, '_albums'); + await fs.ensureDir(albumCollectionRoot); + debug( + `generating release info for album '${baseName}' with ${ + tracks.length + } track(s)` + ); + const albumInfo = getAlbumInfo(albumRoot, tracks); + const releaseInfo = `---\n${yaml.safeDump(albumInfo)}---\n`; + await fs.writeFile( + path.resolve(albumCollectionRoot, `${baseName}.md`), + releaseInfo + ); + + // Write track collection + await Promise.all( + tracks.map(async track => { + const trackInfoPath = path.resolve( + trackCollectionRoot, + baseName, + `${track.slug}.md` + ); + await fs.ensureFile(trackInfoPath); + await fs.writeFile( + trackInfoPath, + `---\n${yaml.safeDump({ + layout: 'track', + ...track, + })}---\n` + ); + }) + ); + + return albumInfo; + }) + ); + + // Create album data + debug(`generating data for ${albumsInfo.length} album(s)`); + const albumsInfoPath = path.resolve(root, '_data', 'albums.yml'); + await fs.ensureFile(albumsInfoPath); + await fs.writeFile(albumsInfoPath, yaml.safeDump(albumsInfo)); }, }; diff --git a/src/organise.js b/src/organise.js index 6ca7156..2274a9e 100644 --- a/src/organise.js +++ b/src/organise.js @@ -6,13 +6,27 @@ const debug = require('debug')('publikator:organise'); const mime = require('mime-types'); const tags = require('./tags'); -const getFolderName = file => file.common.album.replace(/ /g, '_'); +/** + * Given a track file, return the album name. + */ +const getAlbumName = file => file.common.album.replace(/ /g, '_'); +/** + * Given a track file, return the new file name. + */ const getFileName = file => `${file.common.track.no}-${file.common.title}${path.extname( file.path )}`.replace(/ /g, '_'); +/** + * Strips the extension from a file name; + */ +const stripExtension = fileName => { + const i = fileName.lastIndexOf('.'); + return fileName.substr(0, i); +}; + /** * Extracts the cover art and saves it to a file with the same name. */ @@ -43,6 +57,7 @@ module.exports = { * Returns `taggedFiles` with the paths changed to the new paths. */ byAlbum: async (root, taggedFiles) => { + const assetRoot = path.resolve(root, 'assets', 'albums'); const files = taggedFiles.filter(file => tags.hasTags(file, [ 'common.artists', @@ -53,36 +68,38 @@ module.exports = { ); debug(`grouping tracks by album`); - const folders = _.uniq(files.map(file => getFolderName(file))); + const folders = _.uniq(files.map(file => getAlbumName(file))); debug(`found ${folders.length} album(s)`); debug(folders); debug(`creating album directories`); await Promise.all( - folders.map(album => fs.ensureDir(path.resolve(root, sanitize(album)))) + folders.map(album => + fs.ensureDir(path.resolve(assetRoot, sanitize(album))) + ) ); debug(`copying tracks & extracting covers`); return Promise.all( files.map(async file => { - const folderName = getFolderName(file); + const folderName = getAlbumName(file); const fileName = getFileName(file); - const newPath = path.resolve(root, folderName, fileName); + const newPath = path.resolve(assetRoot, folderName, fileName); await fs.copyFile(file.path, newPath); const coverPath = await extractCoverArt(newPath); return _.assign( {}, { path: newPath, - relativePath: `${folderName}/${fileName}`, - folderName, - fileName, + url: `/assets/albums/${folderName}/${fileName}`, + slug: stripExtension(fileName), }, coverPath ? { coverPath, - relativeCoverPath: `${folderName}/${path.basename(coverPath)}`, - coverFileName: path.basename(coverPath), + cover: `/assets/albums/${folderName}/${path.basename( + coverPath + )}`, } : {}, _.omit(file, 'path')