def convert(self, item): """Converts an item in an unsupported media format to ogg. Config pending. This is stolen from Jakob Schnitzers convert plugin. """ fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) source = item.path log.info(u'echonest: encoding {0} to {1}'.format( util.displayable_path(source), util.displayable_path(dest), )) # Build up the FFmpeg command line. # FIXME: use avconv? command = u'ffmpeg -i $source -y -acodec libvorbis -vn -aq 2 $dest' opts = [] for arg in command.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: subprocess.check_call(opts, close_fds=True, stderr=DEVNULL) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: encode failed: {0}'.format(exc)) util.remove(dest) return log.info(u'echonest: finished encoding {0}'.format( util.displayable_path(source)) ) return dest
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. """ 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
def encode(source, dest): log.info('Started encoding ' + source) temp_dest = dest + '~' source_ext = os.path.splitext(source)[1].lower() if source_ext == '.flac': decode = Popen([conf['flac'], '-c', '-d', '-s', source], stdout=PIPE) encode = Popen([conf['lame']] + conf['opts'] + ['-', temp_dest], stdin=decode.stdout, stderr=DEVNULL) decode.stdout.close() encode.communicate() elif source_ext == '.mp3': encode = Popen([conf['lame']] + conf['opts'] + ['--mp3input'] + [source, temp_dest], close_fds=True, stderr=DEVNULL) encode.communicate() else: log.error('Only converting from FLAC or MP3 implemented') return if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files log.info('Encoding {0} failed. Cleaning up...'.format(source)) util.remove(temp_dest) util.prune_dirs(os.path.dirname(temp_dest)) return shutil.move(temp_dest, dest) log.info('Finished encoding ' + source)
def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the library. """ fmt = self.config['format'].as_str().lower() if should_transcode(item, fmt): command, ext = get_format() # Create a temporary file for the conversion. tmpdir = self.config['tmpdir'].get() if tmpdir: tmpdir = util.py3_path(util.bytestring_path(tmpdir)) fd, dest = tempfile.mkstemp(util.py3_path(b'.' + ext), dir=tmpdir) os.close(fd) dest = util.bytestring_path(dest) _temp_files.append(dest) # Delete the transcode later. # Convert. try: self.encode(command, item.path, dest) except subprocess.CalledProcessError: return # Change the newly-imported database entry to point to the # converted file. source_path = item.path item.path = dest item.write() item.read() # Load new audio information data. item.store() if self.config['delete_originals']: self._log.info(u'Removing original file {0}', source_path) util.remove(source_path, False)
def encode(source, dest): quiet = config['convert']['quiet'].get() if not quiet: log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command, _ = get_format() opts = [] for arg in command: opts.append(Template(arg).safe_substitute({ 'source': source, 'dest': dest, })) log.debug(u'convert: executing: {0}'.format( u' '.join(pipes.quote(o.decode('utf8', 'ignore')) for o in opts) )) encode = Popen(opts, close_fds=True, stderr=DEVNULL) encode.wait() if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return if not quiet: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)) )
def encode(source, dest): log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command = get_command() opts = [] for arg in command: arg = arg.encode('utf-8') opts.append(Template(arg).substitute({ 'source': source, 'dest': dest })) encode = Popen(opts, close_fds=True, stderr=DEVNULL) encode.wait() if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return log.info(u'Finished encoding {0}'.format(util.displayable_path(source)))
def prepare_upload(self, item): """Truncate and convert an item's audio file so it can be uploaded to echonest. Return a ``(source, tmp)`` tuple where `source` is the path to the file to be uploaded and `tmp` is a temporary file to be deleted after the upload or `None`. If conversion or truncation fails, return `None`. """ source = item.path tmp = None if item.format not in ALLOWED_FORMATS: if self.config['convert']: tmp = source = self.convert(source) if not tmp: return if os.stat(source).st_size > UPLOAD_MAX_SIZE: if self.config['truncate']: source = self.truncate(source) if tmp is not None: util.remove(tmp) tmp = source else: return if source: return source, tmp
def truncate(self, item): """Truncates an item to a size less than UPLOAD_MAX_SIZE.""" fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) source = item.path log.info(u'echonest: truncating {0} to {1}'.format( util.displayable_path(source), util.displayable_path(dest), )) command = u'ffmpeg -t 300 -i $source -y -acodec copy $dest' opts = [] for arg in command.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: truncate failed: {0}'.format(exc)) util.remove(dest) return log.info(u'echonest: truncate encoding {0}'.format( util.displayable_path(source)) ) return dest
def convert(self, source): """Converts an item in an unsupported media format to ogg. Config pending. This is stolen from Jakob Schnitzers convert plugin. """ fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) self._log.info(u'encoding {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) opts = [] for arg in CONVERT_COMMAND.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: self._log.debug(u'encode failed: {0}', exc) util.remove(dest) return self._log.info(u'finished encoding {0}', util.displayable_path(source)) return dest
def test_add_nonexistent(self): item = self.add_external_track('myexternal') path = self.get_path(item) util.remove(path) self.runcli('alt', 'update', 'myexternal') self.assertIsFile(self.get_path(item))
def encode(source, dest): quiet = config['convert']['quiet'].get() if not quiet: log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command, _ = get_format() opts = [] for arg in command: opts.append( Template(arg).safe_substitute({ 'source': source, 'dest': dest, })) log.debug(u'convert: executing: {0}'.format(u' '.join( pipes.quote(o.decode('utf8', 'ignore')) for o in opts))) encode = Popen(opts, close_fds=True, stderr=DEVNULL) encode.wait() if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...'.format( util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return if not quiet: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)))
def convert(self, source): """Converts an item in an unsupported media format to ogg. Config pending. This is stolen from Jakob Schnitzers convert plugin. """ fd, dest = tempfile.mkstemp(b'.ogg') os.close(fd) self._log.info(u'encoding {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) opts = [] for arg in CONVERT_COMMAND.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: self._log.debug(u'encode failed: {0}', exc) util.remove(dest) return self._log.info(u'finished encoding {0}', util.displayable_path(source)) return dest
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 truncate(self, source): """Truncates an item to a size less than UPLOAD_MAX_SIZE.""" fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) log.info(u'echonest: truncating {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) opts = [] for arg in TRUNCATE_COMMAND.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: truncate failed: {0}', exc) util.remove(dest) return log.info(u'echonest: truncate encoding {0}', util.displayable_path(source)) return dest
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.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 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 remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) log.debug("removing %i old duplicated items" % len(duplicate_items)) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): log.debug(u"deleting duplicate %s" % util.displayable_path(item.path)) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), lib.directory)
def encode(self, command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ # The paths and arguments must be bytes. assert isinstance(command, bytes) assert isinstance(source, bytes) assert isinstance(dest, bytes) quiet = self.config['quiet'].get(bool) if not quiet and not pretend: self._log.info('Encoding {0}', util.displayable_path(source)) command = command.decode(arg_encoding(), 'surrogateescape') source = decode_commandline_path(source) dest = decode_commandline_path(dest) # Substitute $source and $dest in the argument list. args = shlex.split(command) encode_cmd = [] for i, arg in enumerate(args): args[i] = Template(arg).safe_substitute({ 'source': source, 'dest': dest, }) encode_cmd.append(args[i].encode(util.arg_encoding())) if pretend: self._log.info('{0}', ' '.join(ui.decargs(args))) return try: util.command_output(encode_cmd) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files self._log.info('Encoding {0} failed. Cleaning up...', util.displayable_path(source)) self._log.debug('Command {0} exited with status {1}: {2}', args, exc.returncode, exc.output) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( "convert: couldn't invoke '{}': {}".format( ' '.join(ui.decargs(args)), exc ) ) if not quiet and not pretend: self._log.info('Finished encoding {0}', util.displayable_path(source))
def remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) log.debug('removing %i old duplicated items' % len(duplicate_items)) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): log.debug(u'deleting duplicate %s' % util.displayable_path(item.path)) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), lib.directory)
def test_fetch_art_if_imported_file_deleted(self): # See #1126. Test the following scenario: # - Album art imported, `album.artpath` set. # - Imported album art file subsequently deleted (by user or other # program). # `fetchart` should import album art again instead of printing the # message "<album> has album art". self._fetch_art(True) util.remove(self.album.artpath) self.plugin.batch_fetch_art(self.lib, self.lib.albums(), force=False) self.assertExists(self.album.artpath)
def remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) log.debug(u'removing {0} old duplicated items' .format(len(duplicate_items))) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): log.debug(u'deleting duplicate {0}' .format(util.displayable_path(item.path))) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), lib.directory)
def analyze(self, item): """Upload the item to the EchoNest for analysis. May require to convert the item to a supported media format. """ prepared = self.prepare_upload(item) if not prepared: self._log.debug(u'could not prepare file for upload') return source, tmp = prepared self._log.info(u'uploading file, please be patient') track = self._echofun(pyechonest.track.track_from_filename, filename=source) if tmp is not None: util.remove(tmp) if not track: self._log.debug(u'failed to upload file') return # Sometimes we have a track but no song. I guess this happens for # new / unverified songs. We need to "extract" the audio_summary # from the track object manually. I don't know why the # pyechonest API handles tracks (merge audio_summary to __dict__) # and songs (keep audio_summary in an extra attribute) # differently. # Maybe a patch for pyechonest could help? # First get the (limited) metadata from the track in case # there's no associated song. from_track = {} for key in ATTRIBUTES: try: from_track[key] = getattr(track, key) except AttributeError: pass from_track['duration'] = track.duration # Try to look up a song for the full metadata. try: song_id = track.song_id except AttributeError: return from_track songs = self._echofun(pyechonest.song.profile, ids=[song_id], track_ids=[track.id], buckets=['audio_summary']) if songs: pick = self._pick_song(songs, item) if pick: return self._flatten_song(pick) return from_track # Fall back to track metadata.
def encode(source, dest): log.info(u'Started encoding {0}'.format(util.displayable_path(source))) encode = Popen([conf['ffmpeg']] + ['-i', source] + conf['opts'] + [dest], close_fds=True, stderr=DEVNULL) encode.wait() if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...'.format(source)) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return log.info(u'Finished encoding {0}'.format(util.displayable_path(source)))
def encode(self, command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ # The paths and arguments must be bytes. assert isinstance(command, bytes) assert isinstance(source, bytes) assert isinstance(dest, bytes) quiet = self.config['quiet'].get(bool) if not quiet and not pretend: self._log.info(u'Encoding {0}', util.displayable_path(source)) # Substitute $source and $dest in the argument list. args = shlex.split(command) for i, arg in enumerate(args): args[i] = Template(arg).safe_substitute({ 'source': source, 'dest': dest, }) if pretend: self._log.info(u' '.join(ui.decargs(args))) return try: util.command_output(args) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files self._log.info(u'Encoding {0} failed. Cleaning up...', util.displayable_path(source)) self._log.debug(u'Command {0} exited with status {1}: {2}', args, exc.returncode, exc.output) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u"convert: couldn't invoke '{0}': {1}".format( u' '.join(ui.decargs(args)), exc ) ) if not quiet and not pretend: self._log.info(u'Finished encoding {0}', util.displayable_path(source))
def encode(self, command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ # The paths and arguments must be bytes. assert isinstance(command, bytes) assert isinstance(source, bytes) assert isinstance(dest, bytes) quiet = self.config['quiet'].get(bool) if not quiet and not pretend: self._log.info(u'Encoding {0}', util.displayable_path(source)) # Substitute $source and $dest in the argument list. args = shlex.split(command) for i, arg in enumerate(args): args[i] = Template(arg).safe_substitute({ b'source': source, b'dest': dest, }) if pretend: self._log.info(' '.join(args)) return try: util.command_output(args) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files self._log.info(u'Encoding {0} failed. Cleaning up...', util.displayable_path(source)) self._log.debug(u'Command {0} exited with status {1}', exc.cmd.decode('utf8', 'ignore'), exc.returncode) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u"convert: could invoke '{0}': {1}".format( ' '.join(args), exc ) ) if not quiet and not pretend: self._log.info(u'Finished encoding {0}', util.displayable_path(source))
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() task.cleanup() continue items = task.imported_items() # 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() task.cleanup() # 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)
def encode(command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ quiet = config['convert']['quiet'].get() if not quiet and not pretend: log.info(u'Encoding {0}'.format(util.displayable_path(source))) if os.name == 'nt': command = Template(command).safe_substitute({ 'source': '"' + source + '"', 'dest': '"' + dest + '"', }) else: command = Template(command).safe_substitute({ 'source': pipes.quote(source), 'dest': pipes.quote(dest), }) log.debug(u'convert: executing: {0}' .format(util.displayable_path(command))) if pretend: log.info(command) return try: util.command_output(command, shell=True) except subprocess.CalledProcessError: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u"convert: could invoke '{0}': {0}".format(command, exc) ) if not quiet and not pretend: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)) )
def encode(command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ quiet = config['convert']['quiet'].get() if not quiet and not pretend: log.info(u'Encoding {0}'.format(util.displayable_path(source))) # Substitute $source and $dest in the argument list. args = shlex.split(command) for i, arg in enumerate(args): args[i] = Template(arg).safe_substitute({ 'source': source, 'dest': dest, }) if pretend: log.info(' '.join(args)) return try: util.command_output(args) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' .format(util.displayable_path(source))) log.debug(u'Command {0} exited with status {1}'.format( exc.cmd.decode('utf8', 'ignore'), exc.returncode, )) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u"convert: could invoke '{0}': {1}".format( ' '.join(args), exc ) ) if not quiet and not pretend: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)) )
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 remove(self, delete=False, with_items=True): """Removes this album and all its associated items from the library. If delete, then the items' files are also deleted from disk, along with any album art. The directories containing the album are also removed (recursively) if empty. Set with_items to False to avoid removing the album's items. """ super(Album, self).remove() # Delete art file. if delete: artpath = self.artpath if artpath: util.remove(artpath) # Remove (and possibly delete) the constituent items. if with_items: for item in self.items(): item.remove(delete, False)
def encode(source, dest): """Encode ``source`` to ``dest`` using the command from ``get_format()``. Raises an ``ui.UserError`` if the command was not found and a ``subprocess.CalledProcessError`` if the command exited with a non-zero status code. """ quiet = config['convert']['quiet'].get() if not quiet: log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command, _ = get_format() opts = [] for arg in command: opts.append(Template(arg).safe_substitute({ 'source': source, 'dest': dest, })) log.debug(u'convert: executing: {0}'.format( u' '.join(pipes.quote(o.decode('utf8', 'ignore')) for o in opts) )) try: util.command_output(opts) except subprocess.CalledProcessError: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u'convert: could invoke ffmpeg: {0}'.format(exc) ) if not quiet: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)) )
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 = task.all_items() # 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: util.remove(syspath(old_path), False) # Clean up directory if it is emptied. if task.toppath: task.prune(old_path) # Update progress. if config.resume is not False: task.save_progress() if config.incremental: task.save_history()
def encode(command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ quiet = config['convert']['quiet'].get() if not quiet and not pretend: log.info(u'Encoding {0}'.format(util.displayable_path(source))) command = Template(command).safe_substitute({ 'source': pipes.quote(source), 'dest': pipes.quote(dest), }) log.debug(u'convert: executing: {0}' .format(util.displayable_path(command))) if pretend: log.info(command) return try: util.command_output(command, shell=True) except subprocess.CalledProcessError: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u"convert: could invoke '{0}': {0}".format(command, exc) ) if not quiet and not pretend: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)) )
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). """ 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 = task.all_items() # Announce that we've added an album. if task.is_album: album = config.lib.get_album(task.album_id) plugins.send('album_imported', lib=config.lib, album=album, config=config) else: for item in items: plugins.send('item_imported', lib=config.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: util.remove(syspath(old_path), False) task.prune(old_path) # Update progress. if config.resume is not False: task.save_progress() if config.incremental: task.save_history()
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 convert(self, item): """Converts an item in an unsupported media format to ogg. Config pending. This is stolen from Jakob Schnitzers convert plugin. """ fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) source = item.path # FIXME: use avconv? command = u'ffmpeg -i $source -y -acodec libvorbis -vn -aq 2 $dest'.split(u' ') log.info(u'echonest: encoding {0} to {1}' .format(util.displayable_path(source), util.displayable_path(dest))) opts = [] for arg in command: arg = arg.encode('utf-8') opts.append(Template(arg).substitute({ 'source': source, 'dest': dest })) try: encode = Popen(opts, close_fds=True, stderr=DEVNULL) encode.wait() except Exception as exc: log.error(u'echonest: encode failed: {0}'.format(str(exc))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return None if encode.returncode != 0: log.info(u'echonest: encoding {0} failed ({1}). Cleaning up...' .format(util.displayable_path(source), encode.returncode)) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return None log.info(u'echonest: finished encoding {0}' .format(util.displayable_path(source))) return dest
def _generate_playist(self): training_name = self._get_cleaned_training_name() playlist_name = self._get_training_name() target_name = common.get_training_attribute(self.training, "target") if not common.get_target_attribute_for_training( self.training, "generate_playlist"): common.say("Playlist generation to target[{0}] was skipped " "(generate_playlist=no).".format(target_name), log_only=False) return dst_path = Path(common.get_destination_path_for_training( self.training)) dst_sub_dir = dst_path.joinpath(training_name) playlist_filename = "{}.m3u".format(playlist_name) dst = dst_sub_dir.joinpath(playlist_filename) lines = [ "# Playlist generated for training '{}' on {}". \ format(training_name, datetime.now()) ] for item in self.items: path = util.displayable_path( item.get("exportpath", item.get("path"))) if path: path = util.syspath(path) line = "{path}".format(path=path) lines.append(line) with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as ntf: tmp_playlist = ntf.name for line in lines: ntf.write("{}\n".format(line).encode("utf-8")) common.say("Created playlist: {0}".format(dst), log_only=True) util.copy(tmp_playlist, dst) util.remove(tmp_playlist)
def truncate(self, source): """Truncates an item to a size less than UPLOAD_MAX_SIZE.""" fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) self._log.info(u'truncating {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) opts = [] for arg in TRUNCATE_COMMAND.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: self._log.debug(u'truncate failed: {0}', exc) util.remove(dest) return self._log.info(u'truncate encoding {0}', util.displayable_path(source)) return dest
def encode(source, dest): command = get_command() quiet = config["convert"]["quiet"].get() opts = [] if not quiet: log.info(u"Started encoding {0}".format(util.displayable_path(source))) for arg in command: arg = arg.encode("utf-8") opts.append(Template(arg).substitute({"source": source, "dest": dest})) encode = Popen(opts, close_fds=True, stderr=DEVNULL) encode.wait() if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u"Encoding {0} failed. Cleaning up...".format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return if not quiet: log.info(u"Finished encoding {0}".format(util.displayable_path(source)))
def encode(source, dest): """Encode ``source`` to ``dest`` using the command from ``get_format()``. Raises an ``ui.UserError`` if the command was not found and a ``subprocess.CalledProcessError`` if the command exited with a non-zero status code. """ quiet = config['convert']['quiet'].get() if not quiet: log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command, _ = get_format() command = Template(command).safe_substitute({ 'source': pipes.quote(source), 'dest': pipes.quote(dest), }) log.debug(u'convert: executing: {0}'.format( util.displayable_path(command))) try: util.command_output(command, shell=True) except subprocess.CalledProcessError: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...'.format( util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError(u'convert: could invoke ffmpeg: {0}'.format(exc)) if not quiet: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)))
def cleanup(self, copy=False, delete=False, move=False): """Remove and prune imported paths. """ # FIXME Maybe the keywords should be task properties. # FIXME This shouldn't be here. Skipping should be handled in # the stages. if self.skip: return items = self.imported_items() # When copying and deleting originals, delete old files. if copy and delete: new_paths = [os.path.realpath(item.path) for item in items] for old_path in self.old_paths: # Only delete files that were actually copied. if old_path not in new_paths: util.remove(syspath(old_path), False) self.prune(old_path) # When moving, prune empty directories containing the original files. elif move: for old_path in self.old_paths: self.prune(old_path)
def apply_choices(session): """A coroutine for applying changes to albums and singletons during the autotag process. """ task = None while True: task = yield task if task.should_skip(): continue items = task.imported_items() # Clear IDs in case the items are being re-tagged. for item in items: item.id = None item.album_id = None # Change metadata. if task.should_write_tags(): if task.is_album: autotag.apply_metadata( task.match.info, task.match.mapping ) else: autotag.apply_item_metadata(task.item, task.match.info) plugins.send('import_task_apply', session=session, task=task) # Infer album-level fields. if task.is_album: _infer_album_fields(task) # Find existing item entries that these are replacing (for # re-imports). Old album structures are automatically cleaned up # when the last item is removed. task.replaced_items = defaultdict(list) for item in items: dup_items = session.lib.items(library.MatchQuery('path', item.path)) for dup_item in dup_items: task.replaced_items[item].append(dup_item) log.debug('replacing item %i: %s' % (dup_item.id, displayable_path(item.path))) log.debug('%i of %i items replaced' % (len(task.replaced_items), len(items))) # Find old items that should be replaced as part of a duplicate # resolution. duplicate_items = [] if task.remove_duplicates: if task.is_album: for album in _duplicate_check(session.lib, task): duplicate_items += album.items() else: duplicate_items = _item_duplicate_check(session.lib, task) log.debug('removing %i old duplicated items' % len(duplicate_items)) # Delete duplicate files that are located inside the library # directory. for duplicate_path in [i.path for i in duplicate_items]: if session.lib.directory in util.ancestry(duplicate_path): 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) # Add items -- before path changes -- to the library. We add the # items now (rather than at the end) so that album structures # are in place before calls to destination(). with session.lib.transaction(): # Remove old items. for replaced in task.replaced_items.itervalues(): for item in replaced: session.lib.remove(item) for item in duplicate_items: session.lib.remove(item) # Add new ones. if task.is_album: # Add an album. album = session.lib.add_album(items) task.album_id = album.id else: # Add tracks. for item in items: session.lib.add(item)
def encode(self, command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ # The paths and arguments must be bytes. assert isinstance(command, bytes) assert isinstance(source, bytes) assert isinstance(dest, bytes) quiet = self.config['quiet'].get(bool) if not quiet and not pretend: self._log.info(u'Encoding {0}', util.displayable_path(source)) # On Python 3, we need to construct the command to invoke as a # Unicode string. On Unix, this is a little unfortunate---the OS is # expecting bytes---so we use surrogate escaping and decode with the # argument encoding, which is the same encoding that will then be # *reversed* to recover the same bytes before invoking the OS. On # Windows, we want to preserve the Unicode filename "as is." if not six.PY2: command = command.decode(util.arg_encoding(), 'surrogateescape') if platform.system() == 'Windows': source = source.decode(util._fsencoding()) dest = dest.decode(util._fsencoding()) else: source = source.decode(util.arg_encoding(), 'surrogateescape') dest = dest.decode(util.arg_encoding(), 'surrogateescape') # Substitute $source and $dest in the argument list. args = shlex.split(command) encode_cmd = [] for i, arg in enumerate(args): args[i] = Template(arg).safe_substitute({ 'source': source, 'dest': dest, }) if six.PY2: encode_cmd.append(args[i]) else: encode_cmd.append(args[i].encode(util.arg_encoding())) if pretend: self._log.info(u'{0}', u' '.join(ui.decargs(args))) return try: util.command_output(encode_cmd) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files self._log.info(u'Encoding {0} failed. Cleaning up...', util.displayable_path(source)) self._log.debug(u'Command {0} exited with status {1}: {2}', args, exc.returncode, exc.output) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u"convert: couldn't invoke '{0}': {1}".format( u' '.join(ui.decargs(args)), exc ) ) if not quiet and not pretend: self._log.info(u'Finished encoding {0}', util.displayable_path(source))
def play_music(self, lib, opts, args): """Execute query, create temporary playlist and execute player command passing that playlist, at request insert optional arguments. """ command_str = config["play"]["command"].get() use_folders = config["play"]["use_folders"].get(bool) relative_to = config["play"]["relative_to"].get() raw = config["play"]["raw"].get(bool) if relative_to: relative_to = util.normpath(relative_to) # Add optional arguments to the player command. if opts.args: if ARGS_MARKER in command_str: command_str = command_str.replace(ARGS_MARKER, opts.args) else: command_str = "{} {}".format(command_str, opts.args) # Perform search by album and add folders rather than tracks to # playlist. if opts.album: selection = lib.albums(ui.decargs(args)) paths = [] sort = lib.get_default_album_sort() for album in selection: if use_folders: paths.append(album.item_dir()) else: paths.extend(item.path for item in sort.sort(album.items())) item_type = "album" # Perform item query and add tracks to playlist. else: selection = lib.items(ui.decargs(args)) paths = [item.path for item in selection] if relative_to: paths = [relpath(path, relative_to) for path in paths] item_type = "track" item_type += "s" if len(selection) > 1 else "" if not selection: ui.print_(ui.colorize("text_warning", "No {0} to play.".format(item_type))) return # Warn user before playing any huge playlists. if len(selection) > 100: ui.print_(ui.colorize("text_warning", "You are about to queue {0} {1}.".format(len(selection), item_type))) if ui.input_options(("Continue", "Abort")) == "a": return ui.print_("Playing {0} {1}.".format(len(selection), item_type)) if raw: open_args = paths else: open_args = self._create_tmp_playlist(paths) self._log.debug("executing command: {} {}", command_str, b'"' + b" ".join(open_args) + b'"') try: util.interactive_open(open_args, command_str) except OSError as exc: raise ui.UserError("Could not play the music playlist: " "{0}".format(exc)) finally: if not raw: self._log.debug("Removing temporary playlist: {}", open_args[0]) util.remove(open_args[0])
def apply_choices(session): """A coroutine for applying changes to albums and singletons during the autotag process. """ task = None while True: task = yield task if task.should_skip(): continue items = task.imported_items() # Clear IDs in case the items are being re-tagged. for item in items: item.id = None item.album_id = None # Change metadata. if task.should_write_tags(): if task.is_album: autotag.apply_metadata(task.match.info, task.match.mapping) else: autotag.apply_item_metadata(task.item, task.match.info) plugins.send('import_task_apply', session=session, task=task) # Infer album-level fields. if task.is_album: _infer_album_fields(task) # Find existing item entries that these are replacing (for # re-imports). Old album structures are automatically cleaned up # when the last item is removed. task.replaced_items = defaultdict(list) for item in items: dup_items = session.lib.items(library.MatchQuery( 'path', item.path)) for dup_item in dup_items: task.replaced_items[item].append(dup_item) log.debug('replacing item %i: %s' % (dup_item.id, displayable_path(item.path))) log.debug('%i of %i items replaced' % (len(task.replaced_items), len(items))) # Find old items that should be replaced as part of a duplicate # resolution. duplicate_items = [] if task.remove_duplicates: if task.is_album: for album in _duplicate_check(session.lib, task): duplicate_items += album.items() else: duplicate_items = _item_duplicate_check(session.lib, task) log.debug('removing %i old duplicated items' % len(duplicate_items)) # Delete duplicate files that are located inside the library # directory. for duplicate_path in [i.path for i in duplicate_items]: if session.lib.directory in util.ancestry(duplicate_path): 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) # Add items -- before path changes -- to the library. We add the # items now (rather than at the end) so that album structures # are in place before calls to destination(). with session.lib.transaction(): # Remove old items. for replaced in task.replaced_items.itervalues(): for item in replaced: session.lib.remove(item) for item in duplicate_items: session.lib.remove(item) # Add new ones. if task.is_album: # Add an album. album = session.lib.add_album(items) task.album_id = album.id else: # Add tracks. for item in items: session.lib.add(item)
def play_music(lib, opts, args): """Execute query, create temporary playlist and execute player command passing that playlist. """ command_str = config['play']['command'].get() use_folders = config['play']['use_folders'].get(bool) relative_to = config['play']['relative_to'].get() if relative_to: relative_to = util.normpath(relative_to) if command_str: command = shlex.split(command_str) else: # If a command isn't set, then let the OS decide how to open the # playlist. sys_name = platform.system() if sys_name == 'Darwin': command = ['open'] elif sys_name == 'Windows': command = ['start'] else: # If not Mac or Windows, then assume Unixy. command = ['xdg-open'] # Preform search by album and add folders rather then tracks to playlist. if opts.album: selection = lib.albums(ui.decargs(args)) paths = [] for album in selection: if use_folders: paths.append(album.item_dir()) else: # TODO use core's sorting functionality paths.extend([item.path for item in sorted( album.items(), key=lambda item: (item.disc, item.track))]) item_type = 'album' # Preform item query and add tracks to playlist. else: selection = lib.items(ui.decargs(args)) paths = [item.path for item in selection] item_type = 'track' item_type += 's' if len(selection) > 1 else '' if not selection: ui.print_(ui.colorize('yellow', 'No {0} to play.'.format(item_type))) return # Warn user before playing any huge playlists. if len(selection) > 100: ui.print_(ui.colorize( 'yellow', 'You are about to queue {0} {1}.'.format(len(selection), item_type) )) if ui.input_options(('Continue', 'Abort')) == 'a': return # Create temporary m3u file to hold our playlist. m3u = NamedTemporaryFile('w', suffix='.m3u', delete=False) for item in paths: if relative_to: m3u.write(relpath(item, relative_to) + '\n') else: m3u.write(item + '\n') m3u.close() command.append(m3u.name) # Invoke the command and log the output. output = util.command_output(command) if output: log.debug(u'Output of {0}: {1}'.format( util.displayable_path(command[0]), output.decode('utf8', 'ignore'), )) ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) util.remove(m3u.name)
def _cleanup(task, session): for path in task.old_paths: if path in _temp_files: if os.path.isfile(path): util.remove(path) _temp_files.remove(path)
def test_soft_remove_deletes_file(self): util.remove(self.path, True) self.assertNotExists(self.path)
def test_soft_remove_silent_on_no_file(self): try: util.remove(self.path + 'XXX', True) except OSError: self.fail('OSError when removing path')