Create Jekyll collections
This commit is contained in:
parent
7c8138c386
commit
328c56fa19
77
README.md
77
README.md
|
@ -2,6 +2,25 @@
|
||||||
|
|
||||||
The rusty metal heart of the Basspistol release machine.
|
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
|
## Installation
|
||||||
|
|
||||||
1. Make sure `nodejs` is installed and up-tp-date:
|
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.
|
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
|
||||||
|
---
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for album in site.albums %}
|
||||||
|
<li>
|
||||||
|
<a href="/{{ album.slug }}">
|
||||||
|
{{ album.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
Each individual album will be available at the url `/<album_slug>`. To create a detail page for an album, create a new layout `_layouts/album.html`:
|
||||||
|
|
||||||
|
```
|
||||||
|
<h1>{{ page.album }}</h1>
|
||||||
|
<img src="{{ page.cover }}" />
|
||||||
|
<ul>
|
||||||
|
{% for track in page.tracks %}
|
||||||
|
<li>
|
||||||
|
<a href="/{{ page.slug }}/{{ track.slug }}">
|
||||||
|
{{ track.common.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tracks
|
||||||
|
|
||||||
|
Each individual track will be available at the url `/<album_slug>/<track_slug>`. To create a detail page for a track, create a new layout `_layouts/track.html`:
|
||||||
|
|
||||||
|
```
|
||||||
|
<h1>{{ page.common.title }}</h1>
|
||||||
|
<img src="{{ page.cover }}" />
|
||||||
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "publikator",
|
"name": "publikator",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "https://github.com/aengl/publikator.git",
|
"repository": "https://github.com/aengl/publikator.git",
|
||||||
"author": "Lynn Smeria <ae@cephea.de>",
|
"author": "Lynn Smeria <ae@cephea.de>",
|
||||||
|
|
|
@ -41,7 +41,7 @@ program
|
||||||
const files = scan.findFilesSync(source);
|
const files = scan.findFilesSync(source);
|
||||||
const taggedFiles = await scan.readTags(files);
|
const taggedFiles = await scan.readTags(files);
|
||||||
const organisedFiles = await organise.byAlbum(target, taggedFiles);
|
const organisedFiles = await organise.byAlbum(target, taggedFiles);
|
||||||
generate.generateReleaseInfo(organisedFiles);
|
await generate.generateReleaseInfo(target, organisedFiles);
|
||||||
});
|
});
|
||||||
|
|
||||||
debug(process.argv);
|
debug(process.argv);
|
||||||
|
|
|
@ -20,29 +20,71 @@ const collect = (tracks, callback) => {
|
||||||
/**
|
/**
|
||||||
* Creates release information for a single album.
|
* 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),
|
artists: collect(tracks, t => t.common.artists || t.common.artist),
|
||||||
album: collect(tracks, t => t.common.album),
|
|
||||||
bitrate: collect(tracks, t => t.format.bitrate),
|
bitrate: collect(tracks, t => t.format.bitrate),
|
||||||
trackCount: tracks.length,
|
trackCount: tracks.length,
|
||||||
|
cover: tracks[0].coverUrl || null,
|
||||||
tracks,
|
tracks,
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
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));
|
const albums = _.groupBy(taggedFiles, file => path.dirname(file.path));
|
||||||
_.forEach(albums, (albumTracks, albumRoot) => {
|
const albumsInfo = await Promise.all(
|
||||||
|
_.map(albums, async (tracks, albumRoot) => {
|
||||||
const baseName = path.basename(albumRoot);
|
const baseName = path.basename(albumRoot);
|
||||||
|
const albumCollectionRoot = path.resolve(root, '_albums');
|
||||||
|
await fs.ensureDir(albumCollectionRoot);
|
||||||
debug(
|
debug(
|
||||||
`generating release info for album '${baseName}' with ${
|
`generating release info for album '${baseName}' with ${
|
||||||
albumTracks.length
|
tracks.length
|
||||||
} track(s)`
|
} track(s)`
|
||||||
);
|
);
|
||||||
const releaseInfo = yaml.safeDump(getAlbumInfo(albumTracks));
|
const albumInfo = getAlbumInfo(albumRoot, tracks);
|
||||||
fs.writeFileSync(path.resolve(albumRoot, `${baseName}.yml`), releaseInfo);
|
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));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,13 +6,27 @@ const debug = require('debug')('publikator:organise');
|
||||||
const mime = require('mime-types');
|
const mime = require('mime-types');
|
||||||
const tags = require('./tags');
|
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 =>
|
const getFileName = file =>
|
||||||
`${file.common.track.no}-${file.common.title}${path.extname(
|
`${file.common.track.no}-${file.common.title}${path.extname(
|
||||||
file.path
|
file.path
|
||||||
)}`.replace(/ /g, '_');
|
)}`.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.
|
* 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.
|
* Returns `taggedFiles` with the paths changed to the new paths.
|
||||||
*/
|
*/
|
||||||
byAlbum: async (root, taggedFiles) => {
|
byAlbum: async (root, taggedFiles) => {
|
||||||
|
const assetRoot = path.resolve(root, 'assets', 'albums');
|
||||||
const files = taggedFiles.filter(file =>
|
const files = taggedFiles.filter(file =>
|
||||||
tags.hasTags(file, [
|
tags.hasTags(file, [
|
||||||
'common.artists',
|
'common.artists',
|
||||||
|
@ -53,36 +68,38 @@ module.exports = {
|
||||||
);
|
);
|
||||||
|
|
||||||
debug(`grouping tracks by album`);
|
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(`found ${folders.length} album(s)`);
|
||||||
debug(folders);
|
debug(folders);
|
||||||
|
|
||||||
debug(`creating album directories`);
|
debug(`creating album directories`);
|
||||||
await Promise.all(
|
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`);
|
debug(`copying tracks & extracting covers`);
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
files.map(async file => {
|
files.map(async file => {
|
||||||
const folderName = getFolderName(file);
|
const folderName = getAlbumName(file);
|
||||||
const fileName = getFileName(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);
|
await fs.copyFile(file.path, newPath);
|
||||||
const coverPath = await extractCoverArt(newPath);
|
const coverPath = await extractCoverArt(newPath);
|
||||||
return _.assign(
|
return _.assign(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
path: newPath,
|
path: newPath,
|
||||||
relativePath: `${folderName}/${fileName}`,
|
url: `/assets/albums/${folderName}/${fileName}`,
|
||||||
folderName,
|
slug: stripExtension(fileName),
|
||||||
fileName,
|
|
||||||
},
|
},
|
||||||
coverPath
|
coverPath
|
||||||
? {
|
? {
|
||||||
coverPath,
|
coverPath,
|
||||||
relativeCoverPath: `${folderName}/${path.basename(coverPath)}`,
|
cover: `/assets/albums/${folderName}/${path.basename(
|
||||||
coverFileName: path.basename(coverPath),
|
coverPath
|
||||||
|
)}`,
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
_.omit(file, 'path')
|
_.omit(file, 'path')
|
||||||
|
|
Loading…
Reference in a new issue