Initial release
This commit is contained in:
parent
23509c3816
commit
73d5c3335f
8
.eslintrc.yaml
Normal file
8
.eslintrc.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
parserOptions:
|
||||||
|
ecmaVersion: 2017
|
||||||
|
sourceType: module
|
||||||
|
env:
|
||||||
|
node: true
|
||||||
|
extends:
|
||||||
|
- airbnb-base
|
||||||
|
- prettier
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
test/
|
||||||
|
out/
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode", "stkb.rewrap"]
|
||||||
|
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"prettier.singleQuote": true,
|
||||||
|
"prettier.trailingComma": "es5",
|
||||||
|
"editor.rulers": [80, 100],
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
}
|
35
README.md
Normal file
35
README.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Publikator
|
||||||
|
|
||||||
|
The rusty metal heart of the Basspistol release machine.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Make sure `nodejs` is installed and up-tp-date:
|
||||||
|
|
||||||
|
```
|
||||||
|
brew install node
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Install `Publikator` globally via `npm`:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install -g https://github.com/aengl/publikator
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Repeat the previous step to update to the latest version.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To get help, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
publikator -h
|
||||||
|
```
|
||||||
|
|
||||||
|
To organise tracks and generate release information:
|
||||||
|
|
||||||
|
```
|
||||||
|
publikator organise pathToMySongs outputPath
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the `--delete` flag to start with a clean output directory.
|
5
nodemon.json
Normal file
5
nodemon.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"watch": ["src"],
|
||||||
|
"ext": "js",
|
||||||
|
"exec": "node"
|
||||||
|
}
|
35
package.json
Normal file
35
package.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "publikator",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": "https://github.com/aengl/publikator.git",
|
||||||
|
"author": "Lynn Smeria <ae@cephea.de>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"publikator": "publikator.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"caporal": "0.10.0",
|
||||||
|
"debug": "3.1.0",
|
||||||
|
"fs-extra": "7.0.0",
|
||||||
|
"js-yaml": "3.12.0",
|
||||||
|
"jsmediatags": "3.8.1",
|
||||||
|
"lodash": "4.17.10",
|
||||||
|
"sanitize-filename": "1.6.1",
|
||||||
|
"walkdir": "0.0.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "5.3.0",
|
||||||
|
"eslint-config-airbnb-base": "13.1.0",
|
||||||
|
"eslint-config-prettier": "3.0.1",
|
||||||
|
"eslint-plugin-import": "2.14.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "6.1.1",
|
||||||
|
"nodemon": "1.18.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon src/cli.js"
|
||||||
|
}
|
||||||
|
}
|
3
publikator.js
Executable file
3
publikator.js
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('./src/cli');
|
49
src/cli.js
Normal file
49
src/cli.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
const debugModule = require('debug');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const program = require('caporal');
|
||||||
|
const scan = require('./scan');
|
||||||
|
const organise = require('./organise');
|
||||||
|
const generate = require('./generate');
|
||||||
|
|
||||||
|
const debug = debugModule('publikator:cli');
|
||||||
|
const packageJson = require('../package.json');
|
||||||
|
|
||||||
|
process.on('unhandledRejection', error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
program.version(packageJson.version);
|
||||||
|
|
||||||
|
/* ~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^
|
||||||
|
* Command: organise
|
||||||
|
* ~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^ */
|
||||||
|
|
||||||
|
program
|
||||||
|
.command(
|
||||||
|
'organise',
|
||||||
|
'Recursively finds all mp3s in a folder, reads their tags and re-organises them'
|
||||||
|
)
|
||||||
|
.argument('<source>', 'Root folder for the recursive search')
|
||||||
|
.argument('<target>', 'Target folder for the restructured output')
|
||||||
|
.option('-d, --delete', 'Completely delete the target folder first')
|
||||||
|
.action(async (args, options) => {
|
||||||
|
if (!process.env.DEBUG) {
|
||||||
|
debugModule.enable('publikator:*');
|
||||||
|
}
|
||||||
|
const source = path.resolve(args.source);
|
||||||
|
const target = path.resolve(args.target);
|
||||||
|
if (options.delete) {
|
||||||
|
debug(`deleting folder '${target}'`);
|
||||||
|
fs.removeSync(target);
|
||||||
|
}
|
||||||
|
fs.ensureDirSync(args.target);
|
||||||
|
const files = scan.findFilesSync(source);
|
||||||
|
const filesWithTags = await scan.readTags(files);
|
||||||
|
const organisedFiles = await organise.byAlbum(target, filesWithTags);
|
||||||
|
const releaseInfo = generate.releaseInfo(organisedFiles);
|
||||||
|
fs.writeFileSync(path.resolve(target, 'releases.yml'), releaseInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(process.argv);
|
||||||
|
program.parse(process.argv);
|
39
src/generate.js
Normal file
39
src/generate.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const path = require('path');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const debug = require('debug')('publikator:generate');
|
||||||
|
|
||||||
|
const getTags = (track, tags) =>
|
||||||
|
tags.reduce((all, tag) => {
|
||||||
|
all[tag] = track[tag]; // eslint-disable-line
|
||||||
|
return all;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
releaseInfo: files => {
|
||||||
|
debug(`generating release info for ${files.length} file(s)`);
|
||||||
|
const albums = _.groupBy(files, file => path.dirname(file.path));
|
||||||
|
return yaml.safeDump(
|
||||||
|
Object.keys(albums).map(key => {
|
||||||
|
const tracks = albums[key];
|
||||||
|
return {
|
||||||
|
'track-count': tracks.length,
|
||||||
|
tracks: tracks.map((track, i) => ({
|
||||||
|
path: track.path,
|
||||||
|
size: track.size,
|
||||||
|
position: i,
|
||||||
|
tags: getTags(track.tags, [
|
||||||
|
'title',
|
||||||
|
'artist',
|
||||||
|
'album',
|
||||||
|
'year',
|
||||||
|
'comment',
|
||||||
|
'track',
|
||||||
|
'genre',
|
||||||
|
]),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
55
src/organise.js
Normal file
55
src/organise.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const sanitize = require('sanitize-filename');
|
||||||
|
const debug = require('debug')('publikator:organise');
|
||||||
|
|
||||||
|
const ensureTags = (files, tags) => {
|
||||||
|
return files.filter(file => {
|
||||||
|
if (tags.some(tag => file.tags[tag] === undefined)) {
|
||||||
|
debug(
|
||||||
|
`ignored '${file.path}' because it is missing one or more required tags`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFolderName = file => `${file.tags.artist} - ${file.tags.album}`;
|
||||||
|
const getFileName = file =>
|
||||||
|
`${file.tags.track} - ${file.tags.title}${path.extname(file.path)}`;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
byAlbum: async (root, filesWithTags) => {
|
||||||
|
const files = ensureTags(filesWithTags, [
|
||||||
|
'artist',
|
||||||
|
'album',
|
||||||
|
'track',
|
||||||
|
'title',
|
||||||
|
]);
|
||||||
|
|
||||||
|
debug(`grouping tracks by album`);
|
||||||
|
const folders = _.uniq(files.map(file => getFolderName(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))))
|
||||||
|
);
|
||||||
|
|
||||||
|
debug(`copying tracks`);
|
||||||
|
return Promise.all(
|
||||||
|
files.map(file => {
|
||||||
|
const newPath = path.resolve(
|
||||||
|
root,
|
||||||
|
getFolderName(file),
|
||||||
|
getFileName(file)
|
||||||
|
);
|
||||||
|
fs.copyFileSync(file.path, newPath);
|
||||||
|
return _.assign({}, file, { path: newPath });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
44
src/scan.js
Normal file
44
src/scan.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
const debug = require('debug')('publikator:scan');
|
||||||
|
const jsmediatags = require('jsmediatags');
|
||||||
|
const walk = require('walkdir');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
findFilesSync: (root, extension = '.mp3') => {
|
||||||
|
debug(
|
||||||
|
`scanning directory '${root}' for files with extension '${extension}'`
|
||||||
|
);
|
||||||
|
const files = [];
|
||||||
|
walk.sync(root, path => {
|
||||||
|
if (path.endsWith(extension)) {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
debug(`found ${files.length} file(s)`);
|
||||||
|
return files;
|
||||||
|
},
|
||||||
|
|
||||||
|
readTags: files => {
|
||||||
|
debug(`reading tags for ${files.length} file(s)`);
|
||||||
|
return Promise.all(
|
||||||
|
files.map(
|
||||||
|
file =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
jsmediatags.read(file, {
|
||||||
|
onSuccess: info => {
|
||||||
|
resolve({
|
||||||
|
path: file,
|
||||||
|
size: info.size,
|
||||||
|
tags: info.tags,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
debug(error.type);
|
||||||
|
debug(error.info);
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in a new issue