Merge branch 'master' into discogs_original_year

This commit is contained in:
Dmitry Bogdanov 2018-05-02 17:41:07 +02:00 committed by GitHub
commit a840bc700b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 236 additions and 144 deletions

View file

@ -40,6 +40,7 @@ def apply_item_metadata(item, track_info):
item.artist_credit = track_info.artist_credit
item.title = track_info.title
item.mb_trackid = track_info.track_id
item.mb_releasetrackid = track_info.release_track_id
if track_info.artist_id:
item.mb_artistid = track_info.artist_id
if track_info.data_source:
@ -129,6 +130,7 @@ def apply_metadata(album_info, mapping):
# MusicBrainz IDs.
item.mb_trackid = track_info.track_id
item.mb_releasetrackid = track_info.release_track_id
item.mb_albumid = album_info.album_id
if track_info.artist_id:
item.mb_artistid = track_info.artist_id

View file

@ -129,6 +129,8 @@ class TrackInfo(object):
- ``title``: name of the track
- ``track_id``: MusicBrainz ID; UUID fragment only
- ``release_track_id``: MusicBrainz ID respective to a track on a
particular release; UUID fragment only
- ``artist``: individual track artist name
- ``artist_id``
- ``length``: float: duration of the track in seconds
@ -152,14 +154,15 @@ class TrackInfo(object):
may be None. The indices ``index``, ``medium``, and ``medium_index``
are all 1-based.
"""
def __init__(self, title, track_id, artist=None, artist_id=None,
length=None, index=None, medium=None, medium_index=None,
medium_total=None, artist_sort=None, disctitle=None,
artist_credit=None, data_source=None, data_url=None,
media=None, lyricist=None, composer=None, composer_sort=None,
arranger=None, track_alt=None):
def __init__(self, title, track_id, release_track_id=None, artist=None,
artist_id=None, length=None, index=None, medium=None,
medium_index=None, medium_total=None, artist_sort=None,
disctitle=None, artist_credit=None, data_source=None,
data_url=None, media=None, lyricist=None, composer=None,
composer_sort=None, arranger=None, track_alt=None):
self.title = title
self.track_id = track_id
self.release_track_id = release_track_id
self.artist = artist
self.artist_id = artist_id
self.length = length

View file

@ -281,6 +281,10 @@ def album_info(release):
continue
all_tracks = medium['track-list']
if 'data-track-list' in medium:
all_tracks += medium['data-track-list']
track_count = len(all_tracks)
if 'pregap' in medium:
all_tracks.insert(0, medium['pregap'])
@ -302,8 +306,9 @@ def album_info(release):
index,
int(medium['position']),
int(track['position']),
len(medium['track-list']),
track_count,
)
ti.release_track_id = track['id']
ti.disctitle = disctitle
ti.media = format
ti.track_alt = track['number']

View file

@ -455,6 +455,7 @@ class Item(LibModel):
'mb_albumid': types.STRING,
'mb_artistid': types.STRING,
'mb_albumartistid': types.STRING,
'mb_releasetrackid': types.STRING,
'albumtype': types.STRING,
'label': types.STRING,
'acoustid_fingerprint': types.STRING,

View file

@ -1865,6 +1865,12 @@ class MediaFile(object):
StorageStyle('MUSICBRAINZ_TRACKID'),
ASFStorageStyle('MusicBrainz/Track Id'),
)
mb_releasetrackid = MediaField(
MP3DescStorageStyle(u'MusicBrainz Release Track Id'),
MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Track Id'),
StorageStyle('MUSICBRAINZ_RELEASETRACKID'),
ASFStorageStyle('MusicBrainz/Release Track Id'),
)
mb_albumid = MediaField(
MP3DescStorageStyle(u'MusicBrainz Album Id'),
MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Id'),

View file

@ -237,7 +237,7 @@ def show_change(cur_artist, cur_album, match):
medium = track_info.disc
mediums = track_info.disctotal
if config['per_disc_numbering']:
if mediums > 1:
if mediums and mediums > 1:
return u'{0}-{1}'.format(medium, medium_index)
else:
return six.text_type(medium_index or index)

View file

@ -498,9 +498,10 @@ class DiscogsPlugin(BeetsPlugin):
medium, medium_index, _ = self.get_track_index(track['position'])
artist, artist_id = self.get_artist(track.get('artists', []))
length = self.get_track_length(track['duration'])
return TrackInfo(title, track_id, artist, artist_id, length, index,
medium, medium_index, artist_sort=None,
disctitle=None, artist_credit=None)
return TrackInfo(title, track_id, artist=artist, artist_id=artist_id,
length=length, index=index,
medium=medium, medium_index=medium_index,
artist_sort=None, disctitle=None, artist_credit=None)
def get_track_index(self, position):
"""Returns the medium, medium index and subtrack index for a discogs

