Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/sampsyo/beets
Browse files Browse the repository at this point in the history
Conflicts:
	beets/library.py
  • Loading branch information
Rovanion committed Oct 5, 2014
2 parents 725cb9b + 2b1353a commit 60af550
Show file tree
Hide file tree
Showing 85 changed files with 7,038 additions and 2,583 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.svn
.tox
.coverage
.idea

# file patterns

Expand All @@ -22,6 +23,7 @@
*.project
*.pydevproject
*.ropeproject
*.orig

# Project Specific patterns

Expand Down
1 change: 1 addition & 0 deletions .hgignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
^MANIFEST$
^docs/_build/
^\.tox/
^\.idea/
2 changes: 1 addition & 1 deletion beets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

__version__ = '1.3.8'
__version__ = '1.3.9'
__author__ = 'Adrian Sampson <adrian@radbox.org>'

import beets.library
Expand Down
71 changes: 39 additions & 32 deletions beets/autotag/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ def current_metadata(items):
fields = ['artist', 'album', 'albumartist', 'year', 'disctotal',
'mb_albumid', 'label', 'catalognum', 'country', 'media',
'albumdisambig']
for key in fields:
values = [getattr(item, key) for item in items if item]
likelies[key], freq = plurality(values)
consensus[key] = (freq == len(values))
for field in fields:
values = [item[field] for item in items if item]
likelies[field], freq = plurality(values)
consensus[field] = (freq == len(values))

# If there's an album artist consensus, use this for the artist.
if consensus['albumartist'] and likelies['albumartist']:
Expand Down Expand Up @@ -261,16 +261,16 @@ def match_by_id(items):
# Is there a consensus on the MB album ID?
albumids = [item.mb_albumid for item in items if item.mb_albumid]
if not albumids:
log.debug('No album IDs found.')
log.debug(u'No album IDs found.')
return None

# If all album IDs are equal, look up the album.
if bool(reduce(lambda x, y: x if x == y else (), albumids)):
albumid = albumids[0]
log.debug('Searching for discovered album ID: ' + albumid)
log.debug(u'Searching for discovered album ID: {0}'.format(albumid))
return hooks.album_for_mbid(albumid)
else:
log.debug('No album ID consensus.')
log.debug(u'No album ID consensus.')


def _recommendation(results):
Expand Down Expand Up @@ -330,7 +330,7 @@ def _add_candidate(items, results, info):
checking the track count, ordering the items, checking for
duplicates, and calculating the distance.
"""
log.debug('Candidate: %s - %s' % (info.artist, info.album))
log.debug(u'Candidate: {0} - {1}'.format(info.artist, info.album))

# Discard albums with zero tracks.
if not info.tracks:
Expand All @@ -339,13 +339,13 @@ def _add_candidate(items, results, info):

# Don't duplicate.
if info.album_id in results:
log.debug('Duplicate.')
log.debug(u'Duplicate.')
return

# Discard matches without required tags.
for req_tag in config['match']['required'].as_str_seq():
if getattr(info, req_tag) is None:
log.debug('Ignored. Missing required tag: %s' % req_tag)
log.debug(u'Ignored. Missing required tag: {0}'.format(req_tag))
return

# Find mapping between the items and the track info.
Expand All @@ -358,39 +358,44 @@ def _add_candidate(items, results, info):
penalties = [key for _, key in dist]
for penalty in config['match']['ignored'].as_str_seq():
if penalty in penalties:
log.debug('Ignored. Penalty: %s' % penalty)
log.debug(u'Ignored. Penalty: {0}'.format(penalty))
return

log.debug('Success. Distance: %f' % dist)
log.debug(u'Success. Distance: {0}'.format(dist))
results[info.album_id] = hooks.AlbumMatch(dist, info, mapping,
extra_items, extra_tracks)


def tag_album(items, search_artist=None, search_album=None,
search_id=None):
"""Bundles together the functionality used to infer tags for a
set of items comprised by an album. Returns everything relevant:
- The current artist.
- The current album.
- A list of AlbumMatch objects. The candidates are sorted by
distance (i.e., best match first).
- 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.
"""Return a tuple of a artist name, an album name, a list of
`AlbumMatch` candidates from the metadata backend, and a
`Recommendation`.
The artist and album are the most common values of these fields
among `items`.
The `AlbumMatch` objects are generated by searching the metadata
backends. By default, the metadata of the items is used for the
search. This can be customized by setting the parameters. The
`mapping` field of the album has the matched `items` as keys.
The recommendation is calculated from the match qualitiy of the
candidates.
"""
# Get current metadata.
likelies, consensus = current_metadata(items)
cur_artist = likelies['artist']
cur_album = likelies['album']
log.debug('Tagging %s - %s' % (cur_artist, cur_album))
log.debug(u'Tagging {0} - {1}'.format(cur_artist, cur_album))

