Node.jsでID3タグ編集を極力省力化する〜GUIに別れを告げて〜

公開日:2019-03-04
最終更新:2019-03-04

はじめに

入院、それは暇との戦い…(私の場合)。
「さて、音楽でも聴くかぁ」と思ってもPCは持ち込み禁止なので、いつものようにmpv ~/Music/ジャンル/アーティスト/アルバム/トラックナンバー-曲名.mp3することはできません。
渋々、WALKMANを使ってみたものの、『ディレクトリ配下を再生』というような指定はできないようで、表示されるのは曲名不明アーティスト不明アルバム不明のファイル名を列挙したもののみ。

ほげぇぇぇ!!!


さて、辛い入院も終わり退院してきました。
「次回の入院に備えてid3タグ編集するかぁ」と思っても、iTunesは間違いだらけのデータを勝手に設定しやがり、すべて自力で設定しようとしてもUIは使いづらく。

ほげぇぇぇ!!!

次の入院時にも、あの苦行を繰り返すはめになりました。


さて、辛い入院も終わり退院してきました(2回目)。
やり直すんだ。そして次はうまくやる。

概要

長い茶番、すみません。

今回作ったのは、

  1. CDから音声ファイルを読み込み
  2. mp3に変換し
  3. あらかじめ定義しておいたデータをid3タグとして書き込み
  4. ファイル名を曲名に変更

するプログラムです。

環境

  • macOS Mojave 10.14.3
  • Node.js - v11.10.0
  • npm - 6.7.0
  • node-id3 - 0.1.7

PythonよりもJavaScriptの方が得意なので、Node.jsを用いました。

node-id3というID3タグ編集ができるライブラリをnpmでインストールして使いました。

npm install node-id3

詳しくは: Zazama/node-id3: Pure JavaScript ID3 Tag library
もしくは: node-id3 - npm

全体像

ディレクトリ構造

.  
├── audio  
├── master.js  
└── metadata.tsv

master.jsを実行し、metadata.tsvを読み込み、audioディレクトリ内に出力します。

metadata.tsv

私の愛するTSV形式です。

アルバム名  

曲名    アーティスト名  
曲名    アーティスト名  
曲名    アーティスト名

1行目にアルバム名を書き、それ以降にそれぞれの曲のデータを書きます。
曲名\tアーティスト名です。

Node.js

#!/usr/bin/env node  

'use strict';  

// config  
const originalExt = '.aiff';  
const storeDirPath = './audio';  
const metaDataFilePath = './metadata.tsv';  

// require  
const fs = require('fs');  
const process = require('process');  
const childProcess = require('child_process');  
const NodeID3 = require('node-id3');  

// 相対パス用  
process.chdir(__dirname);  

// 関数の定義  
const  
    getCDPath = () => {  
        const parent = '/Volumes';  
        const CDDir = childProcess.spawnSync(`ls ${parent} | peco`, [], { shell: true }).stdout.toString().trim();  

        return `${parent}/${CDDir}`;  
    },  
    ls = (path) => {  
        path = path || './';  
        return fs.readdirSync(path).map(v => `${path}/${v}`);  
    },  
    getFileNum = (filePath) => {  
        const fileName = filePath.split('/');  
        const numStr = fileName[fileName.length - 1].match(/\d+/)[0];  
        return Number(numStr);  
    },  
    ffmpeg = (filesPath, targetDirPath) => {  
        return filesPath  
            .map((filePath, idx) => {  
                const targetPath = `${targetDirPath}/audio${idx + 1}.mp3`;  

                console.log(`${filePath} -> ${targetPath}`);  

                childProcess.execSync(`ffmpeg -i "${filePath}" "${targetPath}" > /dev/null 2> /dev/null`);  
                return targetPath;  
            })  
            ;  
    },  
    getMetaData = (path) => {  
        const lines = fs.readFileSync(path, { encoding: 'utf-8' })  
            .trim()  
            .split('\n')  
            .map(line => line.trim())  
            .filter(line => line)  
            ;  

        const albumName = lines.shift();  
        return lines  
            .map(entry => entry.trim().split('\t').map(v => v.trim()).filter(v => v))  
            .map((entry, i) => {  
                return {  
                    album: albumName,  
                    trackNumber: i + 1,  
                    title: entry[0],  
                    artist: entry[1],  
                };  
            })  
            ;  
    },  
    editTag = (filePath, tags) => {  
        NodeID3.removeTags(filePath);  
        NodeID3.write(tags, filePath);  
    },  
    getFileDirPath = (filePath) => {  
        const fileDirPath = filePath.split('/')  
        fileDirPath.pop();  
        return fileDirPath.join('/');  
    },  
    rename = (filePath, title, idx) => {  
        const fileDirPath = getFileDirPath(filePath);  

        const idxNumForHuman = idx + 1;  
        const idxStrForHuman = idxNumForHuman > 9 ? idxNumForHuman.toString() : `0${idxNumForHuman}`;  

        fs.renameSync(filePath, `${fileDirPath}/${idxStrForHuman}-${title}.mp3`);  
    }  
    ;  