View file

@ -48,7 +48,7 @@ class KeyFinderPlugin(BeetsPlugin):
self.find_key(lib.items(ui.decargs(args)), write=ui.should_write())
def imported(self, session, task):
self.find_key(task.items)
self.find_key(task.imported_items())
def find_key(self, items, write=False):
overwrite = self.config['overwrite'].get(bool)

View file

@ -29,6 +29,9 @@ New features:
and tracklist positions. Track ids are stored in ``mb_trackid``. :bug:`#2336`
Thanks to :user:`dbogdanov`.
* :doc:`/plugins/discogs`: Fetch original year from master releases. :bug:`#1122`
* As a first step to get :bug:`#406` implemented, beets now imports the
``musicbrainz_releasetrackid`` field into the library and tags media files
accordingly. Thanks to :user:`Rawrmonkeys`.
Fixes:
@ -104,6 +107,11 @@ Fixes:
to which a track belongs, not the total number of different mediums present
on the release. :bug:`2887`
Thanks to :user:`dbogdanov`.
* The importer now supports audio files contained in data tracks when they are
listed in MusicBrainz: the corresponding audio tracks are now merged into the
main track list. Thanks to :user:`jdetrey`. :bug:`1638`
* :doc:`/plugins/keyfinder`: Avoid a crash when trying to process unmatched
tracks. :bug:`2537`
For developers:

View file

@ -239,6 +239,7 @@ Audio information:
MusicBrainz and fingerprint information:
* mb_trackid
* mb_releasetrackid
* mb_albumid
* mb_artistid
* mb_albumartistid

View file

