A alembic/versions/9468e73ed926_add_musicbrainz_metadata.py => alembic/versions/9468e73ed926_add_musicbrainz_metadata.py +22 -0
@@ 0,0 1,22 @@
+"""Add MusicBrainz metadata
+
+Revision ID: 9468e73ed926
+Revises: 74eaf7f47b52
+Create Date: 2018-03-29 23:37:07.888458
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '9468e73ed926'
+down_revision = '74eaf7f47b52'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('songs', sa.Column('mb_metadata', sa.JSON(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('songs', 'mb_metadata')
M requirements.txt => requirements.txt +1 -0
@@ 1,3 1,4 @@
+pyacoustid
flask
flask-sockets
gevent
M stewdio-api.conf => stewdio-api.conf +5 -2
@@ 11,7 11,10 @@ host = localhost
port = 5432
[search]
-off-vocal-regex=[^\w](w/o|without)[^\w].*[\)\]-]$|(instrumental|off vocal|カラオケ)[・\s]*(カラオケ|バージョン)?(.*ver(sion|.)?)?\s*[->~~\]))]?[)\]]?\s*$
+off-vocal-regex = [^\w](w/o|without)[^\w].*[\)\]-]$|(instrumental|off vocal|カラオケ)[・\s]*(カラオケ|バージョン)?(.*ver(sion|.)?)?\s*[->~~\]))]?[)\]]?\s*$
[storage-status]
-test=/var/music/some.mp3
+test = /var/music/some.mp3
+
+[acoustid]
+api-key = nrUnHqXcsAE
M stewdio/config.py => stewdio/config.py +4 -0
@@ 42,3 42,7 @@ if off_vocal_regex:
cfg_storage_status: configparser.SectionProxy = config['storage-status']
storage_status = dict(cfg_storage_status)
+
+
+cfg_acoustid: configparser.SectionProxy = config['acoustid']
+acoustid_api_key = cfg_acoustid.get('api-key')
M stewdio/database.py => stewdio/database.py +5 -0
@@ 33,3 33,8 @@ class Database:
Base.query = session.query_property()
return session
+
+if __name__ == '__main__':
+ from .config import db
+ from .types import *
+ session = db.create_session()
M stewdio/library.py => stewdio/library.py +21 -2
@@ 5,6 5,8 @@ from functools import partial
from pathlib import Path
from typing import NamedTuple
+import acoustid
+from acoustid import WebServiceError
from tinytag import TinyTag
from stewdio.types.song import SongStatus
@@ 28,14 30,28 @@ def compute_hash(file):
return h.hexdigest()
+def augment_with_musicbrainz_metadata(song):
+ meta = 'recordings releases'
+ try:
+ data = acoustid.match(config.acoustid_api_key, song.path, meta=meta, parse=False)
+ if data.get('status') != 'ok':
+ raise WebServiceError("status not ok")
+ if not isinstance(data.get('results'), list):
+ raise WebServiceError("invalid results")
+ song.mb_metadata = data['results']
+ L.debug(f"Augmented {song} with MusicBrainz metadata")
+ except WebServiceError as e:
+ L.exception("Failed to fetch MusicBrainz metadata")
+
+
def update(session, scan_dir):
songs = []
scan_dir = Path(scan_dir)
L.info(f"Scanning directory {scan_dir} for new files")
for root, dirs, files in os.walk(scan_dir):
root = Path(root)
- for file in sorted(files):
- path = root / file
+ for path in sorted(files):
+ path = root / path
path = path.absolute()
if path.suffix not in ('.flac', '.mp3', '.aac', '.opus', '.ogg'):
continue
@@ 43,6 59,8 @@ def update(session, scan_dir):
hash = compute_hash(path)
song = session.query(Song).filter_by(hash=hash).one_or_none()
if song:
+ if not song.mb_metadata:
+ augment_with_musicbrainz_metadata(song)
L.info(f"Song {song} (path: {song.path}) already exists in database (new path: {path}), skipping")
continue
metadata = TinyTag.get(str(path))
@@ 65,6 83,7 @@ def update(session, scan_dir):
artist=artist,
album=album,
)
+ augment_with_musicbrainz_metadata(song)
session.add(song)
session.flush()
L.info(f"Added song {song}")
M stewdio/types/song.py => stewdio/types/song.py +2 -0
@@ 46,6 46,8 @@ class Song(Base):
collection_class=set,
back_populates="songs")
+ mb_metadata = sa.Column(sa.JSON)
+
def json(self):
return dict(
**{attr: getattr(self, attr)