Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try add MusicBrainz tag to file #82

Open
wants to merge 43 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
dabfef3
Try add MusicBrainz tag to file
Maxmystere Sep 25, 2021
4b12c70
Move logic to cli:trackQueue
Maxmystere Oct 1, 2021
12aec89
Clean code
Maxmystere Oct 1, 2021
e4aff77
Final code cleaning and separation
Maxmystere Oct 1, 2021
b55eb42
Cleaner logs
Maxmystere Oct 1, 2021
6ccc4b1
Sync logs
Maxmystere Oct 1, 2021
c95d255
Merge branch 'master' into Add-MBID-tag
miraclx Oct 2, 2021
fc22292
touch lookup logic, extract extra metadata
miraclx Oct 2, 2021
d524f78
drop AtomicParsley ignore
miraclx Oct 2, 2021
524774a
Merge branch 'master' into Add-MBID-tag
miraclx Oct 3, 2021
cdbbac9
show error msg + code when musicbrainz lookup failed
miraclx Oct 3, 2021
2fe8a68
add cli flag `-m, --musicbrainz` for enabling musicbrainz functionality
miraclx Oct 3, 2021
afc16e6
default storefront = us;
miraclx Oct 3, 2021
2248ca1
embed release country
miraclx Oct 3, 2021
4d0ba9f
apply default if not using musicbrainz
miraclx Oct 3, 2021
0757056
catch and diffuse any caught error in the current execution loop
miraclx Oct 3, 2021
9c1129c
prioritize storefront+digital media matches
miraclx Oct 3, 2021
7825c3c
cache musicbrainz lookups for efficiency
miraclx Oct 3, 2021
e1a16e3
limit the scope of the args fields: inc & json only
miraclx Oct 3, 2021
2d5c59b
remove static inc from template url
miraclx Oct 3, 2021
c370653
releaseType should be lowerCase
miraclx Oct 3, 2021
52a948c
Merge branch 'miraclx:master' into Add-MBID-tag
Maxmystere May 14, 2022
e489991
Merge remote-tracking branch 'origin/master' into Add-MBID-tag
Maxmystere Aug 23, 2022
320a206
Fix merge issue
Maxmystere Aug 23, 2022
e1ecc53
Finish merge + fixes
Maxmystere Aug 23, 2022
4b79bb8
Add flac support + use taglib for tagging flac
Maxmystere Aug 25, 2022
a454a83
CRLF -> LF
Maxmystere Aug 25, 2022
b49a8e9
Disable Musicbrainz for AtomicParsley has it is unsupported
Maxmystere Aug 25, 2022
8042571
Comment fix
Maxmystere Aug 25, 2022
cfac169
Merge branch 'master' into Add-MBID-tag
Maxmystere Aug 25, 2022
bec59fb
Fix format
Maxmystere Aug 29, 2022
c8a048b
Merge branch 'master' into Add-MBID-tag
Maxmystere Aug 29, 2022
8ecc465
Secure get
Maxmystere Aug 30, 2022
a1b5fd3
eslint
Maxmystere Aug 30, 2022
36b16e1
Up from master
Maxmystere Sep 20, 2022
86fc599
Merge remote-tracking branch 'origin/master' into Add-MBID-tag
Maxmystere Sep 20, 2022
b84283f
LF
Maxmystere Sep 20, 2022
7d6796e
Fix issue with metadata extract
Maxmystere Sep 20, 2022
d50b7e6
Merge remote-tracking branch 'origin/master' into Add-MBID-tag
Maxmystere Oct 28, 2022
0c3f74d
Increase security
Maxmystere Oct 28, 2022
c3633d2
Merge remote-tracking branch 'origin/master' into Add-MBID-tag
Maxmystere Dec 18, 2022
4e67396
Merge remote-tracking branch 'origin/master' into Add-MBID-tag
Maxmystere Jan 1, 2023
1185531
Merge remote-tracking branch 'origin/master' into Add-MBID-tag
Maxmystere Apr 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 140 additions & 60 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import cStringd from 'stringd-colors';
import prettyMs from 'pretty-ms';
import minimatch from 'minimatch';
import filenamify from 'filenamify';
import TagLib from 'node-taglib-sharp';
import TimeFormat from 'hh-mm-ss';
import ProgressBar from 'xprogress';
import countryData from 'country-data';
Expand All @@ -39,6 +40,7 @@ import FreyrCore from './src/freyr.js';
import AuthServer from './src/cli_server.js';
import AsyncQueue from './src/async_queue.js';
import parseRange from './src/parse_range.js';
import musicBrainz from './src/musicbrainz.js';
import StackLogger from './src/stack_logger.js';
import streamUtils from './src/stream_utils.js';
import parseSearchFilter from './src/filter_parser.js';
Expand Down Expand Up @@ -88,7 +90,13 @@ function parseMeta(params) {
return Object.entries(params || {})
.filter(([, value]) => ![undefined, null].includes(value))
.map(([key, value]) =>
Array.isArray(value) ? value.map(tx => (tx ? [`--${key}`, ...(Array.isArray(tx) ? tx : [tx])] : '')) : [`--${key}`, value],
Array.isArray(value)
? value
.filter(val => val !== undefined)
.map((tx, args) =>
(args = Array.isArray(tx) ? tx : [tx]).every(val => val !== undefined) ? [`--${key}`, ...args] : [],
)
: [`--${key}`, value],
)
.flat(Infinity);
}
Expand Down Expand Up @@ -139,6 +147,18 @@ function wrapCliInterface(binaryNames, binaryPath) {
};
}

