diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 2e99c9c92..b1e5fbb63 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -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: diff --git a/beets/device.py b/beets/device.py new file mode 100644 index 000000000..59824bd39 --- /dev/null +++ b/beets/device.py @@ -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 . + +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 + diff --git a/beets/library.py b/beets/library.py index 0e5c17319..87f8f9a8d 100644 --- a/beets/library.py +++ b/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) + + diff --git a/bts b/bts index 8205ad563..43cf1faf3 100755 --- a/bts +++ b/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()) + diff --git a/test/test_db.py b/test/test_db.py index ebcdaf4c1..8739388cd 100755 --- a/test/test_db.py +++ b/test/test_db.py @@ -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(): diff --git a/test/test_files.py b/test/test_files.py index 92ace3d05..d3a94d8f9 100755 --- a/test/test_files.py +++ b/test/test_files.py @@ -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):