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