@ -2,28 +2,29 @@
# zsh completion for beets music library manager and MusicBrainz tagger: http://beets.radbox.org/
# NOTE: it will be very slow the first time you try to complete in a zsh shell (especially if you've enable many plugins)
# You can make it faster in future by creating a cached version:
# 1) perform a query completion with this file (_beet), e.g. do: beet list artist:"<TAB>
# to create the completion function (takes a few seconds)
# 2) save a copy of the completion function: which _beet > _beet_cached
# 3) save a copy of the query completion function: which _beet_query > _beet_query_cached
# 4) copy the contents of _beet_query_cached to the top of _beet_cached
# 5) copy and paste the _beet_field_values function from _beet to the top of _beet_cached
# 6) add the following line to the top of _beet_cached: #compdef beet
# 7) add the following line to the bottom of _beet_cached: _beet "$@"
# 8) save _beet_cached to your completions directory (e.g. /usr/share/zsh/functions/Completion)
# 9) add the following line to your .zshrc file: compdef _beet_cached beet
# You will need to repeat this proceedure each time you enable new plugins if you want them to complete properly.
# Cache will be updated if it is older than the beets database or binary.
# Need to set BEETS_LIBRARY to some preliminary value since it is used by the cache checking function.
typeset -g BEETS_LIBRARY=~/.config/beets/library.db
zstyle ":completion:${curcontext}:" cache-policy _beet_check_cache
_beet_check_cache () {
[[ ! -a "${1}" ]] || [[ ! -a ${~BEETS_LIBRARY} ]] || [[ "${1}" -ot ${~BEETS_LIBRARY} ]] || [[ "${1}" -ot =beet ]]
}
# Try to retrieve the cache, and find out if it needs to be updated
if ! _retrieve_cache beets || _cache_invalid beets; then
local updatecache=1
# Location of database
typeset -g BEETS_LIBRARY="$(beet config|grep library|cut -f 2 -d ' ')"
# List of all fields
local -a fields
fields=(`beet fields | grep -G '^ ' | sort -u | colrm 1 2`)
fi
# useful: argument to _regex_arguments for matching any word
local matchany=/$'[^\0]##\0'/
# Deal with completions for querying and modifying fields..
local fieldargs matchquery matchmodify
local -a fields
# get list of all fields
fields=(`beet fields | grep -G '^ ' | sort -u | colrm 1 2`)
# regexps for matching query and modify terms on the command line
matchquery=/"(${(j/|/)fields[@]})"$':[^\0]##\0'/
matchmodify=/"(${(j/|/)fields[@]})"$'(=[^\0]##|!)\0'/
@ -43,14 +44,17 @@ function _join_lines() {
function _beet_field_values()
{
local -a output fieldvals
local library="$(beet config|grep library|cut -f 2 -d ' ')"
output=$(sqlite3 ${~library} "select distinct $1 from items;")
local sqlcmd="select distinct $1 from items;"
case $1
in
lyrics)
fieldvals=
;;
*)
if [[ "$(sqlite3 ${~BEETS_LIBRARY} ${sqlcmd} 2>&1)" =~ "no such column" ]]; then
sqlcmd="select distinct value from item_attributes where key=='$1' and value!='';"
fi
output="$(sqlite3 ${~BEETS_LIBRARY} ${sqlcmd} 2>/dev/null | sed -rn '/^-+$/,${{/^[- ]+$/n};p}')"
fieldvals=("${(f)output[@]}")
;;
esac
@ -68,8 +72,7 @@ queryelem="_values -S : 'query field (add an extra : to match by regexp)' '::' $
# store call to _values function for completing modify terms (no need to complete field values)
modifyelem="_values -S = 'modify field (replace = with ! to remove field)' $(echo "'${^fields[@]}:: '")"
# Create completion function for queries
_regex_arguments _beet_query "$matchany" \# \( "$matchquery" ":query:query string:$queryelem" \) \
\( "$matchquery" ":query:query string:$queryelem" \) \#
_regex_arguments _beet_query "$matchany" \# \( "$matchquery" ":query:query string:$queryelem" \) \( "$matchquery" ":query:query string:$queryelem" \) \#
# store regexps for completing lists of queries and modifications
local -a query modify
query=( \( "$matchquery" ":query:query string:{_beet_query}" \) \( "$matchquery" ":query:query string:{_beet_query}" \) \# )
@ -174,6 +177,7 @@ function _beet_subcmd_options()
}
# Now build the arguments to _regex_arguments for each subcommand.
if [[ -n $updatecache ]]; then
local -a options regex_words_subcmds regex_words_help
local subcmd cmddesc
for i in ${${(f)"$(beet help | _join_lines ' ' 3 'Commands:')"[@]}[@]}
@ -241,6 +245,8 @@ do
# Add to regex_words args for help subcommand
regex_words_help+=("$subcmd:$cmddesc")
done
_store_cache beets regex_words_subcmds regex_words_help BEETS_LIBRARY fields
fi
local -a opts_for_help
_regex_words subcmds "subcommands" "${regex_words_help[@]}"
@ -253,7 +259,7 @@ _regex_words options "global options" "$configopt" "$debugopt" "$libopt" "$helpo
globalopts=("${reply[@]}")
# Create main completion function
#local -a subcmds
local -a subcmds
_regex_words subcmds "subcommands" "${regex_words_subcmds[@]}"
subcmds=("${reply[@]}")
_regex_arguments _beet "$matchany" \( "${globalopts[@]}" \# \) "${subcmds[@]}"
@ -267,3 +273,4 @@ _beet "$@"
# Local Variables:
# mode:shell-script
# End:

View file

@ -89,6 +89,7 @@ def item(lib=None):
mb_albumid='someID-2',
mb_artistid='someID-3',
mb_albumartistid='someID-4',
mb_releasetrackid='someID-5',
album_id=None,
mtime=12345,
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -103,9 +103,9 @@ def _make_item(title, track, artist=u'some artist'):
def _make_trackinfo():
return [
TrackInfo(u'one', None, u'some artist', length=1, index=1),
TrackInfo(u'two', None, u'some artist', length=1, index=2),
TrackInfo(u'three', None, u'some artist', length=1, index=3),
TrackInfo(u'one', None, artist=u'some artist', length=1, index=1),
TrackInfo(u'two', None, artist=u'some artist', length=1, index=2),
TrackInfo(u'three', None, artist=u'some artist', length=1, index=3),
]
@ -827,15 +827,15 @@ class ApplyCompilationTest(_common.TestCase, ApplyTestUtil):
trackinfo.append(TrackInfo(
u'oneNew',
u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c',
u'artistOneNew',
u'a05686fc-9db2-4c23-b99e-77f5db3e5282',
artist=u'artistOneNew',
artist_id=u'a05686fc-9db2-4c23-b99e-77f5db3e5282',
index=1,
))
trackinfo.append(TrackInfo(
u'twoNew',
u'40130ed1-a27c-42fd-a328-1ebefb6caef4',
u'artistTwoNew',
u'80b3cf5e-18fe-4c59-98c7-e5bb87210710',
artist=u'artistTwoNew',
artist_id=u'80b3cf5e-18fe-4c59-98c7-e5bb87210710',
index=2,
))
self.info = AlbumInfo(

View file

@ -1819,6 +1819,7 @@ def mocked_get_release_by_id(id_, includes=[], release_status=[],
'id': id_,
'medium-list': [{
'track-list': [{
'id': 'baz',
'recording': {
'title': 'foo',
'id': 'bar',

View file

@ -27,7 +27,8 @@ import mock
class MBAlbumInfoTest(_common.TestCase):
def _make_release(self, date_str='2009', tracks=None, track_length=None,
track_artist=False, medium_format='FORMAT'):
track_artist=False, data_tracks=None,
medium_format='FORMAT'):
release = {
'title': 'ALBUM TITLE',
'id': 'ALBUM ID',
@ -62,12 +63,15 @@ class MBAlbumInfoTest(_common.TestCase):
'country': 'COUNTRY',
'status': 'STATUS',
}
if tracks:
i = 0
track_list = []
for i, recording in enumerate(tracks):
if tracks:
for recording in tracks:
i += 1
track = {
'id': 'RELEASE TRACK ID %d' % i,
'recording': recording,
'position': i + 1,
'position': i,
'number': 'A1',
}
if track_length:
@ -87,9 +91,21 @@ class MBAlbumInfoTest(_common.TestCase):
}
]
track_list.append(track)
data_track_list = []
if data_tracks:
for recording in data_tracks:
i += 1
data_track = {
'id': 'RELEASE TRACK ID %d' % i,
'recording': recording,
'position': i,
'number': 'A1',
}
data_track_list.append(data_track)
release['medium-list'].append({
'position': '1',
'track-list': track_list,
'data-track-list': data_track_list,
'format': medium_format,
'title': 'MEDIUM TITLE',
})
@ -183,6 +199,7 @@ class MBAlbumInfoTest(_common.TestCase):
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=[tracks[0]])
second_track_list = [{
'id': 'RELEASE TRACK ID 2',
'recording': tracks[1],
'position': '1',
'number': 'A1',
@ -354,6 +371,18 @@ class MBAlbumInfoTest(_common.TestCase):
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
def test_no_skip_audio_data_tracks(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
data_tracks = [self._make_track('TITLE AUDIO DATA', 'ID DATA TRACK',
100.0 * 1000.0)]
release = self._make_release(tracks=tracks, data_tracks=data_tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 3)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
self.assertEqual(d.tracks[2].title, 'TITLE AUDIO DATA')
def test_skip_video_tracks_by_default(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE VIDEO', 'ID VIDEO', 100.0 * 1000.0,
@ -365,6 +394,17 @@ class MBAlbumInfoTest(_common.TestCase):
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
def test_skip_video_data_tracks_by_default(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
data_tracks = [self._make_track('TITLE VIDEO', 'ID VIDEO',
100.0 * 1000.0, False, True)]
release = self._make_release(tracks=tracks, data_tracks=data_tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 2)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
def test_no_skip_video_tracks_if_configured(self):
config['match']['ignore_video_tracks'] = False
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
@ -378,6 +418,19 @@ class MBAlbumInfoTest(_common.TestCase):
self.assertEqual(d.tracks[1].title, 'TITLE VIDEO')
self.assertEqual(d.tracks[2].title, 'TITLE TWO')
def test_no_skip_video_data_tracks_if_configured(self):
config['match']['ignore_video_tracks'] = False
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
data_tracks = [self._make_track('TITLE VIDEO', 'ID VIDEO',
100.0 * 1000.0, False, True)]
release = self._make_release(tracks=tracks, data_tracks=data_tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 3)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
self.assertEqual(d.tracks[2].title, 'TITLE VIDEO')
class ParseIDTest(_common.TestCase):
def test_parse_id_correct(self):
@ -504,6 +557,7 @@ class MBLibraryTest(unittest.TestCase):
'id': mbid,
'medium-list': [{
'track-list': [{
'id': 'baz',
'recording': {
'title': 'foo',
'id': 'bar',

View file

@ -337,6 +337,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
'bpm': 6,
'comp': True,
'mb_trackid': '8b882575-08a5-4452-a7a7-cbb8a1531f9e',
'mb_releasetrackid': 'c29f3a57-b439-46fd-a2e2-93776b1371e0',
'mb_albumid': '9e873859-8aa4-4790-b985-5a953e8ef628',
'mb_artistid': '7cf0ea9d-86b9-4dad-ba9e-2355a64899ea',
'art': None,
@ -366,6 +367,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
'bpm',
'comp',
'mb_trackid',
'mb_releasetrackid',
'mb_albumid',
'mb_artistid',
'art',
@ -773,7 +775,7 @@ class MusepackTest(ReadWriteTestBase, unittest.TestCase):
extension = 'mpc'
audio_properties = {
'length': 1.0,
'bitrate': 23458,
'bitrate': 24023,
'format': u'Musepack',
'samplerate': 44100,
'bitdepth': 0,
@ -871,7 +873,7 @@ class ApeTest(ReadWriteTestBase, ExtendedImageStructureTestMixin,
extension = 'ape'
audio_properties = {
'length': 1.0,
'bitrate': 112040,
'bitrate': 112608,
'format': u'APE',
'samplerate': 44100,
'bitdepth': 16,
@ -883,7 +885,7 @@ class WavpackTest(ReadWriteTestBase, unittest.TestCase):
extension = 'wv'
audio_properties = {
'length': 1.0,
'bitrate': 108744,
'bitrate': 109312,
'format': u'WavPack',
'samplerate': 44100,
'bitdepth': 0,
@ -895,7 +897,7 @@ class OpusTest(ReadWriteTestBase, unittest.TestCase):
extension = 'opus'
audio_properties = {
'length': 1.0,
'bitrate': 57984,
'bitrate': 66792,
'format': u'Opus',
'samplerate': 48000,
'bitdepth': 0,