Initial release

This commit is contained in:
Lynn Smeria 2018-08-14 19:26:02 +03:00
parent 23509c3816
commit 73d5c3335f
13 changed files with 2989 additions and 0 deletions

8
.eslintrc.yaml Normal file
View file

@ -0,0 +1,8 @@
parserOptions:
ecmaVersion: 2017
sourceType: module
env:
node: true
extends:
- airbnb-base
- prettier

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
test/
out/

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode", "stkb.rewrap"]
}

7
.vscode/settings.json vendored Normal file
View 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
View 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
View file

@ -0,0 +1,5 @@
{
"watch": ["src"],
"ext": "js",
"exec": "node"
}

35
package.json Normal file
View 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
View file

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('./src/cli');

49
src/cli.js Normal file
View 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
View 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
View 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
View 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);
},
});
})
)
);
},
};

2703
yarn.lock Normal file

File diff suppressed because it is too large Load diff