# The output result (distance, AlbumInfo) tuples (keyed by MB album
# ID).
candidates = {}

# Search by explicit ID.
if search_id is not None:
log.debug('Searching for album ID: ' + search_id)
log.debug(u'Searching for album ID: {0}'.format(search_id))
search_cands = hooks.albums_for_id(search_id)

# Use existing metadata or text search.
Expand All @@ -400,32 +405,33 @@ def tag_album(items, search_artist=None, search_album=None,
if id_info:
_add_candidate(items, candidates, id_info)
rec = _recommendation(candidates.values())
log.debug('Album ID match recommendation is ' + str(rec))
log.debug(u'Album ID match recommendation is {0}'.format(str(rec)))
if candidates and not config['import']['timid']:
# If we have a very good MBID match, return immediately.
# Otherwise, this match will compete against metadata-based
# matches.
if rec == Recommendation.strong:
log.debug('ID match.')
log.debug(u'ID match.')
return cur_artist, cur_album, candidates.values(), rec

# Search terms.
if not (search_artist and search_album):
# No explicit search terms -- use current metadata.
search_artist, search_album = cur_artist, cur_album
log.debug(u'Search terms: %s - %s' % (search_artist, search_album))
log.debug(u'Search terms: {0} - {1}'.format(search_artist,
search_album))

# Is this album likely to be a "various artist" release?
va_likely = ((not consensus['artist']) or
(search_artist.lower() in VA_ARTISTS) or
any(item.comp for item in items))
log.debug(u'Album might be VA: %s' % str(va_likely))
log.debug(u'Album might be VA: {0}'.format(str(va_likely)))

# Get the results from the data sources.
search_cands = hooks.album_candidates(items, search_artist,
search_album, va_likely)

log.debug(u'Evaluating %i candidates.' % len(search_cands))
log.debug(u'Evaluating {0} candidates.'.format(len(search_cands)))
for info in search_cands:
_add_candidate(items, candidates, info)

Expand All @@ -450,15 +456,15 @@ def tag_item(item, search_artist=None, search_title=None,
# First, try matching by MusicBrainz ID.
trackid = search_id or item.mb_trackid
if trackid:
log.debug('Searching for track ID: ' + trackid)
log.debug(u'Searching for track ID: {0}'.format(trackid))
for track_info in hooks.tracks_for_id(trackid):
dist = track_distance(item, track_info, incl_artist=True)
candidates[track_info.track_id] = \
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']:
log.debug('Track ID match.')
log.debug(u'Track ID match.')
return candidates.values(), rec

# If we're searching by ID, don't proceed.
Expand All @@ -471,15 +477,16 @@ def tag_item(item, search_artist=None, search_title=None,
# Search terms.
if not (search_artist and search_title):
search_artist, search_title = item.artist, item.title
log.debug(u'Item search terms: %s - %s' % (search_artist, search_title))
log.debug(u'Item search terms: {0} - {1}'.format(search_artist,
search_title))

# Get and evaluate candidate metadata.
for track_info in hooks.item_candidates(item, search_artist, search_title):
dist = track_distance(item, track_info, incl_artist=True)
candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info)

# Sort by distance and return with recommendation.
log.debug('Found %i candidates.' % len(candidates))
log.debug(u'Found {0} candidates.'.format(len(candidates)))
candidates = sorted(candidates.itervalues())
rec = _recommendation(candidates)
return candidates, rec
8 changes: 4 additions & 4 deletions beets/autotag/mb.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,13 +372,13 @@ def album_for_id(releaseid):
"""
albumid = _parse_id(releaseid)
if not albumid:
log.debug('Invalid MBID (%s).' % (releaseid))
log.debug(u'Invalid MBID ({0}).'.format(releaseid))
return
try:
res = musicbrainzngs.get_release_by_id(albumid,
RELEASE_INCLUDES)
except musicbrainzngs.ResponseError:
log.debug('Album ID match failed.')
log.debug(u'Album ID match failed.')
return None
except musicbrainzngs.MusicBrainzError as exc:
raise MusicBrainzAPIError(exc, 'get release by ID', albumid,
Expand All @@ -392,12 +392,12 @@ def track_for_id(releaseid):
"""
trackid = _parse_id(releaseid)
if not trackid:
log.debug('Invalid MBID (%s).' % (releaseid))
log.debug(u'Invalid MBID ({0}).'.format(releaseid))
return
try:
res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES)
except musicbrainzngs.ResponseError:
log.debug('Track ID match failed.')
log.debug(u'Track ID match failed.')
return None
except musicbrainzngs.MusicBrainzError as exc:
raise MusicBrainzAPIError(exc, 'get recording by ID', trackid,
Expand Down
4 changes: 2 additions & 2 deletions beets/config_default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ list_format_item: $artist - $album - $title
list_format_album: $albumartist - $album
time_format: '%Y-%m-%d %H:%M:%S'

sort_album: smartartist+
sort_item: smartartist+
sort_album: albumartist+ album+
sort_item: artist+ album+ disc+ track+

paths:
default: $albumartist/$album%aunique{}/$track $title
Expand Down
1 change: 1 addition & 0 deletions beets/dbcore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
from .types import Type
from .queryparse import query_from_strings
from .queryparse import sort_from_strings
from .queryparse import parse_sorted_query

# flake8: noqa
51 changes: 22 additions & 29 deletions beets/dbcore/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@

import beets
from beets.util.functemplate import Template
from .query import MatchQuery, NullSort
from .types import BASE_TYPE
from beets.dbcore import types
from .query import MatchQuery, NullSort, TrueQuery


class FormattedMapping(collections.Mapping):
Expand Down Expand Up @@ -115,11 +115,6 @@ class Model(object):
keys are field names and the values are `Type` objects.
"""

_bytes_keys = ()
"""Keys whose values should be stored as raw bytes blobs rather than
strings.
"""

_search_fields = ()
"""The fields that should be queried by default by unqualified query
terms.
Expand All @@ -129,6 +124,11 @@ class Model(object):
"""Optional Types for non-fixed (i.e., flexible and computed) fields.
"""

_sorts = {}
"""Optional named sort criteria. The keys are strings and the values
are subclasses of `Sort`.
"""

@classmethod
def _getters(cls):
"""Return a mapping from field names to getter functions.
Expand Down Expand Up @@ -160,21 +160,17 @@ def __init__(self, db=None, **values):
self.clear_dirty()

@classmethod
def _awaken(cls, db=None, fixed_values=None, flex_values=None):
def _awaken(cls, db=None, fixed_values={}, flex_values={}):
"""Create an object with values drawn from the database.
This is a performance optimization: the checks involved with
ordinary construction are bypassed.
"""
obj = cls(db)
if fixed_values:
for key, value in fixed_values.items():
obj._values_fixed[key] = cls._fields[key].normalize(value)
if flex_values:
for key, value in flex_values.items():
if key in cls._types:
value = cls._types[key].normalize(value)
obj._values_flex[key] = value
for key, value in fixed_values.iteritems():
obj._values_fixed[key] = cls._type(key).from_sql(value)
for key, value in flex_values.iteritems():
obj._values_flex[key] = cls._type(key).from_sql(value)
return obj

def __repr__(self):
Expand Down Expand Up @@ -208,7 +204,7 @@ def _type(self, key):
If the field has no explicit type, it is given the base `Type`,
which does no conversion.
"""
return self._fields.get(key) or self._types.get(key) or BASE_TYPE
return self._fields.get(key) or self._types.get(key) or types.DEFAULT

def __getitem__(self, key):
"""Get the value for a field. Raise a KeyError if the field is
Expand Down Expand Up @@ -332,19 +328,15 @@ def store(self):
self._check_db()

# Build assignments for query.
assignments = ''
assignments = []
subvars = []
for key in self._fields:
if key != 'id' and key in self._dirty:
self._dirty.remove(key)
assignments += key + '=?,'
value = self[key]
# Wrap path strings in buffers so they get stored
# "in the raw".
if key in self._bytes_keys and isinstance(value, str):
value = buffer(value)
assignments.append(key + '=?')
value = self._type(key).to_sql(self[key])
subvars.append(value)
assignments = assignments[:-1] # Knock off last ,
assignments = ','.join(assignments)

with self._db.transaction() as tx:
# Main table update.
Expand Down Expand Up @@ -737,22 +729,23 @@ def _make_attribute_table(self, flex_table):
id INTEGER PRIMARY KEY,
entity_id INTEGER,
key TEXT,
value NONE,
value TEXT,
UNIQUE(entity_id, key) ON CONFLICT REPLACE);
CREATE INDEX IF NOT EXISTS {0}_by_entity
ON {0} (entity_id);
""".format(flex_table))

# Querying.

def _fetch(self, model_cls, query, sort=None):
def _fetch(self, model_cls, query=None, sort=None):
"""Fetch the objects of type `model_cls` matching the given
query. The query may be given as a string, string sequence, a
Query object, or None (to fetch everything). `sort` is an
optional Sort object.
`Sort` object.
"""
query = query or TrueQuery() # A null query.
sort = sort or NullSort() # Unsorted.
where, subvals = query.clause()
sort = sort or NullSort()
order_by = sort.order_clause()

sql = ("SELECT * FROM {0} WHERE {1} {2}").format(
Expand Down
Loading

0 comments on commit 60af550

Please sign in to comment.