From a39a5b5d6686c569898fe445e41dda31156bae3d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 12 Apr 2011 20:52:39 -0700 Subject: [PATCH] extremely preliminary item importer skeleton (I shouldn't have started on this yet; the autotagger functionality isn't in place yet. Going back and doing that now...) --- beets/autotag/__init__.py | 33 +-------------- beets/importer.py | 86 +++++++++++++++++++++++++++++++++------ beets/util/__init__.py | 29 +++++++++++++ test/test_importer.py | 2 + 4 files changed, 106 insertions(+), 44 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 0978eec82..f6b90d69d 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -21,7 +21,7 @@ from beets.autotag import mb import re from munkres import Munkres from beets import library, mediafile, plugins -from beets.util import syspath, bytestring_path, levenshtein +from beets.util import levenshtein, sorted_walk import logging # Try 5 releases. In the future, this should be more dynamic: let the @@ -85,42 +85,13 @@ class AutotagError(Exception): # Global logger. log = logging.getLogger('beets') -def _sorted_walk(path): - """Like os.walk, but yields things in sorted, breadth-first - order. - """ - # Make sure the path isn't a Unicode string. - path = bytestring_path(path) - - # Get all the directories and files at this level. - dirs = [] - files = [] - for base in os.listdir(path): - cur = os.path.join(path, base) - if os.path.isdir(syspath(cur)): - dirs.append(base) - else: - files.append(base) - - # Sort lists and yield the current level. - dirs.sort() - files.sort() - yield (path, dirs, files) - - # Recurse into directories. - for base in dirs: - cur = os.path.join(path, base) - # yield from _sorted_walk(cur) - for res in _sorted_walk(cur): - yield res - def albums_in_dir(path): """Recursively searches the given directory and returns an iterable of (path, items) where path is a containing directory and items is a list of Items that is probably an album. Specifically, any folder containing any media files is an album. """ - for root, dirs, files in _sorted_walk(path): + for root, dirs, files in sorted_walk(path): # Get a list of items in the directory. items = [] for filename in files: diff --git a/beets/importer.py b/beets/importer.py index 7f5f700a2..1f15a0d65 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -129,14 +129,18 @@ class ImportConfig(object): for slot in self._fields: setattr(self, slot, kwargs[slot]) + # Normalize the paths. + if self.paths: + self.paths = map(normpath, self.paths) + # The importer task class. class ImportTask(object): - """Represents a single directory to be imported along with its - intermediate state. + """Represents a single set of items to be imported along with its + intermediate state. May represent an album or just a set of items. """ - def __init__(self, toppath, path=None, items=None): + def __init__(self, toppath=None, path=None, items=None): self.toppath = toppath self.path = path self.items = items @@ -151,19 +155,46 @@ class ImportTask(object): obj.sentinel = True return obj + @classmethod + def item_task(cls, item): + """Creates an ImportTask for a single item.""" + obj = cls() + obj.items = [item] + obj.is_album = False + return obj + def set_match(self, cur_artist, cur_album, candidates, rec): - """Sets the candidates matched by the autotag.tag_album method. + """Sets the candidates for this album matched by the + `autotag.tag_album` method. """ assert not self.sentinel self.cur_artist = cur_artist self.cur_album = cur_album self.candidates = candidates self.rec = rec + self.is_album = True def set_null_match(self): - """Set the candidate to indicate no match was found.""" + """Set the candidates to indicate no album match was found. + """ self.set_match(None, None, None, None) + def set_item_matches(self, item_matches): + """Sets the candidates for this set of items after an initial + match. `item_matches` should be a list of match tuples, + one for each item. + """ + assert len(self.items) == len(item_matches) + self.item_candidates = item_matches + self.is_album = False + + def set_null_item_match(self): + """For single-item tasks, mark the item as having no matches. + """ + assert len(self.items) == 1 + assert not self.is_album + self.item_matches = [None] + def set_choice(self, choice): """Given either an (info, items) tuple or an action constant, indicates that an action has been selected by the user (or @@ -192,8 +223,10 @@ class ImportTask(object): else: progress_set(self.toppath, self.path) + # Logical decisions. def should_create_album(self): """Should an album structure be created for these items?""" + assert self.is_album if self.choice_flag in (action.ALBUM, action.ASIS): return True elif self.choice_flag in (action.TRACKS, action.SKIP): @@ -210,7 +243,7 @@ class ImportTask(object): assert False def should_fetch_art(self): """Should album art be downloaded for this album?""" - return self.should_write_tags() + return self.should_write_tags() and self.is_album def should_infer_aa(self): """When creating an album structure, should the album artist field be inferred from the plurality of track artists? @@ -226,7 +259,7 @@ class ImportTask(object): assert False -# Core autotagger pipeline stages. +# Full-album pipeline stages. def read_albums(config): """A generator yielding all the albums (as ImportTask objects) found @@ -234,14 +267,11 @@ def read_albums(config): the resuming feature should be used. It may be True (resume if possible), False (never resume), or None (ask). """ - # Use absolute paths. - paths = [normpath(path) for path in config.paths] - # Look for saved progress. progress = config.resume is not False if progress: resume_dirs = {} - for path in paths: + for path in config.paths: resume_dir = progress_get(path) if resume_dir: @@ -258,11 +288,11 @@ def read_albums(config): # Clear progress; we're starting from the top. progress_set(path, None) - for toppath in paths: + for toppath in config.paths: # Produce each path. if progress: resume_dir = resume_dirs.get(toppath) - for path, items in autotag.albums_in_dir(os.path.expanduser(toppath)): + for path, items in autotag.albums_in_dir(toppath): if progress and resume_dir: # We're fast-forwarding to resume a previous tagging. if path == resume_dir: @@ -415,6 +445,36 @@ def apply_choices(config): task.save_progress() +# Single-item pipeline stages. + +def read_items(config): + """Reads individual items by recursively descending into a set of + directories. Generates ImportTask objects, each of which contains + a single item. + """ + for toppath in config.paths: + for path, items in autotag.albums_in_dir(toppath): + for item in items: + yield ImportTask.item_task(item) + +def item_lookup(config): + """A coroutine used to perform the initial MusicBrainz lookup for + an item task. + """ + task = None + while True: + task = yield task + task.set_null_item_match() #TODO + +def item_query(config): + """A coroutine that queries the user for input on single-item + lookups. + """ + task = None + while True: + task = yield task + task.set_choice(action.ASIS) # TODO + # Main driver. diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 7531ce2b6..53ef89b6c 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -45,6 +45,35 @@ def ancestry(path, pathmod=None): out.insert(0, path) return out +def sorted_walk(path): + """Like os.walk, but yields things in sorted, breadth-first + order. + """ + # Make sure the path isn't a Unicode string. + path = bytestring_path(path) + + # Get all the directories and files at this level. + dirs = [] + files = [] + for base in os.listdir(path): + cur = os.path.join(path, base) + if os.path.isdir(syspath(cur)): + dirs.append(base) + else: + files.append(base) + + # Sort lists and yield the current level. + dirs.sort() + files.sort() + yield (path, dirs, files) + + # Recurse into directories. + for base in dirs: + cur = os.path.join(path, base) + # yield from _sorted_walk(cur) + for res in sorted_walk(cur): + yield res + def mkdirall(path): """Make all the enclosing directories of path (like mkdir -p on the parent). diff --git a/test/test_importer.py b/test/test_importer.py index 6c33fe00f..80d069d11 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -163,11 +163,13 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): def _call_apply(self, coro, items, info): task = importer.ImportTask(None, None, None) + task.is_album = True task.set_choice((info, items)) coro.send(task) def _call_apply_choice(self, coro, items, choice): task = importer.ImportTask(None, None, items) + task.is_album = True task.set_choice(choice) coro.send(task)