mirror of
https://github.com/beetbox/beets.git
synced 2026-02-08 08:25:23 +01:00
spotify, deezer: Store IDs in dedicated fields instead of MusicBrainz fields
This commit is contained in:
parent
cdfb813910
commit
81cd3da1f3
6 changed files with 265 additions and 19 deletions
|
|
@ -20,6 +20,7 @@ import collections
|
|||
import time
|
||||
from typing import TYPE_CHECKING, ClassVar, Literal
|
||||
|
||||
import mediafile
|
||||
import requests
|
||||
|
||||
from beets import ui
|
||||
|
|
@ -40,6 +41,8 @@ class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]):
|
|||
item_types: ClassVar[dict[str, types.Type]] = {
|
||||
"deezer_track_rank": types.INTEGER,
|
||||
"deezer_track_id": types.INTEGER,
|
||||
"deezer_album_id": types.INTEGER,
|
||||
"deezer_artist_id": types.INTEGER,
|
||||
"deezer_updated": types.DATE,
|
||||
}
|
||||
# Base URLs for the Deezer API
|
||||
|
|
@ -51,6 +54,20 @@ class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]):
|
|||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
# Register Deezer ID fields as media fields
|
||||
for field in ["deezer_track_id", "deezer_album_id", "deezer_artist_id"]:
|
||||
media_field = mediafile.MediaField(
|
||||
mediafile.MP3DescStorageStyle(field),
|
||||
mediafile.MP4StorageStyle(f"----:com.apple.iTunes:{field}"),
|
||||
mediafile.StorageStyle(field),
|
||||
mediafile.ASFStorageStyle(field),
|
||||
)
|
||||
try:
|
||||
self.add_media_field(field, media_field)
|
||||
except ValueError:
|
||||
# Ignore errors due to duplicate registration
|
||||
pass
|
||||
|
||||
def commands(self):
|
||||
"""Add beet UI commands to interact with Deezer."""
|
||||
deezer_update_cmd = ui.Subcommand(
|
||||
|
|
@ -127,11 +144,10 @@ class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]):
|
|||
|
||||
return AlbumInfo(
|
||||
album=album_data["title"],
|
||||
album_id=deezer_id,
|
||||
deezer_album_id=deezer_id,
|
||||
artist=artist,
|
||||
artist_credit=self.get_artist([album_data["artist"]])[0],
|
||||
artist_id=artist_id,
|
||||
deezer_artist_id=artist_id,
|
||||
tracks=tracks,
|
||||
albumtype=album_data["record_type"],
|
||||
va=(
|
||||
|
|
@ -202,11 +218,10 @@ class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]):
|
|||
)
|
||||
return TrackInfo(
|
||||
title=track_data["title"],
|
||||
track_id=track_data["id"],
|
||||
deezer_track_id=track_data["id"],
|
||||
isrc=track_data.get("isrc"),
|
||||
artist=artist,
|
||||
artist_id=artist_id,
|
||||
deezer_artist_id=artist_id,
|
||||
length=track_data["duration"],
|
||||
index=track_data.get("track_position"),
|
||||
medium=track_data.get("disk_number"),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import webbrowser
|
|||
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
||||
|
||||
import confuse
|
||||
import mediafile
|
||||
import requests
|
||||
|
||||
from beets import ui
|
||||
|
|
@ -152,6 +153,25 @@ class SpotifyPlugin(
|
|||
self._audio_features_lock = (
|
||||
threading.Lock()
|
||||
) # Protects audio_features_available
|
||||
|
||||
# Register Spotify ID fields as media fields
|
||||
for field in [
|
||||
"spotify_track_id",
|
||||
"spotify_album_id",
|
||||
"spotify_artist_id",
|
||||
]:
|
||||
media_field = mediafile.MediaField(
|
||||
mediafile.MP3DescStorageStyle(field),
|
||||
mediafile.MP4StorageStyle(f"----:com.apple.iTunes:{field}"),
|
||||
mediafile.StorageStyle(field),
|
||||
mediafile.ASFStorageStyle(field),
|
||||
)
|
||||
try:
|
||||
self.add_media_field(field, media_field)
|
||||
except ValueError:
|
||||
# Ignore errors due to duplicate registration
|
||||
pass
|
||||
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
|
|
@ -373,13 +393,10 @@ class SpotifyPlugin(
|
|||
|
||||
return AlbumInfo(
|
||||
album=album_data["name"],
|
||||
album_id=spotify_id,
|
||||
spotify_album_id=spotify_id,
|
||||
artist=artist,
|
||||
artist_id=artists_ids[0] if len(artists_ids) > 0 else None,
|
||||
spotify_artist_id=artists_ids[0] if len(artists_ids) > 0 else None,
|
||||
artists=artists_names,
|
||||
artists_ids=artists_ids,
|
||||
tracks=tracks,
|
||||
albumtype=album_data["album_type"],
|
||||
va=len(album_data["artists"]) == 1
|
||||
|
|
@ -414,14 +431,11 @@ class SpotifyPlugin(
|
|||
album = None
|
||||
return TrackInfo(
|
||||
title=track_data["name"],
|
||||
track_id=track_data["id"],
|
||||
spotify_track_id=track_data["id"],
|
||||
artist=artist,
|
||||
album=album,
|
||||
artist_id=artists_ids[0] if len(artists_ids) > 0 else None,
|
||||
spotify_artist_id=artists_ids[0] if len(artists_ids) > 0 else None,
|
||||
artists=artists_names,
|
||||
artists_ids=artists_ids,
|
||||
length=track_data["duration_ms"] / 1000,
|
||||
index=track_data["track_number"],
|
||||
medium=track_data["disc_number"],
|
||||
|
|
|
|||
|
|
@ -57,3 +57,13 @@ The ``deezer`` plugin provides an additional command ``deezerupdate`` to update
|
|||
the ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a
|
||||
global indicator of a song's popularity on Deezer that is updated daily based on
|
||||
streams. The higher the ``rank``, the more popular the track is.
|
||||
|
||||
Stored Fields
|
||||
-------------
|
||||
|
||||
When Deezer is used as a metadata source during import, the plugin stores these
|
||||
identifiers:
|
||||
|
||||
- ``deezer_track_id``
|
||||
- ``deezer_album_id``
|
||||
- ``deezer_artist_id``
|
||||
|
|
|
|||
|
|
@ -181,3 +181,13 @@ these track attributes from Spotify:
|
|||
- ``tempo``
|
||||
- ``time_signature``
|
||||
- ``valence``
|
||||
|
||||
Stored Fields
|
||||
-------------
|
||||
|
||||
When Spotify is used as a metadata source during import, the plugin stores these
|
||||
identifiers:
|
||||
|
||||
- ``spotify_track_id``
|
||||
- ``spotify_album_id``
|
||||
- ``spotify_artist_id``
|
||||
|
|
|
|||
97
test/plugins/test_deezer.py
Normal file
97
test/plugins/test_deezer.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""Tests for the 'deezer' plugin"""
|
||||
|
||||
from mediafile import MediaFile
|
||||
|
||||
from beets.library import Item
|
||||
from beets.test.helper import PluginTestCase
|
||||
from beets.util import syspath
|
||||
|
||||
|
||||
class DeezerMediaFieldTest(PluginTestCase):
|
||||
"""Test that Deezer IDs are written to and read from media files."""
|
||||
|
||||
plugin = "deezer"
|
||||
|
||||
def test_deezer_track_id_written_to_file(self):
|
||||
"""Verify deezer_track_id is written to media files."""
|
||||
item = self.add_item_fixture()
|
||||
item.deezer_track_id = 123456789
|
||||
item.write()
|
||||
|
||||
# Read back from file (media files store as strings)
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.deezer_track_id == "123456789"
|
||||
|
||||
def test_deezer_album_id_written_to_file(self):
|
||||
"""Verify deezer_album_id is written to media files."""
|
||||
item = self.add_item_fixture()
|
||||
item.deezer_album_id = 987654321
|
||||
item.write()
|
||||
|
||||
# Read back from file (media files store as strings)
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.deezer_album_id == "987654321"
|
||||
|
||||
def test_deezer_artist_id_written_to_file(self):
|
||||
"""Verify deezer_artist_id is written to media files."""
|
||||
item = self.add_item_fixture()
|
||||
item.deezer_artist_id = 111222333
|
||||
item.write()
|
||||
|
||||
# Read back from file (media files store as strings)
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.deezer_artist_id == "111222333"
|
||||
|
||||
def test_deezer_ids_read_from_file(self):
|
||||
"""Verify Deezer IDs can be read from file into Item."""
|
||||
item = self.add_item_fixture()
|
||||
mf = MediaFile(syspath(item.path))
|
||||
mf.deezer_track_id = "123456"
|
||||
mf.deezer_album_id = "654321"
|
||||
mf.deezer_artist_id = "999888"
|
||||
mf.save()
|
||||
|
||||
# Read back into Item (beets converts to int from item_types)
|
||||
item_reloaded = Item.from_path(item.path)
|
||||
assert item_reloaded.deezer_track_id == 123456
|
||||
assert item_reloaded.deezer_album_id == 654321
|
||||
assert item_reloaded.deezer_artist_id == 999888
|
||||
|
||||
def test_deezer_ids_persist_across_writes(self):
|
||||
"""Verify IDs are not lost when updating other fields."""
|
||||
item = self.add_item_fixture()
|
||||
item.deezer_track_id = 123456
|
||||
item.deezer_album_id = 654321
|
||||
item.write()
|
||||
|
||||
# Update different field
|
||||
item.title = "New Title"
|
||||
item.write()
|
||||
|
||||
# Verify IDs still in file (media files store as strings)
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.deezer_track_id == "123456"
|
||||
assert mf.deezer_album_id == "654321"
|
||||
|
||||
def test_deezer_ids_not_in_musicbrainz_fields(self):
|
||||
"""Verify Deezer IDs don't pollute MusicBrainz fields."""
|
||||
item = self.add_item_fixture()
|
||||
# Set Deezer IDs
|
||||
item.deezer_track_id = 123456789
|
||||
item.deezer_album_id = 987654321
|
||||
item.deezer_artist_id = 111222333
|
||||
item.write()
|
||||
|
||||
# Read back and verify MusicBrainz fields are NOT set to Deezer IDs
|
||||
mf = MediaFile(syspath(item.path))
|
||||
|
||||
# MusicBrainz fields should be None or empty, not Deezer IDs
|
||||
assert mf.mb_trackid != "123456789"
|
||||
assert mf.mb_albumid != "987654321"
|
||||
assert mf.mb_artistid != "111222333"
|
||||
assert mf.mb_albumartistid != "111222333"
|
||||
|
||||
# Deezer IDs should be in their own fields
|
||||
assert mf.deezer_track_id == "123456789"
|
||||
assert mf.deezer_album_id == "987654321"
|
||||
assert mf.deezer_artist_id == "111222333"
|
||||
|
|
@ -4,10 +4,12 @@ import os
|
|||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import responses
|
||||
from mediafile import MediaFile
|
||||
|
||||
from beets.library import Item
|
||||
from beets.test import _common
|
||||
from beets.test.helper import PluginTestCase
|
||||
from beets.util import syspath
|
||||
from beetsplug import spotify
|
||||
|
||||
|
||||
|
|
@ -289,21 +291,119 @@ class SpotifyPluginTest(PluginTestCase):
|
|||
assert album_info is not None
|
||||
assert album_info.artist == "Project Skylate, Sugar Shrill"
|
||||
assert album_info.artists == ["Project Skylate", "Sugar Shrill"]
|
||||
assert album_info.artist_id == "6m8MRXIVKb6wQaPlBIDMr1"
|
||||
assert album_info.artists_ids == [
|
||||
"6m8MRXIVKb6wQaPlBIDMr1",
|
||||
"4kkAIoQmNT5xEoNH5BuQLe",
|
||||
]
|
||||
assert album_info.spotify_artist_id == "6m8MRXIVKb6wQaPlBIDMr1"
|
||||
|
||||
assert len(album_info.tracks) == 1
|
||||
assert album_info.tracks[0].artist == "Foo, Bar"
|
||||
assert album_info.tracks[0].artists == ["Foo", "Bar"]
|
||||
assert album_info.tracks[0].artist_id == "12345"
|
||||
assert album_info.tracks[0].artists_ids == ["12345", "67890"]
|
||||
assert album_info.tracks[0].spotify_artist_id == "12345"
|
||||
|
||||
track_info = self.spotify.track_for_id("6sjZfVJworBX6TqyjkxIJ1")
|
||||
assert track_info is not None
|
||||
assert track_info.artist == "Foo, Bar"
|
||||
assert track_info.artists == ["Foo", "Bar"]
|
||||
assert track_info.artist_id == "12345"
|
||||
assert track_info.artists_ids == ["12345", "67890"]
|
||||
assert track_info.spotify_artist_id == "12345"
|
||||
|
||||
|
||||
class SpotifyMediaFieldTest(PluginTestCase):
|
||||
"""Test that Spotify IDs are written to and read from media files."""
|
||||
|
||||
plugin = "spotify"
|
||||
|
||||
@responses.activate
|
||||
def setUp(self):
|
||||
responses.add(
|
||||
responses.POST,
|
||||
spotify.SpotifyPlugin.oauth_token_url,
|
||||
status=200,
|
||||
json={
|
||||
"access_token": "test_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
},
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
def test_spotify_track_id_written_to_file(self):
|
||||
"""Verify spotify_track_id is written to media files."""
|
||||
item = self.add_item_fixture()
|
||||
item.spotify_track_id = "6NPVjNh8Jhru9xOmyQigds"
|
||||
item.write()
|
||||
|
||||
# Read back from file
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.spotify_track_id == "6NPVjNh8Jhru9xOmyQigds"
|
||||
|
||||
def test_spotify_album_id_written_to_file(self):
|
||||
"""Verify spotify_album_id is written to media files."""
|
||||
item = self.add_item_fixture()
|
||||
item.spotify_album_id = "5l3zEmMrOhOzG8d8s83GOL"
|
||||
item.write()
|
||||
|
||||
# Read back from file
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.spotify_album_id == "5l3zEmMrOhOzG8d8s83GOL"
|
||||
|
||||
def test_spotify_artist_id_written_to_file(self):
|
||||
"""Verify spotify_artist_id is written to media files."""
|
||||
item = self.add_item_fixture()
|
||||
item.spotify_artist_id = "3OWO2LOPTl1u6XvJHkwHmd"
|
||||
item.write()
|
||||
|
||||
# Read back from file
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.spotify_artist_id == "3OWO2LOPTl1u6XvJHkwHmd"
|
||||
|
||||
def test_spotify_ids_read_from_file(self):
|
||||
"""Verify Spotify IDs can be read from file into Item."""
|
||||
item = self.add_item_fixture()
|
||||
mf = MediaFile(syspath(item.path))
|
||||
mf.spotify_track_id = "track123"
|
||||
mf.spotify_album_id = "album456"
|
||||
mf.spotify_artist_id = "artist789"
|
||||
mf.save()
|
||||
|
||||
# Read back into Item
|
||||
item_reloaded = Item.from_path(item.path)
|
||||
assert item_reloaded.spotify_track_id == "track123"
|
||||
assert item_reloaded.spotify_album_id == "album456"
|
||||
assert item_reloaded.spotify_artist_id == "artist789"
|
||||
|
||||
def test_spotify_ids_persist_across_writes(self):
|
||||
"""Verify IDs are not lost when updating other fields."""
|
||||
item = self.add_item_fixture()
|
||||
item.spotify_track_id = "track123"
|
||||
item.spotify_album_id = "album456"
|
||||
item.write()
|
||||
|
||||
# Update different field
|
||||
item.title = "New Title"
|
||||
item.write()
|
||||
|
||||
# Verify IDs still in file
|
||||
mf = MediaFile(syspath(item.path))
|
||||
assert mf.spotify_track_id == "track123"
|
||||
assert mf.spotify_album_id == "album456"
|
||||
|
||||
def test_spotify_ids_not_in_musicbrainz_fields(self):
|
||||
"""Verify Spotify IDs don't pollute MusicBrainz fields."""
|
||||
item = self.add_item_fixture()
|
||||
# Set Spotify IDs
|
||||
item.spotify_track_id = "6NPVjNh8Jhru9xOmyQigds"
|
||||
item.spotify_album_id = "5l3zEmMrOhOzG8d8s83GOL"
|
||||
item.spotify_artist_id = "3OWO2LOPTl1u6XvJHkwHmd"
|
||||
item.write()
|
||||
|
||||
# Read back and verify MusicBrainz fields are NOT set to Spotify IDs
|
||||
mf = MediaFile(syspath(item.path))
|
||||
|
||||
# MusicBrainz fields should be None or empty, not Spotify IDs
|
||||
assert mf.mb_trackid != "6NPVjNh8Jhru9xOmyQigds"
|
||||
assert mf.mb_albumid != "5l3zEmMrOhOzG8d8s83GOL"
|
||||
assert mf.mb_artistid != "3OWO2LOPTl1u6XvJHkwHmd"
|
||||
assert mf.mb_albumartistid != "3OWO2LOPTl1u6XvJHkwHmd"
|
||||
|
||||
# Spotify IDs should be in their own fields
|
||||
assert mf.spotify_track_id == "6NPVjNh8Jhru9xOmyQigds"
|
||||
assert mf.spotify_album_id == "5l3zEmMrOhOzG8d8s83GOL"
|
||||
assert mf.spotify_artist_id == "3OWO2LOPTl1u6XvJHkwHmd"
|
||||
|
|
|
|||
Loading…
Reference in a new issue