diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 1475d78..888dd18 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -1,5 +1,5 @@ parserOptions: - ecmaVersion: 2017 + ecmaVersion: 2018 sourceType: module env: node: true diff --git a/src/cli.js b/src/cli.js index 558c5ea..05d4a83 100644 --- a/src/cli.js +++ b/src/cli.js @@ -39,8 +39,8 @@ program } fs.ensureDirSync(args.target); const files = scan.findFilesSync(source); - const filesWithTags = await scan.readTags(files); - const organisedFiles = await organise.byAlbum(target, filesWithTags); + const taggedFiles = await scan.readTags(files); + const organisedFiles = await organise.byAlbum(target, taggedFiles); const releaseInfo = generate.releaseInfo(organisedFiles); fs.writeFileSync(path.resolve(target, 'releases.yml'), releaseInfo); }); diff --git a/src/generate.js b/src/generate.js index b245c1d..73b4df8 100644 --- a/src/generate.js +++ b/src/generate.js @@ -2,17 +2,15 @@ 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; - }, {}); +const tags = require('./tags'); module.exports = { - releaseInfo: files => { - debug(`generating release info for ${files.length} file(s)`); - const albums = _.groupBy(files, file => path.dirname(file.path)); + /** + * Generates a release YAML with data + */ + releaseInfo: taggedFiles => { + debug(`generating release info for ${taggedFiles.length} file(s)`); + const albums = _.groupBy(taggedFiles, file => path.dirname(file.path)); return yaml.safeDump( Object.keys(albums).map(key => { const tracks = albums[key]; @@ -22,7 +20,7 @@ module.exports = { path: track.path, size: track.size, position: i, - tags: getTags(track.tags, [ + ...tags.getTags(track, [ 'title', 'artist', 'album', diff --git a/src/organise.js b/src/organise.js index 9e6786c..fee68ad 100644 --- a/src/organise.js +++ b/src/organise.js @@ -3,31 +3,27 @@ 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 tags = require('./tags'); 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', - ]); + /** + * Organises tracks into a new folder structure in `root`, as follows: + * + * {artist} - {album}/ + * {track} - {title}.{ext} + * {track} - {title}.{ext} + * ... + * + * Returns `taggedFiles` with the paths changed to the new paths. + */ + byAlbum: async (root, taggedFiles) => { + const files = taggedFiles.filter(file => + tags.hasTags(file, ['artist', 'album', 'track', 'title']) + ); debug(`grouping tracks by album`); const folders = _.uniq(files.map(file => getFolderName(file))); @@ -41,13 +37,13 @@ module.exports = { debug(`copying tracks`); return Promise.all( - files.map(file => { + files.map(async file => { const newPath = path.resolve( root, getFolderName(file), getFileName(file) ); - fs.copyFileSync(file.path, newPath); + await fs.copyFile(file.path, newPath); return _.assign({}, file, { path: newPath }); }) ); diff --git a/src/scan.js b/src/scan.js index 002544d..00bf14c 100644 --- a/src/scan.js +++ b/src/scan.js @@ -1,8 +1,11 @@ const debug = require('debug')('publikator:scan'); -const jsmediatags = require('jsmediatags'); const walk = require('walkdir'); +const tags = require('./tags'); module.exports = { + /** + * Recursively searches for files. + */ findFilesSync: (root, extension = '.mp3') => { debug( `scanning directory '${root}' for files with extension '${extension}'` @@ -17,28 +20,21 @@ module.exports = { return files; }, + /** + * Reads ID3 tags from all files and returns an array in the form of: + * [{ path, size, tags }, ...] + */ readTags: files => { - debug(`reading tags for ${files.length} file(s)`); + debug(`reading tags from ${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); - }, - }); - }) - ) + files.map(async file => { + const info = await tags.readTags(file); + return { + path: file, + size: info.size, + tags: info.tags, + }; + }) ); }, }; diff --git a/src/tags.js b/src/tags.js new file mode 100644 index 0000000..b4292c8 --- /dev/null +++ b/src/tags.js @@ -0,0 +1,43 @@ +const jsmediatags = require('jsmediatags'); +const debug = require('debug')('publikator:tags'); + +module.exports = { + /** + * Reads tags from a track. + */ + readTags: file => + new Promise((resolve, reject) => { + jsmediatags.read(file, { + onSuccess: info => { + resolve(info); + }, + onError: error => { + debug(error.type); + debug(error.info); + reject(error); + }, + }); + }), + + /** + * Returns true if a file has all required tags. + */ + hasTags: (taggedFile, tags) => { + if (tags.some(tag => taggedFile.tags[tag] === undefined)) { + debug(`track'${taggedFile.path}' is missing one or more required tags`); + return false; + } + return true; + }, + + /** + * Extracts tags from a file into an object, ignoring missing tags. + */ + getTags: (taggedFile, tags) => + tags.reduce((all, tag) => { + if (taggedFile.tags[tag] !== undefined) { + all[tag] = taggedFile.tags[tag]; // eslint-disable-line + } + return all; + }, {}), +};