mirror of
https://github.com/beetbox/beets.git
synced 2026-02-20 06:14:22 +01:00
fold OrderedEnum into autotag module
This puts the OrderedEnum generic class next to where it is actually used. It also refers to the recipe it is taken from on docs.python.org. I also took the opportunity to give this a capitalized name (since it's a proper type).
This commit is contained in:
parent
443b8089d5
commit
323be89d4d
7 changed files with 76 additions and 66 deletions
|
|
@ -24,7 +24,7 @@ from beets.util import sorted_walk, ancestry, displayable_path
|
|||
# Parts of external interface.
|
||||
from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch
|
||||
from .match import tag_item, tag_album
|
||||
from .match import recommendation
|
||||
from .match import Recommendation
|
||||
|
||||
# Global logger.
|
||||
log = logging.getLogger('beets')
|
||||
|
|
|
|||
|
|
@ -21,16 +21,14 @@ import datetime
|
|||
import logging
|
||||
import re
|
||||
from munkres import Munkres
|
||||
import enum
|
||||
|
||||
from beets import plugins
|
||||
from beets import config
|
||||
from beets.util import plurality
|
||||
from beets.util.enumeration import OrderedEnum
|
||||
from beets.autotag import hooks
|
||||
|
||||
# Recommendation enumeration.
|
||||
recommendation = OrderedEnum('recommendation', ['none', 'low', 'medium',
|
||||
'strong'])
|
||||
|
||||
# Artist signals that indicate "various artists". These are used at the
|
||||
# album level to determine whether a given release is likely a VA
|
||||
|
|
@ -42,6 +40,40 @@ VA_ARTISTS = (u'', u'various artists', u'various', u'va', u'unknown')
|
|||
log = logging.getLogger('beets')
|
||||
|
||||
|
||||
# Recommendation enumeration.
|
||||
|
||||
# https://docs.python.org/3.4/library/enum.html#orderedenum
|
||||
class OrderedEnum(enum.Enum):
|
||||
"""An Enum subclass that allows comparison of members.
|
||||
"""
|
||||
def __ge__(self, other):
|
||||
if self.__class__ is other.__class__:
|
||||
return self.value >= other.value
|
||||
return NotImplemented
|
||||
def __gt__(self, other):
|
||||
if self.__class__ is other.__class__:
|
||||
return self.value > other.value
|
||||
return NotImplemented
|
||||
def __le__(self, other):
|
||||
if self.__class__ is other.__class__:
|
||||
return self.value <= other.value
|
||||
return NotImplemented
|
||||
def __lt__(self, other):
|
||||
if self.__class__ is other.__class__:
|
||||
return self.value < other.value
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class Recommendation(OrderedEnum):
|
||||
"""Indicates a qualitative suggestion to the user about what should
|
||||
be done with a given match.
|
||||
"""
|
||||
none = 0
|
||||
low = 1
|
||||
medium = 2
|
||||
strong = 3
|
||||
|
||||
|
||||
# Primary matching functionality.
|
||||
|
||||
def current_metadata(items):
|
||||
|
|
@ -252,7 +284,7 @@ def match_by_id(items):
|
|||
return None
|
||||
|
||||
# If all album IDs are equal, look up the album.
|
||||
if bool(reduce(lambda x,y: x if x==y else (), albumids)):
|
||||
if bool(reduce(lambda x,y: x if x == y else (), albumids)):
|
||||
albumid = albumids[0]
|
||||
log.debug('Searching for discovered album ID: ' + albumid)
|
||||
return hooks.album_for_mbid(albumid)
|
||||
|
|
@ -269,26 +301,26 @@ def _recommendation(results):
|
|||
"""
|
||||
if not results:
|
||||
# No candidates: no recommendation.
|
||||
return recommendation.none
|
||||
return Recommendation.none
|
||||
|
||||
# Basic distance thresholding.
|
||||
min_dist = results[0].distance
|
||||
if min_dist < config['match']['strong_rec_thresh'].as_number():
|
||||
# Strong recommendation level.
|
||||
rec = recommendation.strong
|
||||
rec = Recommendation.strong
|
||||
elif min_dist <= config['match']['medium_rec_thresh'].as_number():
|
||||
# Medium recommendation level.
|
||||
rec = recommendation.medium
|
||||
rec = Recommendation.medium
|
||||
elif len(results) == 1:
|
||||
# Only a single candidate.
|
||||
rec = recommendation.low
|
||||
rec = Recommendation.low
|
||||
elif results[1].distance - min_dist >= \
|
||||
config['match']['rec_gap_thresh'].as_number():
|
||||
# Gap between first two candidates is large.
|
||||
rec = recommendation.low
|
||||
rec = Recommendation.low
|
||||
else:
|
||||
# No conclusion. Return immediately. Can't be downgraded any further.
|
||||
return recommendation.none
|
||||
return Recommendation.none
|
||||
|
||||
# Downgrade to the max rec if it is lower than the current rec for an
|
||||
# applied penalty.
|
||||
|
|
@ -300,10 +332,10 @@ def _recommendation(results):
|
|||
for key in keys:
|
||||
if key in max_rec_view.keys():
|
||||
max_rec = max_rec_view[key].as_choice({
|
||||
'strong': recommendation.strong,
|
||||
'medium': recommendation.medium,
|
||||
'low': recommendation.low,
|
||||
'none': recommendation.none,
|
||||
'strong': Recommendation.strong,
|
||||
'medium': Recommendation.medium,
|
||||
'low': Recommendation.low,
|
||||
'none': Recommendation.none,
|
||||
})
|
||||
rec = min(rec, max_rec)
|
||||
|
||||
|
|
@ -347,7 +379,7 @@ def tag_album(items, search_artist=None, search_album=None,
|
|||
- The current album.
|
||||
- A list of AlbumMatch objects. The candidates are sorted by
|
||||
distance (i.e., best match first).
|
||||
- A recommendation.
|
||||
- A :class:`Recommendation`.
|
||||
If search_artist and search_album or search_id are provided, then
|
||||
they are used as search terms in place of the current metadata.
|
||||
"""
|
||||
|
|
@ -378,7 +410,7 @@ def tag_album(items, search_artist=None, search_album=None,
|
|||
# If we have a very good MBID match, return immediately.
|
||||
# Otherwise, this match will compete against metadata-based
|
||||
# matches.
|
||||
if rec == recommendation.strong:
|
||||
if rec == Recommendation.strong:
|
||||
log.debug('ID match.')
|
||||
return cur_artist, cur_album, candidates.values(), rec
|
||||
|
||||
|
|
@ -429,7 +461,7 @@ def tag_item(item, search_artist=None, search_title=None,
|
|||
hooks.TrackMatch(dist, track_info)
|
||||
# If this is a good match, then don't keep searching.
|
||||
rec = _recommendation(candidates.values())
|
||||
if rec == recommendation.strong and not config['import']['timid']:
|
||||
if rec == Recommendation.strong and not config['import']['timid']:
|
||||
log.debug('Track ID match.')
|
||||
return candidates.values(), rec
|
||||
|
||||
|
|
@ -438,7 +470,7 @@ def tag_item(item, search_artist=None, search_title=None,
|
|||
if candidates:
|
||||
return candidates.values(), rec
|
||||
else:
|
||||
return [], recommendation.none
|
||||
return [], Recommendation.none
|
||||
|
||||
# Search terms.
|
||||
if not (search_artist and search_title):
|
||||
|
|
|
|||
|
|
@ -664,7 +664,7 @@ def user_query(session):
|
|||
process.
|
||||
|
||||
The coroutine accepts an ImportTask objects. It uses the
|
||||
session's ``choose_match`` method to determine the ``action`` for
|
||||
session's `choose_match` method to determine the `action` for
|
||||
this task. Depending on the action additional stages are exectuted
|
||||
and the processed task is yielded.
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import beets
|
|||
from beets import ui
|
||||
from beets.ui import print_, input_, decargs
|
||||
from beets import autotag
|
||||
from beets.autotag import recommendation
|
||||
from beets.autotag import Recommendation
|
||||
from beets.autotag import hooks
|
||||
from beets import plugins
|
||||
from beets import importer
|
||||
|
|
@ -402,7 +402,7 @@ def _summary_judment(rec):
|
|||
made.
|
||||
"""
|
||||
if config['import']['quiet']:
|
||||
if rec == recommendation.strong:
|
||||
if rec == Recommendation.strong:
|
||||
return importer.action.APPLY
|
||||
else:
|
||||
action = config['import']['quiet_fallback'].as_choice({
|
||||
|
|
@ -410,7 +410,7 @@ def _summary_judment(rec):
|
|||
'asis': importer.action.ASIS,
|
||||
})
|
||||
|
||||
elif rec == recommendation.none:
|
||||
elif rec == Recommendation.none:
|
||||
action = config['import']['none_rec_action'].as_choice({
|
||||
'skip': importer.action.SKIP,
|
||||
'asis': importer.action.ASIS,
|
||||
|
|
@ -479,13 +479,13 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
|
||||
# Is the change good enough?
|
||||
bypass_candidates = False
|
||||
if rec != recommendation.none:
|
||||
if rec != Recommendation.none:
|
||||
match = candidates[0]
|
||||
bypass_candidates = True
|
||||
|
||||
while True:
|
||||
# Display and choose from candidates.
|
||||
require = rec <= recommendation.low
|
||||
require = rec <= Recommendation.low
|
||||
|
||||
if not bypass_candidates:
|
||||
# Display list of candidates.
|
||||
|
|
@ -559,7 +559,7 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
show_change(cur_artist, cur_album, match)
|
||||
|
||||
# Exact match => tag automatically if we're not in timid mode.
|
||||
if rec == recommendation.strong and not config['import']['timid']:
|
||||
if rec == Recommendation.strong and not config['import']['timid']:
|
||||
return match
|
||||
|
||||
# Ask for confirmation.
|
||||
|
|
|
|||
|
|
@ -1020,6 +1020,21 @@ class StringDistanceTest(unittest.TestCase):
|
|||
dist = string_dist(u'\xe9\xe1\xf1', u'ean')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
|
||||
class EnumTest(_common.TestCase):
|
||||
"""
|
||||
Test Enum Subclasses defined in beets.util.enumeration
|
||||
"""
|
||||
def test_ordered_enum(self):
|
||||
OrderedEnumTest = match.OrderedEnum('OrderedEnumTest', ['a', 'b', 'c'])
|
||||
self.assertLess(OrderedEnumTest.a, OrderedEnumTest.b)
|
||||
self.assertLess(OrderedEnumTest.a, OrderedEnumTest.c)
|
||||
self.assertLess(OrderedEnumTest.b, OrderedEnumTest.c)
|
||||
self.assertGreater(OrderedEnumTest.b, OrderedEnumTest.a)
|
||||
self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.a)
|
||||
self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.b)
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -436,7 +436,7 @@ class AutotagTest(_common.TestCase):
|
|||
'path',
|
||||
[_common.item()],
|
||||
)
|
||||
task.set_candidates('artist', 'album', [], autotag.recommendation.none)
|
||||
task.set_candidates('artist', 'album', [], autotag.Recommendation.none)
|
||||
session = _common.import_session(cli=True)
|
||||
res = session.choose_match(task)
|
||||
self.assertEqual(res, result)
|
||||
|
|
@ -791,7 +791,8 @@ class ShowChangeTest(_common.TestCase):
|
|||
self.info = autotag.AlbumInfo(
|
||||
u'the album', u'album id', u'the artist', u'artist id', [
|
||||
autotag.TrackInfo(u'the title', u'track id', index=1)
|
||||
])
|
||||
]
|
||||
)
|
||||
|
||||
def _show_change(self, items=None, info=None,
|
||||
cur_artist=u'the artist', cur_album=u'the album',
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# This file is part of beets.
|
||||
# Copyright 2013, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
"""Tests for utils."""
|
||||
|
||||
import _common
|
||||
from _common import unittest
|
||||
from beets.util.enumeration import OrderedEnum
|
||||
|
||||
class EnumTest(_common.TestCase):
|
||||
"""
|
||||
Test Enum Subclasses defined in beets.util.enumeration
|
||||
"""
|
||||
def test_ordered_enum(self):
|
||||
OrderedEnumTest = OrderedEnum('OrderedEnumTest', ['a', 'b', 'c'])
|
||||
self.assertLess(OrderedEnumTest.a, OrderedEnumTest.b)
|
||||
self.assertLess(OrderedEnumTest.a, OrderedEnumTest.c)
|
||||
self.assertLess(OrderedEnumTest.b, OrderedEnumTest.c)
|
||||
self.assertGreater(OrderedEnumTest.b, OrderedEnumTest.a)
|
||||
self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.a)
|
||||
self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.b)
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='suite')
|
||||
Loading…
Reference in a new issue