def manipulate_files(self, move=False, copy=False, write=False, link=False, session=None): items = self.imported_items() # Save the original paths of all items for deletion and pruning # in the next step (finalization). self.old_paths = [item.path for item in items] for item in items: if move or copy or link: # In copy and link modes, treat re-imports specially: # move in-library files. (Out-of-library files are # copied/moved as usual). old_path = item.path if (copy or link) and self.replaced_items[item] and \ session.lib.directory in util.ancestry(old_path): item.move() # We moved the item, so remove the # now-nonexistent file from old_paths. self.old_paths.remove(old_path) else: # A normal import. Just copy files and keep track of # old paths. item.move(copy, link) if write and self.apply: item.try_write() with session.lib.transaction(): for item in self.imported_items(): item.store() plugins.send('import_task_files', session=session, task=self)
def _run_importer(self): """ Create an instance of the plugin, run the importer, and remove/unregister the plugin instance so a new instance can be created when this method is run again. This is a convenience method that can be called to setup, exercise and teardown the system under test after setting any config options and before assertions are made regarding changes to the filesystem. """ # Setup # Create an instance of the plugin plugins.find_plugins() # Exercise # Run the importer self.importer.run() # Fake the occurence of the cli_exit event plugins.send('cli_exit', lib=self.lib) # Teardown if plugins._instances: classes = list(plugins._classes) # Unregister listners for event in classes[0].listeners: del classes[0].listeners[event][0] # Delete the plugin instance so a new one gets created for each test del plugins._instances[classes[0]] log.debug("--- library structure") self._list_files(self.lib_dir)
def album_candidates(items, artist, album, va_likely): """Search for album matches. ``items`` is a list of Item objects that make up the album. ``artist`` and ``album`` are the respective names (strings), which may be derived from the item list or may be entered by the user. ``va_likely`` is a boolean indicating whether the album is likely to be a "various artists" release. """ out = [] # Base candidates if we have album and artist to match. if artist and album: try: out.extend(mb.match_album(artist, album, len(items))) except mb.MusicBrainzAPIError as exc: exc.log(log) # Also add VA matches from MusicBrainz where appropriate. if va_likely and album: try: out.extend(mb.match_album(None, album, len(items))) except mb.MusicBrainzAPIError as exc: exc.log(log) # Candidates from plugins. out.extend(plugins.candidates(items, artist, album, va_likely)) # Notify subscribed plugins about fetched album info for a in out: plugins.send('albuminfo_received', info=a) return out
def write(self, path=None): """Write the item's metadata to a media file. Updates the mediafile with properties from itself. Can raise either a `ReadError` or a `WriteError`. """ if path is None: path = self.path else: path = normpath(path) try: mediafile = MediaFile(path) except (OSError, IOError) as exc: raise ReadError(self.path, exc) plugins.send('write', item=self, path=path) try: mediafile.update(self, id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. if path == self.path: self.mtime = self.current_mtime() plugins.send('after_write', item=self, path=path)
def _emit_imported(self, lib): # FIXME This shouldn't be here. Skipped tasks should be removed from # the pipeline. if self.skip: return for item in self.imported_items(): plugins.send("item_imported", lib=lib, item=item)
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ parser = SubcommandsOptionParser() parser.add_option('-l', '--library', dest='library', help='library database file to use') parser.add_option('-d', '--directory', dest='directory', help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='print debugging information') parser.add_option('-c', '--config', dest='config', help='path to configuration file') parser.add_option('-h', '--help', dest='help', action='store_true', help='how this help message and exit') parser.add_option('--version', dest='version', action='store_true', help=optparse.SUPPRESS_HELP) options, subargs = parser.parse_global_options(args) subcommands, plugins, lib = _setup(options, lib) parser.add_subcommand(*subcommands) subcommand, suboptions, subargs = parser.parse_subcommand(subargs) subcommand.func(lib, suboptions, subargs) plugins.send('cli_exit', lib=lib)
def write(self, path=None): """Write the item's metadata to a media file. ``path`` defaults to the item's path property. Can raise either a `ReadError` or a `WriteError`. """ if path is None: path = self.path else: path = normpath(path) try: f = MediaFile(syspath(path)) except (OSError, IOError) as exc: raise ReadError(self.path, exc) plugins.send('write', item=self, path=path) for key in ITEM_KEYS_WRITABLE: setattr(f, key, self[key]) try: f.save(id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. self.mtime = self.current_mtime() plugins.send('after_write', item=self)
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ parser = SubcommandsOptionParser() parser.add_format_option(flags=("--format-item",), target=library.Item) parser.add_format_option(flags=("--format-album",), target=library.Album) parser.add_option("-l", "--library", dest="library", help="library database file to use") parser.add_option("-d", "--directory", dest="directory", help="destination music directory") parser.add_option("-v", "--verbose", dest="verbose", action="count", help="print debugging information") parser.add_option("-c", "--config", dest="config", help="path to configuration file") parser.add_option("-h", "--help", dest="help", action="store_true", help="how this help message and exit") parser.add_option("--version", dest="version", action="store_true", help=optparse.SUPPRESS_HELP) options, subargs = parser.parse_global_options(args) # Special case for the `config --edit` command: bypass _setup so # that an invalid configuration does not prevent the editor from # starting. if subargs and subargs[0] == "config" and ("-e" in subargs or "--edit" in subargs): from beets.ui.commands import config_edit return config_edit() subcommands, plugins, lib = _setup(options, lib) parser.add_subcommand(*subcommands) subcommand, suboptions, subargs = parser.parse_subcommand(subargs) subcommand.func(lib, suboptions, subargs) plugins.send("cli_exit", lib=lib)
def write(self, path=None): """Write the item's metadata to a media file. All fields in `_media_fields` are written to disk according to the values on this object. Can raise either a `ReadError` or a `WriteError`. """ if path is None: path = self.path else: path = normpath(path) tags = dict(self) plugins.send('write', item=self, path=path, tags=tags) try: mediafile = MediaFile(syspath(path), id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, UnreadableFileError) as exc: raise ReadError(self.path, exc) mediafile.update(tags) try: mediafile.save() except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. if path == self.path: self.mtime = self.current_mtime() plugins.send('after_write', item=self, path=path)
def _setup(options, lib=None): """Prepare and global state and updates it with command line options. Returns a list of subcommands, a list of plugins, and a library instance. """ # Configure the MusicBrainz API. mb.configure() config = _configure(options) plugins = _load_plugins(config) # Get the default subcommands. from beets.ui.commands import default_commands subcommands = list(default_commands) subcommands.extend(plugins.commands()) if lib is None: lib = _open_library(config) plugins.send("library_opened", lib=lib) library.Item._types.update(plugins.types(library.Item)) library.Album._types.update(plugins.types(library.Album)) return subcommands, plugins, lib
def user_query(session): """A coroutine for interfacing with the user about the tagging process. The coroutine accepts an ImportTask objects. It uses the session's ``choose_match`` method to determine the ``action`` for this task. Depending on the action additional stages are exectuted and the processed task is yielded. It emits the ``import_task_choice`` event for plugins. Plugins have acces to the choice via the ``taks.choice_flag`` property and may choose to change it. """ recent = set() task = None while True: task = yield task if task.should_skip(): continue # Ask the user for a choice. choice = session.choose_match(task) task.set_choice(choice) session.log_choice(task) plugins.send("import_task_choice", session=session, task=task) # As-tracks: transition to singleton workflow. if task.choice_flag is action.TRACKS: # Set up a little pipeline for dealing with the singletons. def emitter(task): for item in task.items: yield ImportTask.item_task(item) yield ImportTask.progress_sentinel(task.toppath, task.paths) ipl = pipeline.Pipeline([emitter(task), item_lookup(session), item_query(session)]) task = pipeline.multiple(ipl.pull()) continue # As albums: group items by albums and create task for each album if task.choice_flag is action.ALBUMS: def emitter(task): yield task ipl = pipeline.Pipeline( [emitter(task), group_albums(session), initial_lookup(session), user_query(session)] ) task = pipeline.multiple(ipl.pull()) continue # Check for duplicates if we have a match (or ASIS). if task.choice_flag in (action.ASIS, action.APPLY): ident = task.chosen_ident() # The "recent" set keeps track of identifiers for recently # imported albums -- those that haven't reached the database # yet. if ident in recent or _duplicate_check(session.lib, task): session.resolve_duplicate(task) session.log_choice(task, True) recent.add(ident)
def set_art(self, path, copy=True): """Sets the album's cover art to the image at the given path. The image is copied (or moved) into place, replacing any existing art. Sends an 'art_set' event with `self` as the sole argument. """ path = bytestring_path(path) oldart = self.artpath artdest = self.art_destination(path) if oldart and samefile(path, oldart): # Art already set. return elif samefile(path, artdest): # Art already in place. self.artpath = path return # Normal operation. if oldart == artdest: util.remove(oldart) artdest = util.unique_path(artdest) if copy: util.copy(path, artdest) else: util.move(path, artdest) self.artpath = artdest plugins.send('art_set', album=self)
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ subcommand, suboptions, subargs = _configure(args) if lib is None: # Open library file. dbpath = config["library"].as_filename() try: lib = library.Library(dbpath, config["directory"].as_filename(), get_path_formats(), get_replacements()) except sqlite3.OperationalError: raise UserError(u"database file {0} could not be opened".format(util.displayable_path(dbpath))) plugins.send("library_opened", lib=lib) log.debug( u"data directory: {0}\n" u"library database: {1}\n" u"library directory: {2}".format( util.displayable_path(config.config_dir()), util.displayable_path(lib.path), util.displayable_path(lib.directory), ) ) # Configure the MusicBrainz API. mb.configure() # Invoke the subcommand. subcommand.func(lib, suboptions, subargs) plugins.send("cli_exit", lib=lib)
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ parser = SubcommandsOptionParser() parser.add_option('-l', '--library', dest='library', help='library database file to use') parser.add_option('-d', '--directory', dest='directory', help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='print debugging information') parser.add_option('-c', '--config', dest='config', help='path to configuration file') parser.add_option('-h', '--help', dest='help', action='store_true', help='how this help message and exit') parser.add_option('--version', dest='version', action='store_true', help=optparse.SUPPRESS_HELP) options, subargs = parser.parse_global_options(args) # Special case for the `config --edit` command: bypass _setup so # that an invalid configuration does not prevent the editor from # starting. if subargs[0] == 'config' and ('-e' in subargs or '--edit' in subargs): from beets.ui.commands import config_edit return config_edit() subcommands, plugins, lib = _setup(options, lib) parser.add_subcommand(*subcommands) subcommand, suboptions, subargs = parser.parse_subcommand(subargs) subcommand.func(lib, suboptions, subargs) plugins.send('cli_exit', lib=lib)
def save(self, event=True): """Writes the library to disk (completing an sqlite transaction). """ self.conn.commit() if event: plugins.send('save', lib=self)
def _setup(options, lib=None): """Prepare and global state and updates it with command line options. Returns a list of subcommands, a list of plugins, and a library instance. """ # Configure the MusicBrainz API. mb.configure() config = _configure(options) plugins = _load_plugins(config) # Temporary: Migrate from 1.0-style configuration. from beets.ui import migrate migrate.automigrate() # Get the default subcommands. from beets.ui.commands import default_commands subcommands = list(default_commands) subcommands.append(migrate.migrate_cmd) subcommands.extend(plugins.commands()) if lib is None: lib = _open_library(config) plugins.send("library_opened", lib=lib) return subcommands, plugins, lib
def test_listener_level2(self): self.config['verbose'] = 2 with helper.capture_log() as logs: plugins.send('dummy_event') self.assertIn(u'dummy: warning listener', logs) self.assertIn(u'dummy: info listener', logs) self.assertIn(u'dummy: debug listener', logs)
def convert_item(dest_dir, keep_new, path_formats, command, ext): while True: item = yield dest = item.destination(basedir=dest_dir, path_formats=path_formats) # When keeping the new file in the library, we first move the # current (pristine) file to the destination. We'll then copy it # back to its old path or transcode it to a new path. if keep_new: original = dest converted = replace_ext(item.path, ext) else: original = item.path dest = replace_ext(dest, ext) converted = dest # Ensure that only one thread tries to create directories at a # time. (The existence check is not atomic with the directory # creation inside this function.) with _fs_lock: util.mkdirall(dest) if os.path.exists(util.syspath(dest)): log.info(u'Skipping {0} (target file exists)'.format( util.displayable_path(item.path) )) continue if keep_new: log.info(u'Moving to {0}'. format(util.displayable_path(original))) util.move(item.path, original) if not should_transcode(item): # No transcoding necessary. log.info(u'Copying {0}'.format(util.displayable_path(item.path))) util.copy(original, converted) else: try: encode(command, original, converted) except subprocess.CalledProcessError: continue # Write tags from the database to the converted file. item.write(path=converted) if keep_new: # If we're keeping the transcoded file, read it again (after # writing) to get new bitrate, duration, etc. item.path = converted item.read() item.store() # Store new path and audio data. if config['convert']['embed']: album = item.get_album() if album and album.artpath: embed_item(item, album.artpath, itempath=converted) plugins.send('after_convert', item=item, dest=dest, keepnew=keep_new)
def user_query(session): """A coroutine for interfacing with the user about the tagging process. lib is the Library to import into and logfile may be a file-like object for logging the import process. The coroutine accepts and yields ImportTask objects. """ recent = set() task = None while True: task = yield task if task.sentinel: continue # Ask the user for a choice. choice = session.choose_match(task) task.set_choice(choice) session.log_choice(task) plugins.send('import_task_choice', session=session, task=task) # As-tracks: transition to singleton workflow. if choice is action.TRACKS: # Set up a little pipeline for dealing with the singletons. def emitter(task): for item in task.items: yield ImportTask.item_task(item) yield ImportTask.progress_sentinel(task.toppath, task.paths) ipl = pipeline.Pipeline([ emitter(task), item_lookup(session), item_query(session), ]) task = pipeline.multiple(ipl.pull()) continue # As albums: group items by albums and create task for each album if choice is action.ALBUMS: def emitter(task): yield task ipl = pipeline.Pipeline([ emitter(task), group_albums(session), initial_lookup(session), user_query(session) ]) task = pipeline.multiple(ipl.pull()) continue # Check for duplicates if we have a match (or ASIS). if task.choice_flag in (action.ASIS, action.APPLY): ident = task.chosen_ident() # The "recent" set keeps track of identifiers for recently # imported albums -- those that haven't reached the database # yet. if ident in recent or _duplicate_check(session.lib, task): session.resolve_duplicate(task) session.log_choice(task, True) recent.add(ident)
def albums_for_id(album_id): """Get a list of albums for an ID.""" candidates = [album_for_mbid(album_id)] plugin_albums = plugins.album_for_id(album_id) for a in plugin_albums: plugins.send('albuminfo_received', info=a) candidates.extend(plugin_albums) return filter(None, candidates)
def tracks_for_id(track_id): """Get a list of tracks for an ID.""" candidates = [track_for_mbid(track_id)] plugin_tracks = plugins.track_for_id(track_id) for t in plugin_tracks: plugins.send('trackinfo_received', info=t) candidates.extend(plugin_tracks) return filter(None, candidates)
def manipulate_files(session): """A coroutine (pipeline stage) that performs necessary file manipulations *after* items have been added to the library. """ task = None while True: task = yield task if task.should_skip(): continue # Remove duplicate files marked for deletion. if task.remove_duplicates: for duplicate_path in task.duplicate_paths: log.debug(u'deleting replaced duplicate %s' % util.displayable_path(duplicate_path)) util.remove(duplicate_path) util.prune_dirs(os.path.dirname(duplicate_path), session.lib.directory) # Move/copy/write files. items = task.imported_items() # Save the original paths of all items for deletion and pruning # in the next step (finalization). task.old_paths = [item.path for item in items] for item in items: if config['import']['move']: # Just move the file. item.move(False) elif config['import']['copy']: # If it's a reimport, move in-library files and copy # out-of-library files. Otherwise, copy and keep track # of the old path. old_path = item.path if task.replaced_items[item]: # This is a reimport. Move in-library files and copy # out-of-library files. if session.lib.directory in util.ancestry(old_path): item.move(False) # We moved the item, so remove the # now-nonexistent file from old_paths. task.old_paths.remove(old_path) else: item.move(True) else: # A normal import. Just copy files and keep track of # old paths. item.move(True) if config['import']['write'] and task.should_write_tags(): item.try_write() # Save new paths. with session.lib.transaction(): for item in items: item.store() # Plugin event. plugins.send('import_task_files', session=session, task=task)
def import_files(lib, paths, copy, write, autot, logpath, art, threaded, color, delete, quiet, resume, quiet_fallback): """Import the files in the given list of paths, tagging each leaf directory as an album. If copy, then the files are copied into the library folder. If write, then new metadata is written to the files themselves. If not autot, then just import the files without attempting to tag. If logpath is provided, then untaggable albums will be logged there. If art, then attempt to download cover art for each album. If threaded, then accelerate autotagging imports by running them in multiple threads. If color, then ANSI-colorize some terminal output. If delete, then old files are deleted when they are copied. If quiet, then the user is never prompted for input; instead, the tagger just skips anything it is not confident about. resume indicates whether interrupted imports can be resumed and is either a boolean or None. quiet_fallback should be either CHOICE_ASIS or CHOICE_SKIP and indicates what should happen in quiet mode when the recommendation is not strong. """ # Open the log. if logpath: logfile = open(logpath, 'w') else: logfile = None # Never ask for input in quiet mode. if resume is None and quiet: resume = False # Perform the import. if autot: # Autotag. Set up the pipeline. pl = pipeline.Pipeline([ read_albums(paths, resume), initial_lookup(), user_query(lib, logfile, color, quiet, quiet_fallback), apply_choices(lib, copy, write, art, delete, resume is not False), ]) # Run the pipeline. try: if threaded: pl.run_parallel(QUEUE_SIZE) else: pl.run_sequential() except ImportAbort: # User aborted operation. Silently stop. pass else: # Simple import without autotagging. Always sequential. simple_import(lib, paths, copy, delete, resume) # If we were logging, close the file. if logfile: logfile.close() # Emit event. plugins.send('import', lib=lib, paths=paths)
def tracks_for_id(track_id): """Get a list of tracks for an ID.""" t = track_for_mbid(track_id) if t: yield t for t in plugins.track_for_id(track_id): if t: plugins.send(u'trackinfo_received', info=t) yield t
def albums_for_id(album_id): """Get a list of albums for an ID.""" a = album_for_mbid(album_id) if a: yield a for a in plugins.album_for_id(album_id): if a: plugins.send(u'albuminfo_received', info=a) yield a
def track_for_mbid(recording_id): """Get a TrackInfo object for a MusicBrainz recording ID. Return None if the ID is not found. """ try: track = mb.track_for_id(recording_id) if track: plugins.send('trackinfo_received', info=track) return track except mb.MusicBrainzAPIError as exc: exc.log(log)
def album_for_mbid(release_id): """Get an AlbumInfo object for a MusicBrainz release ID. Return None if the ID is not found. """ try: album = mb.album_for_id(release_id) if album: plugins.send('albuminfo_received', info=album) return album except mb.MusicBrainzAPIError as exc: exc.log(log)
def write(self): """Writes the item's metadata to the associated file. """ f = MediaFile(syspath(self.path)) plugins.send('write', item=self, mf=f) for key in ITEM_KEYS_WRITABLE: setattr(f, key, getattr(self, key)) f.save() # The file has a new mtime. self.mtime = self.current_mtime()
def user_query(session): """A coroutine for interfacing with the user about the tagging process. lib is the Library to import into and logfile may be a file-like object for logging the import process. The coroutine accepts and yields ImportTask objects. """ recent = set() task = None while True: task = yield task if task.sentinel: continue # Ask the user for a choice. choice = session.choose_match(task) task.set_choice(choice) session.log_choice(task) plugins.send('import_task_choice', session=session, task=task) # As-tracks: transition to singleton workflow. if choice is action.TRACKS: # Set up a little pipeline for dealing with the singletons. item_tasks = [] def emitter(): for item in task.items: yield ImportTask.item_task(item) yield ImportTask.progress_sentinel(task.toppath, task.paths) def collector(): while True: item_task = yield item_tasks.append(item_task) ipl = pipeline.Pipeline((emitter(), item_lookup(session), item_query(session), collector())) ipl.run_sequential() task = pipeline.multiple(item_tasks) continue # Check for duplicates if we have a match (or ASIS). if task.choice_flag in (action.ASIS, action.APPLY): ident = task.chosen_ident() # The "recent" set keeps track of identifiers for recently # imported albums -- those that haven't reached the database # yet. task.duplicates = _duplicate_check(session.lib, task) if ident in recent: # TODO: Somehow manage duplicate hooks for recents pass plugins.send('import_task_duplicate', session=session, task=task) if task.duplicates: session.resolve_duplicate(task) session.log_choice(task, True) recent.add(ident)
def item_lookup(session): """A coroutine used to perform the initial MusicBrainz lookup for an item task. """ task = None while True: task = yield task if task.sentinel: continue plugins.send('import_task_start', session=session, task=task) task.set_item_candidates(*autotag.tag_item(task.item))
def initial_lookup(session): """A coroutine for performing the initial MusicBrainz lookup for an album. It accepts lists of Items and yields (items, cur_artist, cur_album, candidates, rec) tuples. If no match is found, all of the yielded parameters (except items) are None. """ task = None while True: task = yield task if task.sentinel: continue plugins.send('import_task_start', session=session, task=task) log.debug('Looking up: %s' % displayable_path(task.paths)) task.set_candidates(*autotag.tag_album( task.items, config['import']['timid'].get(bool)))
def test_hook_bytes_interpolation(self): temporary_paths = [ get_temporary_path().encode('utf-8') for i in range(self.TEST_HOOK_COUNT) ] for index, path in enumerate(temporary_paths): self._add_hook(f'test_bytes_event_{index}', 'touch "{path}"') self.load_plugins('hook') for index, path in enumerate(temporary_paths): plugins.send(f'test_bytes_event_{index}', path=path) for path in temporary_paths: self.assertTrue(os.path.isfile(path)) os.remove(path)
def test_hook_no_arguments(self): temporary_paths = [ get_temporary_path() for i in range(self.TEST_HOOK_COUNT) ] for index, path in enumerate(temporary_paths): self._add_hook(f'test_no_argument_event_{index}', f'touch "{path}"') self.load_plugins('hook') for index in range(len(temporary_paths)): plugins.send(f'test_no_argument_event_{index}') for path in temporary_paths: self.assertTrue(os.path.isfile(path)) os.remove(path)
def finalize(session): """A coroutine that finishes up importer tasks. In particular, the coroutine sends plugin events, deletes old files, and saves progress. This is a "terminal" coroutine (it yields None). """ while True: task = yield if task.should_skip(): if _resume(): task.save_progress() if config['import']['incremental']: task.save_history() continue items = task.imported_items() # Announce that we've added an album. if task.is_album: album = session.lib.get_album(task.album_id) plugins.send('album_imported', lib=session.lib, album=album) else: for item in items: plugins.send('item_imported', lib=session.lib, item=item) # When copying and deleting originals, delete old files. if config['import']['copy'] and config['import']['delete']: new_paths = [os.path.realpath(item.path) for item in items] for old_path in task.old_paths: # Only delete files that were actually copied. if old_path not in new_paths: util.remove(syspath(old_path), False) task.prune(old_path) # When moving, prune empty directories containing the original # files. elif config['import']['move']: for old_path in task.old_paths: task.prune(old_path) # Update progress. if _resume(): task.save_progress() if config['import']['incremental']: task.save_history()
def import_files(lib, paths, query): """Import the files in the given list of paths or matching the query. """ # Check the user-specified directories. for path in paths: fullpath = syspath(normpath(path)) if not config['import']['singletons'] and not os.path.isdir(fullpath): raise ui.UserError(u'not a directory: {0}'.format( displayable_path(path))) elif config['import']['singletons'] and not os.path.exists(fullpath): raise ui.UserError(u'no such file: {0}'.format( displayable_path(path))) # Check parameter consistency. if config['import']['quiet'] and config['import']['timid']: raise ui.UserError("can't be both quiet and timid") # Open the log. if config['import']['log'].get() is not None: logpath = config['import']['log'].as_filename() try: logfile = codecs.open(syspath(logpath), 'a', 'utf8') except IOError: raise ui.UserError(u"could not open log file for writing: %s" % displayable_path(logpath)) print(u'import started', time.asctime(), file=logfile) else: logfile = None # Never ask for input in quiet mode. if config['import']['resume'].get() == 'ask' and \ config['import']['quiet']: config['import']['resume'] = False session = TerminalImportSession(lib, logfile, paths, query) try: session.run() finally: # If we were logging, close the file. if logfile: print(u'', file=logfile) logfile.close() # Emit event. plugins.send('import', lib=lib, paths=paths)
def test_hook_argument_substitution(self): temporary_paths = [ get_temporary_path() for i in range(self.TEST_HOOK_COUNT) ] for index, path in enumerate(temporary_paths): self._add_hook('test_argument_event_{0}'.format(index), 'touch "{path}"') self.load_plugins('hook') for index, path in enumerate(temporary_paths): plugins.send('test_argument_event_{0}'.format(index), path=path) for path in temporary_paths: self.assertTrue(os.path.isfile(path)) os.remove(path)
def apply_choices(session, task): """A coroutine for applying changes to albums and singletons during the autotag process. """ if task.skip: return # Change metadata. if task.apply: task.apply_metadata() plugins.send('import_task_apply', session=session, task=task) # Infer album-level fields. if task.is_album: task.infer_album_fields() task.add(session.lib)
def finalize(config): """A coroutine that finishes up importer tasks. In particular, the coroutine sends plugin events, deletes old files, and saves progress. This is a "terminal" coroutine (it yields None). """ lib = _reopen_lib(config.lib) while True: task = yield if task.should_skip(): if config.resume is not False: task.save_progress() if config.incremental: task.save_history() continue items = [i for i in task.items if i] if task.is_album else [task.item] # Announce that we've added an album. if task.is_album: album = lib.get_album(task.album_id) plugins.send('album_imported', lib=lib, album=album, config=config) else: for item in items: plugins.send('item_imported', lib=lib, item=item, config=config) # Finally, delete old files. if config.copy and config.delete: new_paths = [os.path.realpath(item.path) for item in items] for old_path in task.old_paths: # Only delete files that were actually copied. if old_path not in new_paths: os.remove(syspath(old_path)) # Clean up directory if it is emptied. if task.toppath: util.prune_dirs(os.path.dirname(old_path), task.toppath) # Update progress. if config.resume is not False: task.save_progress() if config.incremental: task.save_history()
def write(self, path=None, tags=None): """Write the item's metadata to a media file. All fields in `_media_fields` are written to disk according to the values on this object. `path` is the path of the mediafile to write the data to. It defaults to the item's path. `tags` is a dictionary of additional metadata the should be written to the file. (These tags need not be in `_media_fields`.) Can raise either a `ReadError` or a `WriteError`. """ if path is None: path = self.path else: path = normpath(path) # Get the data to write to the file. item_tags = dict(self) item_tags = {k: v for k, v in item_tags.items() if k in self._media_fields} # Only write media fields. if tags is not None: item_tags.update(tags) plugins.send('write', item=self, path=path, tags=item_tags) # Open the file. try: mediafile = MediaFile(syspath(path), id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, UnreadableFileError) as exc: raise ReadError(self.path, exc) # Write the tags to the file. mediafile.update(item_tags) try: mediafile.save() except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. if path == self.path: self.mtime = self.current_mtime() plugins.send('after_write', item=self, path=path)
def explicits(lib, opts, args): args = ui.decargs(args) items = lib.items(args) results = self.spotify._match_library_tracks(lib, args) if results: for item, track in zip(items, results): if track['explicit']: title = track['name'] album = track['album']['name'] artist = track['artists'][0]['name'] tracknum = track['track_number'] url = track['external_urls']['spotify'] plugins.send("spotify_explicit_track", lib=lib, track=track, item=item) print('{} - {} - {} - {} - {}'.format( album, tracknum, artist, title, url))
def move_file(self, dest, copy=False): """Moves or copies the item's file, updating the path value if the move succeeds. If a file exists at ``dest``, then it is slightly modified to be unique. """ if not util.samefile(self.path, dest): dest = util.unique_path(dest) if copy: util.copy(self.path, dest) plugins.send("item_copied", item=self, source=self.path, destination=dest) else: util.move(self.path, dest) plugins.send("item_moved", item=self, source=self.path, destination=dest) # Either copying or moving succeeded, so update the stored path. self.path = dest
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ parser = SubcommandsOptionParser() parser.add_format_option(flags=('--format-item',), target=library.Item) parser.add_format_option(flags=('--format-album',), target=library.Album) parser.add_option('-l', '--library', dest='library', help=u'library database file to use') parser.add_option('-d', '--directory', dest='directory', help=u"destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='count', help=u'log more details (use twice for even more)') parser.add_option('-c', '--config', dest='config', help=u'path to configuration file') parser.add_option('-p', '--plugins', dest='plugins', help=u'a comma-separated list of plugins to load') parser.add_option('-h', '--help', dest='help', action='store_true', help=u'show this help message and exit') parser.add_option('--version', dest='version', action='store_true', help=optparse.SUPPRESS_HELP) options, subargs = parser.parse_global_options(args) # Special case for the `config --edit` command: bypass _setup so # that an invalid configuration does not prevent the editor from # starting. if subargs and subargs[0] == 'config' \ and ('-e' in subargs or '--edit' in subargs): from beets.ui.commands import config_edit return config_edit() test_lib = bool(lib) subcommands, plugins, lib = _setup(options, lib) parser.add_subcommand(*subcommands) subcommand, suboptions, subargs = parser.parse_subcommand(subargs) subcommand.func(lib, suboptions, subargs) plugins.send('cli_exit', lib=lib) if not test_lib: # Clean up the library unless it came from the test harness. lib._close()
def initial_lookup(config): """A coroutine for performing the initial MusicBrainz lookup for an album. It accepts lists of Items and yields (items, cur_artist, cur_album, candidates, rec) tuples. If no match is found, all of the yielded parameters (except items) are None. """ task = None while True: task = yield task if task.sentinel: continue plugins.send('import_task_start', task=task, config=config) log.debug('Looking up: %s' % task.path) try: task.set_match(*autotag.tag_album(task.items, config.timid)) except autotag.AutotagError: task.set_null_match()
def test_hook_event_substitution(self): temporary_directory = tempfile._get_default_tempdir() event_names = ['test_event_event_{0}'.format(i) for i in range(self.TEST_HOOK_COUNT)] for event in event_names: self._add_hook(event, 'touch "{0}/{{event}}"'.format(temporary_directory)) self.load_plugins('hook') for event in event_names: plugins.send(event) for event in event_names: path = os.path.join(temporary_directory, event) self.assertTrue(os.path.isfile(path)) os.remove(path)
def run(self): """Run the import task. """ self.set_config(config['import']) # Set up the pipeline. if self.query is None: stages = [read_tasks(self)] else: stages = [query_tasks(self)] if self.config['pretend']: # Only log the imported files and end the pipeline stages += [log_files(self)] else: if self.config['group_albums'] and \ not self.config['singletons']: # Split directory tasks into one task for each album stages += [group_albums(self)] if self.config['autotag']: # FIXME We should also resolve duplicates when not # autotagging. This is currently handled in `user_query` stages += [lookup_candidates(self), user_query(self)] else: stages += [import_asis(self)] stages += [apply_choices(self)] for stage_func in plugins.import_stages(): stages.append(plugin_stage(self, stage_func)) stages += [manipulate_files(self)] pl = pipeline.Pipeline(stages) # Run the pipeline. plugins.send('import_begin', session=self) try: if config['threaded']: pl.run_parallel(QUEUE_SIZE) else: pl.run_sequential() except ImportAbort: # User aborted operation. Silently stop. pass
def _raw_main(args): """A helper function for `main` without top-level exception handling. """ subcommand, suboptions, subargs = _configure(args) # Open library file. dbpath = config['library'].as_filename() try: lib = library.Library( dbpath, config['directory'].as_filename(), get_path_formats(), get_replacements(), ) except sqlite3.OperationalError: raise UserError(u"database file {0} could not be opened".format( util.displayable_path(dbpath) )) plugins.send("library_opened", lib=lib) # Configure the logger. if config['verbose'].get(bool): log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) log.debug(u'data directory: {0}\n' u'library database: {1}\n' u'library directory: {2}' .format( util.displayable_path(config.config_dir()), util.displayable_path(lib.path), util.displayable_path(lib.directory), ) ) # Configure the MusicBrainz API. mb.configure() # Invoke the subcommand. subcommand.func(lib, suboptions, subargs) plugins.send('cli_exit', lib=lib)
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ parser = SubcommandsOptionParser() options, subargs = parser.parse_global_options(args) subcommands, plugins, lib = _setup(options, lib) parser.add_subcommand(*subcommands) if options.version: from beets.ui import commands commands.version_cmd.func(lib, None, None) else: subcommand, suboptions, subargs = parser.parse_subcommand(subargs) subcommand.func(lib, suboptions, subargs) plugins.send('cli_exit', lib=lib)
def manipulate_files(self, move=False, copy=False, write=False, session=None): items = self.imported_items() # Save the original paths of all items for deletion and pruning # in the next step (finalization). self.old_paths = [item.path for item in items] for item in items: if session.config['move']: # Just move the file. item.move(False) elif session.config['copy']: # If it's a reimport, move in-library files and copy # out-of-library files. Otherwise, copy and keep track # of the old path. old_path = item.path if self.replaced_items[item]: # This is a reimport. Move in-library files and copy # out-of-library files. if session.lib.directory in util.ancestry(old_path): item.move(False) # We moved the item, so remove the # now-nonexistent file from old_paths. self.old_paths.remove(old_path) else: item.move(True) else: # A normal import. Just copy files and keep track of # old paths. item.move(True) if session.config['write'] and self.apply: item.try_write() with session.lib.transaction(): for item in self.imported_items(): item.store() plugins.send('import_task_files', session=session, task=self)
def _load_plugins(config): """Load the plugins specified in the configuration. """ paths = config['pluginpath'].as_str_seq(split=False) paths = [util.normpath(p) for p in paths] log.debug(u'plugin paths: {0}', util.displayable_path(paths)) # On Python 3, the search paths need to be unicode. paths = [util.py3_path(p) for p in paths] # Extend the `beetsplug` package to include the plugin paths. import beetsplug beetsplug.__path__ = paths + beetsplug.__path__ # For backwards compatibility, also support plugin paths that # *contain* a `beetsplug` package. sys.path += paths plugins.load_plugins(config['plugins'].as_str_seq()) plugins.send("pluginload") return plugins
def test_events_called(self, mock_inspect, mock_find_plugins): mock_inspect.getargspec.return_value = None class DummyPlugin(plugins.BeetsPlugin): def __init__(self): super(DummyPlugin, self).__init__() self.foo = Mock(__name__=b'foo') self.register_listener('event_foo', self.foo) self.bar = Mock(__name__=b'bar') self.register_listener('event_bar', self.bar) d = DummyPlugin() mock_find_plugins.return_value = d, plugins.send('event') d.foo.assert_has_calls([]) d.bar.assert_has_calls([]) plugins.send('event_foo', var="tagada") d.foo.assert_called_once_with(var="tagada") d.bar.assert_has_calls([])
def item_candidates(item, artist, title): """Search for item matches. ``item`` is the Item to be matched. ``artist`` and ``title`` are strings and either reflect the item or are specified by the user. """ out = [] # MusicBrainz candidates. if artist and title: try: out.extend(mb.match_track(artist, title)) except mb.MusicBrainzAPIError as exc: exc.log(log) # Plugin candidates. out.extend(plugins.item_candidates(item, artist, title)) # Notify subscribed plugins about fetched track info for i in out: plugins.send('trackinfo_received', info=i) return out
def write(self): """Write the item's metadata to the associated file. Can raise either a `ReadError` or a `WriteError`. """ try: f = MediaFile(syspath(self.path)) except (OSError, IOError) as exc: raise ReadError(self.path, exc) plugins.send('write', item=self) for key in ITEM_KEYS_WRITABLE: setattr(f, key, self[key]) try: f.save(id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. self.mtime = self.current_mtime() plugins.send('after_write', item=self)
def write(self): """Writes the item's metadata to the associated file. """ plugins.send('write', item=self) try: f = MediaFile(syspath(self.path)) except (OSError, IOError) as exc: raise util.FilesystemError(exc, 'read', (self.path, ), traceback.format_exc()) for key in ITEM_KEYS_WRITABLE: setattr(f, key, self[key]) try: f.save(id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError) as exc: raise util.FilesystemError(exc, 'write', (self.path, ), traceback.format_exc()) # The file has a new mtime. self.mtime = self.current_mtime()
def remove(self, delete=False, with_album=True): """Removes the item. If `delete`, then the associated file is removed from disk. If `with_album`, then the item's album (if any) is removed if it the item was the last in the album. """ super(Item, self).remove() # Remove the album if it is empty. if with_album: album = self.get_album() if album and not album.items(): album.remove(delete, False) # Send a 'item_removed' signal to plugins plugins.send('item_removed', item=self) # Delete the associated file. if delete: util.remove(self.path) util.prune_dirs(os.path.dirname(self.path), self._db.directory) self._db._memotable = {}
def item_candidates(item, artist, title): """Search for item matches. ``item`` is the Item to be matched. ``artist`` and ``title`` are strings and either reflect the item or are specified by the user. """ # MusicBrainz candidates. if artist and title: try: for candidate in mb.match_track(artist, title): yield TrackAlbumTuple(candidate, None) except mb.MusicBrainzAPIError as exc: exc.log(log) # Plugin candidates. for candidate in plugins.item_candidates(item, artist, title): # allow (track_info, album_info) tuples plugins.send(u'trackinfo_received', info=candidate) if isinstance(candidate, TrackAlbumTuple): yield candidate else: yield TrackAlbumTuple(candidate, None)
def item_query(session): """A coroutine that queries the user for input on single-item lookups. """ task = None recent = set() while True: task = yield task if task.sentinel: continue choice = session.choose_item(task) task.set_choice(choice) session.log_choice(task) plugins.send('import_task_choice', session=session, task=task) # Duplicate check. if task.choice_flag in (action.ASIS, action.APPLY): ident = task.chosen_ident() if ident in recent or _item_duplicate_check(session.lib, task): session.resolve_duplicate(task) session.log_choice(task, True) recent.add(ident)
def match_benchmark(lib, prof, query=None, album_id=None): # If no album ID is provided, we'll match against a suitably huge # album. if not album_id: album_id = '9c5c043e-bc69-4edb-81a4-1aaf9c81e6dc' # Get an album from the library to use as the source for the match. items = lib.albums(query).get().items() # Ensure fingerprinting is invoked (if enabled). plugins.send('import_task_start', task=importer.ImportTask(None, None, items), session=importer.ImportSession(lib, None, None, None)) # Run the match. def _run_match(): match.tag_album(items, search_id=album_id) if prof: cProfile.runctx('_run_match()', {}, {'_run_match': _run_match}, 'match.prof') else: interval = timeit.timeit(_run_match, number=1) print('match duration:', interval)
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ parser = SubcommandsOptionParser() parser.add_format_option(flags=('--format-item',), target=library.Item) parser.add_format_option(flags=('--format-album',), target=library.Album) parser.add_option('-l', '--library', dest='library', help='library database file to use') parser.add_option('-d', '--directory', dest='directory', help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='count', help='print debugging information') parser.add_option('-c', '--config', dest='config', help='path to configuration file') parser.add_option('-h', '--help', dest='help', action='store_true', help='how this help message and exit') parser.add_option('--version', dest='version', action='store_true', help=optparse.SUPPRESS_HELP) options, subargs = parser.parse_global_options(args) # Special case for the `config --edit` command: bypass _setup so # that an invalid configuration does not prevent the editor from # starting. if subargs and subargs[0] == 'config' \ and ('-e' in subargs or '--edit' in subargs): from beets.ui.commands import config_edit return config_edit() subcommands, plugins, lib = _setup(options, lib) parser.add_subcommand(*subcommands) subcommand, suboptions, subargs = parser.parse_subcommand(subargs) subcommand.func(lib, suboptions, subargs) plugins.send('cli_exit', lib=lib)
def _setup(options, lib=None): """Prepare and global state and updates it with command line options. Returns a list of subcommands, a list of plugins, and a library instance. """ # Configure the MusicBrainz API. mb.configure() config = _configure(options) plugins = _load_plugins(options, config) # Add types and queries defined by plugins. plugin_types_album = plugins.types(library.Album) library.Album._types.update(plugin_types_album) item_types = plugin_types_album.copy() item_types.update(library.Item._types) item_types.update(plugins.types(library.Item)) library.Item._types = item_types library.Item._queries.update(plugins.named_queries(library.Item)) library.Album._queries.update(plugins.named_queries(library.Album)) plugins.send("pluginload") # Get the default subcommands. from beets.ui.commands import default_commands subcommands = list(default_commands) subcommands.extend(plugins.commands()) if lib is None: lib = _open_library(config) plugins.send("library_opened", lib=lib) return subcommands, plugins, lib