function embedTagLib(file, meta, cb) {
let myfile = TagLib.File.createFromPath(file);

meta.forEach((value, key) => {
if (value) myfile.tag[key] = value;
});

myfile.save();
myfile.dispose();
cb();
}

function getRetryMessage({meta, ref, retryCount, maxRetries, bytesRead, totalBytes, lastErr}) {
return cStringd(
[
Expand Down Expand Up @@ -526,6 +546,7 @@ async function init(packageJson, queries, options) {
netCheck: {type: 'boolean'},
attemptAuth: {type: 'boolean'},
autoOpenBrowser: {type: 'boolean'},
musicBrainz: {type: 'boolean'},
},
},
dirs: {
Expand Down Expand Up @@ -690,6 +711,7 @@ async function init(packageJson, queries, options) {
netCheck: options.netCheck,
attemptAuth: options.auth,
autoOpenBrowser: options.browser,
musicBrainz: options.musicbrainz,
});
Config.playlist = _merge(Config.playlist, {
always: !!options.playlist,
Expand Down Expand Up @@ -1095,56 +1117,103 @@ async function init(packageJson, queries, options) {
Config.concurrency.embedder,
async ({track, meta, files, audioSource}) => {
try {
await Promise.promisify(atomicParsley)(meta.outFile.path, {
overWrite: '', // overwrite the file

title: track.name, // ©nam
artist: track.artists[0], // ©ART
composer: track.composers, // ©wrt
album: track.album, // ©alb
genre: (genre => (genre ? genre.concat(' ') : ''))((track.genres || [])[0]), // ©gen | gnre
tracknum: `${track.track_number}/${track.total_tracks}`, // trkn
disk: `${track.disc_number}/${track.disc_number}`, // disk
year: new Date(track.release_date).toISOString().split('T')[0], // ©day
compilation: track.compilation, // ©cpil
gapless: options.gapless, // pgap
rDNSatom: [
// ----
['Digital Media', 'name=MEDIA', 'domain=com.apple.iTunes'],
[track.isrc, 'name=ISRC', 'domain=com.apple.iTunes'],
[track.artists[0], 'name=ARTISTS', 'domain=com.apple.iTunes'],
[track.label, 'name=LABEL', 'domain=com.apple.iTunes'],
[`${meta.service[symbols.meta].DESC}: ${track.uri}`, 'name=SOURCE', 'domain=com.apple.iTunes'],
[
`${audioSource.service[symbols.meta].DESC}: ${audioSource.source.videoId}`,
'name=PROVIDER',
'domain=com.apple.iTunes',
if (options.format == 'flac') {
await Promise.promisify(embedTagLib)(
meta.outFile.path,
new Map([
['album', track.album],
['albumArtists', track.artists],
['albumArtistsSort', track.musicBrainz?.artistSortOrder],
['composers', track.composers],
['copyright', track.copyrights.sort(({type}) => (type === 'P' ? -1 : 1))[0]?.text],
[
'description',
`${meta.service[symbols.meta].DESC}: ${track.uri} , ${audioSource.service[symbols.meta].DESC}: ${
audioSource.source.videoId
}`,
],
['disc', track.disc_number],
['discCount', track.disc_number],
['genres', track.genres],
['isrc', track.isrc],
['musicBrainzArtistId', track.musicBrainz?.artistId],
['musicBrainzReleaseArtistId', track.musicBrainz?.artistId],
['musicBrainzReleaseCountry', track.musicBrainz?.releaseCountry],
['musicBrainzReleaseGroupId', track.musicBrainz?.releaseGroupId],
['musicBrainzReleaseId', track.musicBrainz?.releaseId],
['musicBrainzReleaseStatus', track.musicBrainz?.releaseStatus],
['musicBrainzReleaseType', track.musicBrainz?.releaseType],
['musicBrainzTrackId', track.musicBrainz?.trackId],
['performers', track.artists],
['title', track.name],
['track', track.track_number],
['trackCount', track.total_tracks],
['year', new Date(track.release_date).getFullYear()],
]),
);
} else {
await Promise.promisify(atomicParsley)(meta.outFile.path, {
overWrite: '', // overwrite the file

title: track.name, // ©nam
artist: track.artists[0], // ©ART
composer: track.composers, // ©wrt
album: track.album, // ©alb
genre: (genre => (genre ? genre.concat(' ') : ''))((track.genres || [])[0]), // ©gen | gnre
tracknum: `${track.track_number}/${track.total_tracks}`, // trkn
disk: `${track.disc_number}/${track.disc_number}`, // disk
year: new Date(track.release_date).toISOString().split('T')[0], // ©day
compilation: track.compilation, // ©cpil
gapless: options.gapless, // pgap
rDNSatom: [
// ----
['Digital Media', 'name=MEDIA', 'domain=com.apple.iTunes'],
[track.isrc, 'name=ISRC', 'domain=com.apple.iTunes'],
[track.artists[0], 'name=ARTISTS', 'domain=com.apple.iTunes'],
[track.label, 'name=LABEL', 'domain=com.apple.iTunes'],
// There is a bug in Atomic Parsley currently preventing MusicBrainz tagging
//[track.musicBrainz.trackId, 'name="MusicBrainz Track Id"', 'domain=com.apple.iTunes'],
//[track.musicBrainz.artistId, 'name="MusicBrainz Artist Id"', 'domain=com.apple.iTunes'],
//[track.musicBrainz.artistId, 'name="MusicBrainz Album Artist Id"', 'domain=com.apple.iTunes'],
//[track.musicBrainz.releaseId, 'name="MusicBrainz Album Id"', 'domain=com.apple.iTunes'],
//[track.musicBrainz.releaseGroupId, 'name="MusicBrainz Release Group Id"', 'domain=com.apple.iTunes'],
//[track.musicBrainz.barcode, 'name=BARCODE', 'domain=com.apple.iTunes'],
//[track.musicBrainz.releaseStatus, 'name="MusicBrainz Album Status"', 'domain=com.apple.iTunes'],
//[track.musicBrainz.releaseCountry, 'name="MusicBrainz Album Release Country"', 'domain=com.apple.iTunes'],
//[track.musicBrainz.script, 'name=SCRIPT', 'domain=com.apple.iTunes'],
//[track.musicBrainz.media, 'name=MEDIA', 'domain=com.apple.iTunes'],
[`${meta.service[symbols.meta].DESC}: ${track.uri}`, 'name=SOURCE', 'domain=com.apple.iTunes'],
[
`${audioSource.service[symbols.meta].DESC}: ${audioSource.source.videoId}`,
'name=PROVIDER',
'domain=com.apple.iTunes',
],
],
],
advisory: ['explicit', 'clean'].includes(track.contentRating) // rtng
? track.contentRating
: track.contentRating === true
? 'explicit'
: 'Inoffensive',
stik: 'Normal', // stik
// geID: 0, // geID: genreID. See `AtomicParsley --genre-list`
// sfID: 0, // ~~~~: store front ID
// cnID: 0, // cnID: catalog ID
albumArtist: track.album_artist, // aART
// ownr? <owner>
purchaseDate: 'timestamp', // purd
apID: '[email protected]', // apID
copyright: track.copyrights.sort(({type}) => (type === 'P' ? -1 : 1))[0]?.text, // cprt
encodingTool: `freyr-js cli v${packageJson.version}`, // ©too
encodedBy: 'd3vc0dr', // ©enc
artwork: files.image.file.path, // covr
// sortOrder: [
// ['name', 'NAME'], // sonm
// ['album', 'NAME'], // soal
// ['artist', 'NAME'], // soar
// ['albumartist', 'NAME'], // soaa
// ],
});
advisory: ['explicit', 'clean'].includes(track.contentRating) // rtng
? track.contentRating
: track.contentRating === true
? 'explicit'
: 'Inoffensive',
stik: 'Normal', // stik
// geID: 0, // geID: genreID. See `AtomicParsley --genre-list`
// sfID: 0, // ~~~~: store front ID
// cnID: 0, // cnID: catalog ID
albumArtist: track.album_artist, // aART
// ownr? <owner>
purchaseDate: 'timestamp', // purd
apID: '[email protected]', // apID
copyright: track.copyrights.sort(({type}) => (type === 'P' ? -1 : 1))[0].text, // cprt
encodingTool: `freyr-js cli v${packageJson.version}`, // ©too
encodedBy: 'd3vc0dr', // ©enc
artwork: files.image.file.path, // covr
// sortOrder: [
// ['name', 'NAME'], // sonm
// ['album', 'NAME'], // soal
// ['artist', track.musicBrainz.artistSortOrder], // soar
// ['albumartist', track.musicBrainz.artistSortOrder], // soaa
// ],
});
}
} catch (err) {
throw {err, [symbols.errorCode]: 8};
}
Expand Down Expand Up @@ -1172,7 +1241,7 @@ async function init(packageJson, queries, options) {
'-i',
infile,
'-acodec',
'aac',
options.format == 'flac' ? 'flac' : 'aac',
'-b:a',
options.bitrate,
'-ar',
Expand All @@ -1181,9 +1250,7 @@ async function init(packageJson, queries, options) {
'-t',
TimeFormat.fromMs(track.duration, 'hh:mm:ss.sss'),
'-f',
'ipod',
'-aac_pns',
'0',
options.format == 'flac' ? 'flac' : 'ipod',
outfile,
);
await fs.writeFile(meta.outFile.handle, ffmpeg.FS('readFile', outfile));
Expand Down Expand Up @@ -1315,6 +1382,12 @@ async function init(packageJson, queries, options) {
for (let path of otherLocations) trackLogger.log(`| [\u2022] - ${path}`);
}
}
track.musicBrainz =
(await processPromise(props.extraTrackMeta, trackLogger, {
onInit: '| \u27a4 Sourcing extra metadata...',
noVal: () => '[skipped]\n',
onErr: err => `[failed, ${err.statusCode ? `(${err.statusCode}) ` : ''}${err.message}]\n`,
})) || {};
trackLogger.log('| \u27a4 Collating sources...');
const audioSource = await props.collectSources((service, sourcesPromise) =>
processPromise(sourcesPromise, trackLogger, {
Expand Down Expand Up @@ -1373,9 +1446,10 @@ async function init(packageJson, queries, options) {
? ` \u2012 ${track.artists.join(', ')}`
: '',
);
const fileExtension = options.format == 'flac' ? 'flac' : 'm4a';
const outFileName = `${filenamify(trackBaseName, {
replacement: '_',
})}.m4a`;
})}.${fileExtension}`;
const trackPath = xpath.join(
...(options.tree ? [track.album_artist, track.album].map(name => filenamify(name, {replacement: '_'})) : []),
);
Expand All @@ -1390,8 +1464,14 @@ async function init(packageJson, queries, options) {
).flatMap(([dir, exists]) => (exists ? [dir] : []));
let fileExists = !!fileExistsIn.length;
const processTrack = !fileExists || options.force;
let collectSources;
if (processTrack) collectSources = buildSourceCollectorFor(track, results => results[0]);
let collectSources, extraTrackMeta;
if (processTrack) {
collectSources = buildSourceCollectorFor(track, results => results[0]);
if (Config.opts.musicBrainz && track.isrc) {
extraTrackMeta = musicBrainz.lookupISRC(track.isrc, options.storefront || 'us');
Promise.resolve(extraTrackMeta).catch(() => {}); // diffuse any caught error in the meantime
}
}
const meta = {
trackName,
outFile: {path: outFilePath},
Expand All @@ -1404,6 +1484,7 @@ async function init(packageJson, queries, options) {
meta,
props: {
collectSources,
extraTrackMeta,
fileExists,
fileExistsIn,
processTrack,
Expand Down Expand Up @@ -1791,13 +1872,11 @@ function prepCli(packageJson) {
'640x640',
)
.option('-C, --no-cover', 'skip saving a cover art')
/* Unimplemented Feature
.option(
'-x, --format <FORMAT>',
['preferred audio output format (to export) (unimplemented)', '(valid: mp3,m4a,flac)'].join('\n'),
'm4a',
)
*/
.option(
'-S, --sources <SERVICE>',
[
Expand All @@ -1806,6 +1885,7 @@ function prepCli(packageJson) {
].join('\n'),
'yt_music',
)
.option('-m, --musicbrainz', 'attempt to source and embed extra metadata from MusicBrainz')
.option(
'-l, --filter <MATCH>',
[
Expand Down Expand Up @@ -1863,7 +1943,7 @@ function prepCli(packageJson) {
.option('--rm-cache [RM]', 'remove original downloaded files in cache directory (default: false)', v =>
['true', '1', 'yes', 'y'].includes(v) ? true : ['false', '0', 'no', 'n'].includes(v) ? false : v,
)
.option('-m, --mem-cache <SIZE>', 'max size of bytes to be cached in-memory for each download chunk')
.option('-M, --mem-cache <SIZE>', 'max size of bytes to be cached in-memory for each download chunk')
.option('--no-mem-cache', 'disable in-memory chunk caching (restricts to sequential download)')
.option('--timeout <N>', 'network inactivity timeout (ms)', 10000)
.option('--no-auth', 'skip authentication procedure')
Expand Down
3 changes: 2 additions & 1 deletion conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"opts": {
"netCheck": true,
"attemptAuth": true,
"autoOpenBrowser": true
"autoOpenBrowser": true,
"musicBrainz": false
},
"filters": [],
"dirs": {
Expand Down
Loading