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...)
This commit is contained in:
Adrian Sampson 2011-04-12 20:52:39 -07:00
parent 237f20a0a8
commit a39a5b5d66
4 changed files with 106 additions and 44 deletions

View file

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

View file

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

View file

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

View file

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