// CDのパスの取得  
const CDPath = getCDPath();  
// CD内を`ls`し  
const originalFilesPath = ls(CDPath)  
    // あらかじめ定義していた拡張子のファイルのみにフィルタリング  
    .filter(v => v.endsWith(originalExt))  
    // ファイル名に含まれる数字を用いてソート  
    .sort((a, b) => getFileNum(a) > getFileNum(b) ? 1 : -1)  
    ;  
// ffmpegを呼び出し、変換しながら読み込み  
const copiedFilesPath = ffmpeg(originalFilesPath, storeDirPath);  
// メタデータファイルを読み込み  
// // 一行目: アルバム名  
// // それ以降: タイトル\tアーティスト  
const metaData = getMetaData(metaDataFilePath);  

copiedFilesPath.forEach((filePath, i) => {  
    console.log(`${filePath} << ${metaData[i].title}`);  

    // メタデータの書き込み  
    editTag(filePath, metaData[i]);  
    // メタデータをもとにファイルをリネーム  
    rename(filePath, metaData[i].title, i);  
});

説明

長いですが単純です。

  1. CDへのパスを取得し
  2. CDの中身を見て
  3. ffmpegを呼び出し、変換&読み込み
  4. metadata.tsvを読み込み
  5. ファイルのID3タグを初期化し
  6. ファイルにID3タグを書き込み
  7. ファイルをリネームし

ているだけです。

CDへのパスを取得

面倒なのでシェルスクリプトを呼び出します。
lsをpecoにパイプしているだけです。
(peco、かわいい名前!)

CDの中身を見る

readdirSync()です。
非同期処理はよくわからないのでやりません。

拡張子がaiffのもののみにfilter()し、sort()します。

ffmpegを呼び出し、変換&読み込み

Node.jsだけでも変換できるのかもしれませんが、ここはffmpegに頼ってシェルスクリプトを呼び出します。
音質にはこだわりません(沼るので)。
ffmpeg -i "${filePath}" "${targetPath}"で、出力を/dev/nullへ捨てています。

metadata.tsvの読み込み

特記事項なしです。
普通にreadFileSync()split('\n')split('\t')をして、オブジェクトの配列にしています。

ファイルのID3タグの初期化

NodeID3.removeTags(filePath)だけ。
便利。

ファイルへのID3タグの書き込み

先程のオブジェクトとファイルのパスをNodeID3.write()の引数に渡すだけです。
楽です。node-id3すごい!

ファイルのリネーム

トラックナンバー生成でもたついていますが(ForHuman)、renameSyncしています。

おわりに

いろいろ雑な感じ(エラー処理をしない)ですが、まぁよしとします。

さて、あとはTSVを書くだけですね。
何百曲分になることやら。

記事が少しでもいいなと思ったらクラップを送ってみよう!
0
+1
@okayuの大して技術的ではないブログ

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする