merge in device branch (including BaseLibrary refactor)

This commit is contained in:
Adrian Sampson 2010-04-06 12:14:46 -07:00
commit cd9bb22270
6 changed files with 396 additions and 270 deletions

View file

@ -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
View 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

View file

@ -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
View file

@ -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())

View file

@ -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():

View file

@ -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):