mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
merge in device branch (including BaseLibrary refactor)
This commit is contained in:
commit
cd9bb22270
6 changed files with 396 additions and 270 deletions
|
|
@ -65,7 +65,7 @@ def _first_n(it, n):
|
|||
break
|
||||
yield v
|
||||
|
||||
def albums_in_dir(path, lib=None):
|
||||
def albums_in_dir(path):
|
||||
"""Recursively searches the given directory and returns an iterable
|
||||
of lists of items where each list is probably an album.
|
||||
Specifically, any folder containing any media files is an album.
|
||||
|
|
@ -75,7 +75,7 @@ def albums_in_dir(path, lib=None):
|
|||
items = []
|
||||
for filename in files:
|
||||
try:
|
||||
i = library.Item.from_path(os.path.join(root, filename), lib)
|
||||
i = library.Item.from_path(os.path.join(root, filename))
|
||||
except mediafile.FileTypeError:
|
||||
pass
|
||||
else:
|
||||
|
|
|
|||
99
beets/device.py
Normal file
99
beets/device.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# This file is part of beets.
|
||||
# Copyright 2009, Adrian Sampson.
|
||||
#
|
||||
# Beets is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Beets is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with beets. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import locale
|
||||
import gpod
|
||||
from beets.library import BaseLibrary, Item
|
||||
|
||||
FIELD_MAP = {
|
||||
'artist': 'artist',
|
||||
'title': 'title',
|
||||
'BPM': 'bpm',
|
||||
'genre': 'genre',
|
||||
'album': 'album',
|
||||
'cd_nr': 'disc',
|
||||
'cds': 'disctotal',
|
||||
'track_nr': 'track',
|
||||
'tracks': 'tracktotal',
|
||||
}
|
||||
|
||||
class PodLibrary(BaseLibrary):
|
||||
def __init__(self, path):
|
||||
self.db = gpod.Database(path)
|
||||
self.syncing = False
|
||||
# Browsing convenience.
|
||||
def artists(self, query=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def albums(self, artist=None, query=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def items(self, artist=None, album=None, title=None, query=None):
|
||||
raise NotImplementedError
|
||||
@classmethod
|
||||
def by_name(cls, name):
|
||||
return cls(os.path.join(os.path.expanduser('~'), '.gvfs', name))
|
||||
|
||||
def _start_sync(self):
|
||||
# Make sure we have a version of libgpod with these
|
||||
# iPhone-specific functions.
|
||||
if self.syncing:
|
||||
return
|
||||
if hasattr(gpod, 'itdb_start_sync'):
|
||||
gpod.itdb_start_sync(self.db._itdb)
|
||||
self.syncing = True
|
||||
|
||||
def _stop_sync(self):
|
||||
if not self.syncing:
|
||||
return
|
||||
if hasattr(gpod, 'itdb_stop_sync'):
|
||||
gpod.itdb_stop_sync(self.db._itdb)
|
||||
self.syncing = False
|
||||
|
||||
def add(self, item):
|
||||
self._start_sync()
|
||||
track = self.db.new_Track()
|
||||
track['userdata'] = {
|
||||
'transferred': 0,
|
||||
'hostname': socket.gethostname(),
|
||||
'charset': locale.getpreferredencoding(),
|
||||
'pc_mtime': os.stat(item.path).st_mtime,
|
||||
}
|
||||
track._set_userdata_utf8('filename', item.path.encode())
|
||||
for dname, bname in FIELD_MAP.items():
|
||||
track[dname] = getattr(item, bname)
|
||||
track['tracklen'] = int(item.length * 1000)
|
||||
self.db.copy_delayed_files()
|
||||
|
||||
def get(self, query=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self):
|
||||
self._stop_sync()
|
||||
gpod.itdb_write(self.db._itdb, None)
|
||||
|
||||
def load(self, item, load_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def store(self, item, store_id=None, store_all=False):
|
||||
raise NotImplementedError
|
||||
|
||||
def remove(self, item):
|
||||
raise NotImplementedError
|
||||
|
||||
458
beets/library.py
458
beets/library.py
|
|
@ -136,12 +136,19 @@ def _components(path):
|
|||
|
||||
|
||||
class Item(object):
|
||||
def __init__(self, values, library=None):
|
||||
self.library = library
|
||||
def __init__(self, values):
|
||||
self.dirty = {}
|
||||
self._fill_record(values)
|
||||
self._clear_dirty()
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path):
|
||||
"""Creates a new item from the media file at the specified path.
|
||||
"""
|
||||
i = cls({})
|
||||
i.read(path)
|
||||
return i
|
||||
|
||||
def _fill_record(self, values):
|
||||
self.record = {}
|
||||
for key in item_keys:
|
||||
|
|
@ -156,8 +163,7 @@ class Item(object):
|
|||
self.dirty[key] = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'Item(' + repr(self.record) + \
|
||||
', library=' + repr(self.library) + ')'
|
||||
return 'Item(' + repr(self.record) + ')'
|
||||
|
||||
|
||||
#### item field accessors ####
|
||||
|
|
@ -189,92 +195,6 @@ class Item(object):
|
|||
super(Item, self).__setattr__(key, value)
|
||||
|
||||
|
||||
#### interaction with the database ####
|
||||
|
||||
def load(self, load_id=None):
|
||||
"""Refresh the item's metadata from the library database. If fetch_id
|
||||
is not specified, use the current item's id.
|
||||
"""
|
||||
if not self.library:
|
||||
raise LibraryError('no library to load from')
|
||||
|
||||
if load_id is None:
|
||||
load_id = self.id
|
||||
|
||||
c = self.library.conn.execute(
|
||||
'SELECT * FROM items WHERE id=?', (load_id,) )
|
||||
self._fill_record(c.fetchone())
|
||||
self._clear_dirty()
|
||||
c.close()
|
||||
|
||||
def store(self, store_id=None, store_all=False):
|
||||
"""Save the item's metadata into the library database. If store_id is
|
||||
specified, use it instead of the item's current id. If store_all is
|
||||
true, save the entire record instead of just the dirty fields.
|
||||
"""
|
||||
if not self.library:
|
||||
raise LibraryError('no library to store to')
|
||||
|
||||
if store_id is None:
|
||||
store_id = self.id
|
||||
|
||||
# build assignments for query
|
||||
assignments = ''
|
||||
subvars = []
|
||||
for key in item_keys:
|
||||
if (key != 'id') and (self.dirty[key] or store_all):
|
||||
assignments += key + '=?,'
|
||||
subvars.append(getattr(self, key))
|
||||
|
||||
if not assignments:
|
||||
# nothing to store (i.e., nothing was dirty)
|
||||
return
|
||||
|
||||
assignments = assignments[:-1] # knock off last ,
|
||||
|
||||
# finish the query
|
||||
query = 'UPDATE items SET ' + assignments + ' WHERE id=?'
|
||||
subvars.append(self.id)
|
||||
|
||||
self.library.conn.execute(query, subvars)
|
||||
self._clear_dirty()
|
||||
|
||||
def add(self, library=None):
|
||||
"""Add the item as a new object to the library database. The id field
|
||||
will be updated; the new id is returned. If library is specified, set
|
||||
the item's library before adding.
|
||||
"""
|
||||
if library:
|
||||
self.library = library
|
||||
if not self.library:
|
||||
raise LibraryError('no library to add to')
|
||||
|
||||
# build essential parts of query
|
||||
columns = ','.join([key for key in item_keys if key != 'id'])
|
||||
values = ','.join( ['?'] * (len(item_keys)-1) )
|
||||
subvars = []
|
||||
for key in item_keys:
|
||||
if key != 'id':
|
||||
subvars.append(getattr(self, key))
|
||||
|
||||
# issue query
|
||||
c = self.library.conn.cursor()
|
||||
query = 'INSERT INTO items (' + columns + ') VALUES (' + values + ')'
|
||||
c.execute(query, subvars)
|
||||
new_id = c.lastrowid
|
||||
c.close()
|
||||
|
||||
self._clear_dirty()
|
||||
self.id = new_id
|
||||
return new_id
|
||||
|
||||
def remove(self):
|
||||
"""Removes the item from the database (leaving the file on disk).
|
||||
"""
|
||||
self.library.conn.execute('DELETE FROM items WHERE id=?',
|
||||
(self.id,) )
|
||||
|
||||
|
||||
#### interaction with files' metadata ####
|
||||
|
||||
def read(self, read_path=None):
|
||||
|
|
@ -300,47 +220,7 @@ class Item(object):
|
|||
|
||||
#### dealing with files themselves ####
|
||||
|
||||
def destination(self):
|
||||
"""Returns the path within the library directory designated for this
|
||||
item (i.e., where the file ought to be).
|
||||
"""
|
||||
libpath = self.library.directory
|
||||
subpath_tmpl = Template(self.library.path_format)
|
||||
|
||||
# build the mapping for substitution in the path template, beginning
|
||||
# with the values from the database
|
||||
mapping = {}
|
||||
for key in metadata_keys:
|
||||
value = getattr(self, key)
|
||||
# sanitize the value for inclusion in a path:
|
||||
# replace / and leading . with _
|
||||
if isinstance(value, basestring):
|
||||
value.replace(os.sep, '_')
|
||||
value = re.sub(r'[\\/:]|^\.', '_', value)
|
||||
elif key in ('track', 'tracktotal', 'disc', 'disctotal'):
|
||||
# pad with zeros
|
||||
value = '%02i' % value
|
||||
else:
|
||||
value = str(value)
|
||||
mapping[key] = value
|
||||
|
||||
# Perform substitution.
|
||||
subpath = subpath_tmpl.substitute(mapping)
|
||||
|
||||
# Truncate path components.
|
||||
comps = _components(subpath)
|
||||
for i, comp in enumerate(comps):
|
||||
if len(comp) > MAX_FILENAME_LENGTH:
|
||||
comps[i] = comp[:MAX_FILENAME_LENGTH]
|
||||
subpath = os.path.join(*comps)
|
||||
|
||||
# Preserve extension.
|
||||
_, extension = os.path.splitext(self.path)
|
||||
subpath += extension
|
||||
|
||||
return _normpath(os.path.join(libpath, subpath))
|
||||
|
||||
def move(self, copy=False):
|
||||
def move(self, library, copy=False):
|
||||
"""Move the item to its designated location within the library
|
||||
directory (provided by destination()). Subdirectories are created as
|
||||
needed. If the operation succeeds, the item's path field is updated to
|
||||
|
|
@ -354,7 +234,7 @@ class Item(object):
|
|||
Note that one should almost certainly call store() and library.save()
|
||||
after this method in order to keep on-disk data consistent.
|
||||
"""
|
||||
dest = self.destination()
|
||||
dest = library.destination(self)
|
||||
|
||||
# Create necessary ancestry for the move. Like os.renames but only
|
||||
# halfway.
|
||||
|
|
@ -369,28 +249,7 @@ class Item(object):
|
|||
|
||||
# Either copying or moving succeeded, so update the stored path.
|
||||
self.path = dest
|
||||
|
||||
def delete(self):
|
||||
"""Deletes the item from the filesystem. If the item is located
|
||||
in the library directory, any empty parent directories are trimmed.
|
||||
Also calls remove(), deleting the appropriate row from the database.
|
||||
|
||||
As with move(), library.save() should almost certainly be called after
|
||||
invoking this (although store() should not).
|
||||
"""
|
||||
os.unlink(self.path)
|
||||
self.remove()
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path, library=None):
|
||||
"""Creates a new item from the media file at the specified path. Sets
|
||||
the item's library (but does not add the item) if library is
|
||||
specified.
|
||||
"""
|
||||
i = cls({})
|
||||
i.read(path)
|
||||
i.library = library
|
||||
return i
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -587,40 +446,19 @@ class ResultIterator(object):
|
|||
except StopIteration:
|
||||
self.cursor.close()
|
||||
raise
|
||||
return Item(row, self.library)
|
||||
return Item(row)
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseLibrary(object):
|
||||
"""Abstract base class for music libraries, which are loosely
|
||||
defined as sets of Items.
|
||||
"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
|
||||
class Library(object):
|
||||
def __init__(self, path='library.blb',
|
||||
directory='~/Music',
|
||||
path_format='$artist/$album/$track $title'):
|
||||
self.path = path
|
||||
self.directory = directory
|
||||
self.path_format = path_format
|
||||
|
||||
self.conn = sqlite3.connect(self.path)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
# this way we can access our SELECT results like dictionaries
|
||||
|
||||
self._setup()
|
||||
|
||||
def _setup(self):
|
||||
"""Set up the schema of the library file."""
|
||||
|
||||
setup_sql = 'CREATE TABLE IF NOT EXISTS items ('
|
||||
setup_sql += ', '.join([' '.join(f) for f in item_fields])
|
||||
setup_sql += ');'
|
||||
|
||||
self.conn.executescript(setup_sql)
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
### helpers ###
|
||||
|
||||
@classmethod
|
||||
|
|
@ -636,30 +474,196 @@ class Library(object):
|
|||
return val
|
||||
elif not isinstance(query, Query):
|
||||
raise ValueError('query must be None or have type Query or str')
|
||||
|
||||
|
||||
|
||||
#### main interface ####
|
||||
|
||||
def add(self, path, copy=False):
|
||||
"""Add a file to the library or recursively search a directory and add
|
||||
all its contents. If copy is True, copy files to their destination in
|
||||
the library directory while adding.
|
||||
|
||||
### basic operations ###
|
||||
|
||||
def add(self, item, copy=False): #FIXME copy should default to true
|
||||
"""Add the item as a new object to the library database. The id field
|
||||
will be updated; the new id is returned. If copy, then each item is
|
||||
copied to the destination location before it is added.
|
||||
"""
|
||||
|
||||
for f in _walk_files(path):
|
||||
try:
|
||||
i = Item.from_path(_normpath(f), self)
|
||||
if copy:
|
||||
i.move(copy=True)
|
||||
i.add()
|
||||
except FileTypeError:
|
||||
log.warn(f + ' of unknown type, skipping')
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, query=None):
|
||||
"""Returns a ResultIterator to the items matching query, which may be
|
||||
"""Returns a sequence of the items matching query, which may be
|
||||
None (match the entire library), a Query object, or a query string.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self):
|
||||
"""Ensure that the library is consistent on disk. A no-op by
|
||||
default.
|
||||
"""
|
||||
pass
|
||||
|
||||
def load(self, item, load_id=None):
|
||||
"""Refresh the item's metadata from the library database. If fetch_id
|
||||
is not specified, use the item's current id.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def store(self, item, store_id=None, store_all=False):
|
||||
"""Save the item's metadata into the library database. If store_id is
|
||||
specified, use it instead of the item's current id. If store_all is
|
||||
true, save the entire record instead of just the dirty fields.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def remove(self, item):
|
||||
"""Removes the item from the database (leaving the file on disk).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
### browsing operations ###
|
||||
# Naive implementations are provided, but these methods should be
|
||||
# overridden if a better implementation exists.
|
||||
|
||||
def artists(self, query=None):
|
||||
"""Returns a sorted sequence of artists in the database, possibly
|
||||
filtered by a query (in the same sense as get()).
|
||||
"""
|
||||
out = set()
|
||||
for item in self.get(query):
|
||||
out.add(item.artist)
|
||||
return sorted(out)
|
||||
|
||||
def albums(self, artist=None, query=None):
|
||||
"""Returns a sorted list of (artist, album) pairs, possibly filtered
|
||||
by an artist name or an arbitrary query.
|
||||
"""
|
||||
out = set()
|
||||
for item in self.get(query):
|
||||
if artist is None or item.artist == artist:
|
||||
out.add((item.artist, item.album))
|
||||
return sorted(out)
|
||||
|
||||
def items(self, artist=None, album=None, title=None, query=None):
|
||||
"""Returns a sequence of the items matching the given artist,
|
||||
album, title, and query (if present). Sorts in such a way as to
|
||||
group albums appropriately.
|
||||
"""
|
||||
out = []
|
||||
for item in self.get(query):
|
||||
if (artist is None or item.artist == artist) and \
|
||||
(album is None or item.album == album) and \
|
||||
(title is None or item.title == title):
|
||||
out.append(item)
|
||||
|
||||
# Sort by: artist, album, disc, track.
|
||||
def compare(a, b):
|
||||
return cmp(a.artist, b.artist) or \
|
||||
cmp(a.album, b.album) or \
|
||||
cmp(a.disc, b.disc) or \
|
||||
cmp(a.track, b.track)
|
||||
return sorted(out, compare)
|
||||
|
||||
|
||||
### convenience methods ###
|
||||
|
||||
def add_path(self, path, copy=False):
|
||||
items = []
|
||||
for f in _walk_files(path):
|
||||
try:
|
||||
item = Item.from_path(_normpath(f))
|
||||
except FileTypeError:
|
||||
log.warn(f + ' of unknown type, skipping')
|
||||
self.add(item, copy)
|
||||
|
||||
|
||||
class Library(BaseLibrary):
|
||||
"""A music library using an SQLite database as a metadata store."""
|
||||
def __init__(self, path='library.blb',
|
||||
directory='~/Music',
|
||||
path_format='$artist/$album/$track $title'):
|
||||
self.path = path
|
||||
self.directory = directory
|
||||
self.path_format = path_format
|
||||
|
||||
self.conn = sqlite3.connect(self.path)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
# this way we can access our SELECT results like dictionaries
|
||||
|
||||
self._setup()
|
||||
|
||||
def _setup(self):
|
||||
"""Set up the schema of the library file."""
|
||||
setup_sql = 'CREATE TABLE IF NOT EXISTS items ('
|
||||
setup_sql += ', '.join([' '.join(f) for f in item_fields])
|
||||
setup_sql += ');'
|
||||
|
||||
self.conn.executescript(setup_sql)
|
||||
self.conn.commit()
|
||||
|
||||
def destination(self, item):
|
||||
"""Returns the path in the library directory designated for item
|
||||
item (i.e., where the file ought to be).
|
||||
"""
|
||||
libpath = self.directory
|
||||
subpath_tmpl = Template(self.path_format)
|
||||
|
||||
# build the mapping for substitution in the path template, beginning
|
||||
# with the values from the database
|
||||
mapping = {}
|
||||
for key in metadata_keys:
|
||||
value = getattr(item, key)
|
||||
# sanitize the value for inclusion in a path:
|
||||
# replace / and leading . with _
|
||||
if isinstance(value, basestring):
|
||||
value.replace(os.sep, '_')
|
||||
value = re.sub(r'[\\/:]|^\.', '_', value)
|
||||
elif key in ('track', 'tracktotal', 'disc', 'disctotal'):
|
||||
# pad with zeros
|
||||
value = '%02i' % value
|
||||
else:
|
||||
value = str(value)
|
||||
mapping[key] = value
|
||||
|
||||
# Perform substitution.
|
||||
subpath = subpath_tmpl.substitute(mapping)
|
||||
|
||||
# Truncate path components.
|
||||
comps = _components(subpath)
|
||||
for i, comp in enumerate(comps):
|
||||
if len(comp) > MAX_FILENAME_LENGTH:
|
||||
comps[i] = comp[:MAX_FILENAME_LENGTH]
|
||||
subpath = os.path.join(*comps)
|
||||
|
||||
# Preserve extension.
|
||||
_, extension = os.path.splitext(item.path)
|
||||
subpath += extension
|
||||
|
||||
return _normpath(os.path.join(libpath, subpath))
|
||||
|
||||
#### main interface ####
|
||||
|
||||
def add(self, item, copy=False):
|
||||
#FIXME make a deep copy of the item?
|
||||
item.library = self
|
||||
if copy:
|
||||
item.move(self, copy=True)
|
||||
|
||||
# build essential parts of query
|
||||
columns = ','.join([key for key in item_keys if key != 'id'])
|
||||
values = ','.join( ['?'] * (len(item_keys)-1) )
|
||||
subvars = []
|
||||
for key in item_keys:
|
||||
if key != 'id':
|
||||
subvars.append(getattr(item, key))
|
||||
|
||||
# issue query
|
||||
c = self.conn.cursor()
|
||||
query = 'INSERT INTO items (' + columns + ') VALUES (' + values + ')'
|
||||
c.execute(query, subvars)
|
||||
new_id = c.lastrowid
|
||||
c.close()
|
||||
|
||||
item._clear_dirty()
|
||||
item.id = new_id
|
||||
return new_id
|
||||
|
||||
def get(self, query=None):
|
||||
return self._get_query(query).execute(self)
|
||||
|
||||
def save(self):
|
||||
|
|
@ -667,13 +671,48 @@ class Library(object):
|
|||
"""
|
||||
self.conn.commit()
|
||||
|
||||
def load(self, item, load_id=None):
|
||||
if load_id is None:
|
||||
load_id = item.id
|
||||
|
||||
c = self.conn.execute(
|
||||
'SELECT * FROM items WHERE id=?', (load_id,) )
|
||||
item._fill_record(c.fetchone())
|
||||
item._clear_dirty()
|
||||
c.close()
|
||||
|
||||
def store(self, item, store_id=None, store_all=False):
|
||||
if store_id is None:
|
||||
store_id = item.id
|
||||
|
||||
# build assignments for query
|
||||
assignments = ''
|
||||
subvars = []
|
||||
for key in item_keys:
|
||||
if (key != 'id') and (item.dirty[key] or store_all):
|
||||
assignments += key + '=?,'
|
||||
subvars.append(getattr(item, key))
|
||||
|
||||
if not assignments:
|
||||
# nothing to store (i.e., nothing was dirty)
|
||||
return
|
||||
|
||||
assignments = assignments[:-1] # knock off last ,
|
||||
|
||||
# finish the query
|
||||
query = 'UPDATE items SET ' + assignments + ' WHERE id=?'
|
||||
subvars.append(item.id)
|
||||
|
||||
self.conn.execute(query, subvars)
|
||||
item._clear_dirty()
|
||||
|
||||
def remove(self, item):
|
||||
self.conn.execute('DELETE FROM items WHERE id=?', (item.id,))
|
||||
|
||||
|
||||
### browsing ###
|
||||
|
||||
def artists(self, query=None):
|
||||
"""Returns a list of artists in the database, possibly filtered by a
|
||||
query (in the same sense as get()).
|
||||
"""
|
||||
where, subvals = self._get_query(query).clause()
|
||||
sql = "SELECT DISTINCT artist FROM items " + \
|
||||
"WHERE " + where + \
|
||||
|
|
@ -682,9 +721,6 @@ class Library(object):
|
|||
return [res[0] for res in c.fetchall()]
|
||||
|
||||
def albums(self, artist=None, query=None):
|
||||
"""Returns a list of (artist, album) pairs, possibly filtered by an
|
||||
artist name or an arbitrary query.
|
||||
"""
|
||||
query = self._get_query(query)
|
||||
if artist is not None:
|
||||
# "Add" the artist to the query.
|
||||
|
|
@ -697,10 +733,6 @@ class Library(object):
|
|||
return [(res[0], res[1]) for res in c.fetchall()]
|
||||
|
||||
def items(self, artist=None, album=None, title=None, query=None):
|
||||
"""Returns a ResultIterator over the items matching the given artist,
|
||||
album, title, and query (if present). Sorts in such a way as to group
|
||||
albums appropriately.
|
||||
"""
|
||||
queries = [self._get_query(query)]
|
||||
if artist is not None:
|
||||
queries.append(MatchQuery('artist', artist))
|
||||
|
|
@ -717,3 +749,5 @@ class Library(object):
|
|||
c = self.conn.execute(sql, subvals)
|
||||
return ResultIterator(c, self)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
24
bts
24
bts
|
|
@ -95,12 +95,11 @@ def tag_album(items, lib):
|
|||
# Change metadata and add to library.
|
||||
autotag.apply_metadata(items, info)
|
||||
for item in items:
|
||||
item.move(True)
|
||||
item.add()
|
||||
item.move(lib, True)
|
||||
lib.add(item)
|
||||
item.write()
|
||||
|
||||
|
||||
|
||||
class BeetsApp(cmdln.Cmdln):
|
||||
name = "bts"
|
||||
|
||||
|
|
@ -136,8 +135,7 @@ class BeetsApp(cmdln.Cmdln):
|
|||
${cmd_option_list}
|
||||
"""
|
||||
for path in paths:
|
||||
for album in autotag.albums_in_dir(os.path.expanduser(path),
|
||||
self.lib):
|
||||
for album in autotag.albums_in_dir(os.path.expanduser(path)):
|
||||
print
|
||||
tag_album(album, self.lib)
|
||||
self.lib.save()
|
||||
|
|
@ -176,7 +174,23 @@ class BeetsApp(cmdln.Cmdln):
|
|||
from beets.player.bpd import Server
|
||||
Server(self.lib, host, int(port), password).run()
|
||||
|
||||
def do_dadd(self, subcmd, opts, name, *criteria):
|
||||
"""${cmd_name}: add files to a device
|
||||
|
||||
${cmd_usage}
|
||||
${cmd_option_list}
|
||||
"""
|
||||
q = ' '.join(criteria)
|
||||
if not q.strip(): q = None
|
||||
items = self.lib.items(query=q)
|
||||
|
||||
from beets import device
|
||||
pod = device.PodLibrary.by_name(name)
|
||||
for item in items:
|
||||
pod.add(item)
|
||||
pod.save()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = BeetsApp()
|
||||
sys.exit(app.main())
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import beets.library
|
|||
|
||||
def lib(): return beets.library.Library('rsrc' + os.sep + 'test.blb')
|
||||
def boracay(l): return beets.library.Item(l.conn.execute('select * from items '
|
||||
'where id=3').fetchone(), l)
|
||||
'where id=3').fetchone())
|
||||
def item(lib=None): return beets.library.Item({
|
||||
'title': u'the title',
|
||||
'artist': u'the artist',
|
||||
|
|
@ -45,7 +45,7 @@ def item(lib=None): return beets.library.Item({
|
|||
'path': 'somepath',
|
||||
'length': 60.0,
|
||||
'bitrate': 128000,
|
||||
}, lib)
|
||||
})
|
||||
np = beets.library._normpath
|
||||
|
||||
class LoadTest(unittest.TestCase):
|
||||
|
|
@ -58,12 +58,12 @@ class LoadTest(unittest.TestCase):
|
|||
def test_load_restores_data_from_db(self):
|
||||
original_title = self.i.title
|
||||
self.i.title = 'something'
|
||||
self.i.load()
|
||||
self.lib.load(self.i)
|
||||
self.assertEqual(original_title, self.i.title)
|
||||
|
||||
def test_load_clears_dirty_flags(self):
|
||||
self.i.artist = 'something'
|
||||
self.i.load()
|
||||
self.lib.load(self.i)
|
||||
self.assertTrue(not self.i.dirty['artist'])
|
||||
|
||||
class StoreTest(unittest.TestCase):
|
||||
|
|
@ -75,7 +75,7 @@ class StoreTest(unittest.TestCase):
|
|||
|
||||
def test_store_changes_database_value(self):
|
||||
self.i.year = 1987
|
||||
self.i.store()
|
||||
self.lib.store(self.i)
|
||||
new_year = self.lib.conn.execute('select year from items where '
|
||||
'title="Boracay"').fetchone()['year']
|
||||
self.assertEqual(new_year, 1987)
|
||||
|
|
@ -83,14 +83,14 @@ class StoreTest(unittest.TestCase):
|
|||
def test_store_only_writes_dirty_fields(self):
|
||||
original_genre = self.i.genre
|
||||
self.i.record['genre'] = 'beatboxing' # change value w/o dirtying
|
||||
self.i.store()
|
||||
self.lib.store(self.i)
|
||||
new_genre = self.lib.conn.execute('select genre from items where '
|
||||
'title="Boracay"').fetchone()['genre']
|
||||
self.assertEqual(new_genre, original_genre)
|
||||
|
||||
def test_store_clears_dirty_flags(self):
|
||||
self.i.composer = 'tvp'
|
||||
self.i.store()
|
||||
self.lib.store(self.i)
|
||||
self.assertTrue(not self.i.dirty['composer'])
|
||||
|
||||
class AddTest(unittest.TestCase):
|
||||
|
|
@ -101,13 +101,13 @@ class AddTest(unittest.TestCase):
|
|||
self.lib.conn.close()
|
||||
|
||||
def test_item_add_inserts_row(self):
|
||||
self.i.add()
|
||||
self.lib.add(self.i)
|
||||
new_grouping = self.lib.conn.execute('select grouping from items '
|
||||
'where composer="the composer"').fetchone()['grouping']
|
||||
self.assertEqual(new_grouping, self.i.grouping)
|
||||
|
||||
def test_library_add_inserts_row(self):
|
||||
self.lib.add(os.path.join('rsrc', 'full.mp3'))
|
||||
def test_library_add_path_inserts_row(self):
|
||||
self.lib.add_path(os.path.join('rsrc', 'full.mp3'))
|
||||
new_grouping = self.lib.conn.execute('select grouping from items '
|
||||
'where composer="the composer"').fetchone()['grouping']
|
||||
self.assertEqual(new_grouping, self.i.grouping)
|
||||
|
|
@ -121,7 +121,7 @@ class RemoveTest(unittest.TestCase):
|
|||
self.lib.conn.close()
|
||||
|
||||
def test_remove_deletes_from_db(self):
|
||||
self.i.remove()
|
||||
self.lib.remove(self.i)
|
||||
c = self.lib.conn.execute('select * from items where id=3')
|
||||
self.assertEqual(c.fetchone(), None)
|
||||
|
||||
|
|
@ -154,12 +154,12 @@ class DestinationTest(unittest.TestCase):
|
|||
def test_directory_works_with_trailing_slash(self):
|
||||
self.lib.directory = 'one/'
|
||||
self.lib.path_format = 'two'
|
||||
self.assertEqual(self.i.destination(), np('one/two'))
|
||||
self.assertEqual(self.lib.destination(self.i), np('one/two'))
|
||||
|
||||
def test_directory_works_without_trailing_slash(self):
|
||||
self.lib.directory = 'one'
|
||||
self.lib.path_format = 'two'
|
||||
self.assertEqual(self.i.destination(), np('one/two'))
|
||||
self.assertEqual(self.lib.destination(self.i), np('one/two'))
|
||||
|
||||
def test_destination_substitues_metadata_values(self):
|
||||
self.lib.directory = 'base'
|
||||
|
|
@ -167,13 +167,15 @@ class DestinationTest(unittest.TestCase):
|
|||
self.i.title = 'three'
|
||||
self.i.artist = 'two'
|
||||
self.i.album = 'one'
|
||||
self.assertEqual(self.i.destination(), np('base/one/two three'))
|
||||
self.assertEqual(self.lib.destination(self.i),
|
||||
np('base/one/two three'))
|
||||
|
||||
def test_destination_preserves_extension(self):
|
||||
self.lib.directory = 'base'
|
||||
self.lib.path_format = '$title'
|
||||
self.i.path = 'hey.audioFormat'
|
||||
self.assertEqual(self.i.destination(),np('base/the title.audioFormat'))
|
||||
self.assertEqual(self.lib.destination(self.i),
|
||||
np('base/the title.audioFormat'))
|
||||
|
||||
def test_destination_pads_some_indices(self):
|
||||
self.lib.directory = 'base'
|
||||
|
|
@ -185,11 +187,12 @@ class DestinationTest(unittest.TestCase):
|
|||
self.i.disctotal = 4
|
||||
self.i.bpm = 5
|
||||
self.i.year = 6
|
||||
self.assertEqual(self.i.destination(), np('base/01 02 03 04 5 6'))
|
||||
self.assertEqual(self.lib.destination(self.i),
|
||||
np('base/01 02 03 04 5 6'))
|
||||
|
||||
def test_destination_escapes_slashes(self):
|
||||
self.i.album = 'one/two'
|
||||
dest = self.i.destination()
|
||||
dest = self.lib.destination(self.i)
|
||||
self.assertTrue('one' in dest)
|
||||
self.assertTrue('two' in dest)
|
||||
self.assertFalse('one/two' in dest)
|
||||
|
|
@ -197,13 +200,13 @@ class DestinationTest(unittest.TestCase):
|
|||
def test_destination_long_names_truncated(self):
|
||||
self.i.title = 'X'*300
|
||||
self.i.artist = 'Y'*300
|
||||
for c in self.i.destination().split(os.path.sep):
|
||||
for c in self.lib.destination(self.i).split(os.path.sep):
|
||||
self.assertTrue(len(c) <= 255)
|
||||
|
||||
def test_destination_long_names_keep_extension(self):
|
||||
self.i.title = 'X'*300
|
||||
self.i.path = 'something.extn'
|
||||
dest = self.i.destination()
|
||||
dest = self.lib.destination(self.i)
|
||||
self.assertEqual(dest[-5:], '.extn')
|
||||
|
||||
def suite():
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class MoveTest(unittest.TestCase):
|
|||
# add it to a temporary library
|
||||
self.lib = beets.library.Library(':memory:')
|
||||
self.i = beets.library.Item.from_path(self.path)
|
||||
self.i.add(self.lib)
|
||||
self.lib.add(self.i)
|
||||
|
||||
# set up the destination
|
||||
self.libdir = join('rsrc', 'testlibdir')
|
||||
|
|
@ -52,49 +52,25 @@ class MoveTest(unittest.TestCase):
|
|||
shutil.rmtree(self.libdir)
|
||||
|
||||
def test_move_arrives(self):
|
||||
self.i.move()
|
||||
self.i.move(self.lib)
|
||||
self.assertTrue(os.path.exists(self.dest))
|
||||
|
||||
def test_move_departs(self):
|
||||
self.i.move()
|
||||
self.i.move(self.lib)
|
||||
self.assertTrue(not os.path.exists(self.path))
|
||||
|
||||
def test_copy_arrives(self):
|
||||
self.i.move(copy=True)
|
||||
self.i.move(self.lib, copy=True)
|
||||
self.assertTrue(os.path.exists(self.dest))
|
||||
|
||||
def test_copy_does_not_depart(self):
|
||||
self.i.move(copy=True)
|
||||
self.i.move(self.lib, copy=True)
|
||||
self.assertTrue(os.path.exists(self.path))
|
||||
|
||||
def test_move_changes_path(self):
|
||||
self.i.move()
|
||||
self.i.move(self.lib)
|
||||
self.assertEqual(self.i.path, beets.library._normpath(self.dest))
|
||||
|
||||
class DeleteTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# make a temporary file
|
||||
self.path = join('rsrc', 'temp.mp3')
|
||||
shutil.copy(join('rsrc', 'full.mp3'), self.path)
|
||||
|
||||
# add it to a temporary library
|
||||
self.lib = beets.library.Library(':memory:')
|
||||
self.i = beets.library.Item.from_path(self.path)
|
||||
self.i.add(self.lib)
|
||||
def tearDown(self):
|
||||
# make sure the temp file is gone
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
|
||||
def test_delete_deletes_file(self):
|
||||
self.i.delete()
|
||||
self.assertTrue(not os.path.exists(self.path))
|
||||
|
||||
def test_delete_removes_from_db(self):
|
||||
self.i.delete()
|
||||
c = self.lib.conn.execute('select * from items where 1')
|
||||
self.assertEqual(c.fetchone(), None)
|
||||
|
||||
class WalkTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# create a directory structure for testing
|
||||
|
|
@ -146,8 +122,8 @@ class AddTest(unittest.TestCase):
|
|||
if os.path.exists(self.dir):
|
||||
shutil.rmtree(self.dir)
|
||||
|
||||
def test_library_add_copies(self):
|
||||
self.lib.add(os.path.join('rsrc', 'full.mp3'), copy=True)
|
||||
def test_library_add_path_copies(self):
|
||||
self.lib.add_path(os.path.join('rsrc', 'full.mp3'), copy=True)
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.dir, 'item.mp3')))
|
||||
|
||||
class HelperTest(unittest.TestCase):
|
||||
|
|
|
|||
Loading…
Reference in a new issue