Beispiel #1
0
def check_art_similarity(item, imagepath, compare_threshold):
    """A boolean indicating if an image is similar to embedded item art.
    """
    with NamedTemporaryFile(delete=True) as f:
        art = extract(f.name, item)

        if art:
            # Converting images to grayscale tends to minimize the weight
            # of colors in the diff score
            cmd = 'convert {0} {1} -colorspace gray MIFF:- | ' \
                  'compare -metric PHASH - null:'.format(syspath(imagepath),
                                                         syspath(art))

            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE,
                                    close_fds=platform.system() != 'Windows',
                                    shell=True)
            stdout, stderr = proc.communicate()
            if proc.returncode:
                if proc.returncode != 1:
                    log.warn(u'embedart: IM phashes compare failed for {0}, \
                   {1}'.format(displayable_path(imagepath),
                               displayable_path(art)))
                    return
                phashDiff = float(stderr)
            else:
                phashDiff = float(stdout)

            log.info(u'embedart: compare PHASH score is {0}'.format(phashDiff))
            if phashDiff > compare_threshold:
                return False

    return True
Beispiel #2
0
def im_resize(maxwidth, path_in, path_out=None):
    """Resize using ImageMagick.

    Use the ``magick`` program or ``convert`` on older versions. Return
    the output path of resized image.
    """
    path_out = path_out or temp_file_for(path_in)
    log.debug(u'artresizer: ImageMagick resizing {0} to {1}',
              util.displayable_path(path_in), util.displayable_path(path_out))

    # "-resize WIDTHx>" shrinks images with the width larger
    # than the given width while maintaining the aspect ratio
    # with regards to the height.
    cmd = ArtResizer.shared.im_convert_cmd + \
        [util.syspath(path_in, prefix=False),
            '-resize', '{0}x>'.format(maxwidth),
            util.syspath(path_out, prefix=False)]

    try:
        util.command_output(cmd)
    except subprocess.CalledProcessError:
        log.warning(u'artresizer: IM convert failed for {0}',
                    util.displayable_path(path_in))
        return path_in

    return path_out
Beispiel #3
0
def _embed(path, items, maxwidth=0):
    """Embed an image file, located at `path`, into each item.
    """
    if maxwidth:
        path = ArtResizer.shared.resize(maxwidth, syspath(path))

    data = open(syspath(path), 'rb').read()
    kindstr = imghdr.what(None, data)
    if kindstr not in ('jpeg', 'png'):
        log.error('A file of type %s is not allowed as coverart.' % kindstr)
        return

    # Add art to each file.
    log.debug('Embedding album art.')

    for item in items:
        try:
            f = mediafile.MediaFile(syspath(item.path))
        except mediafile.UnreadableFileError as exc:
            log.warn('Could not embed art in {0}: {1}'.format(
                displayable_path(item.path), exc
            ))
            continue
        f.art = data
        f.save()
Beispiel #4
0
def extract(lib, outpath, query):
    items = lib.items(query)
    for i_item in items:
        item = i_item
        break
    else:
        log.error('No item matches query.')
        return

    # Extract the art.
    mf = mediafile.MediaFile(syspath(item.path))
    art = mf.art
    if not art:
        log.error('No album art present in %s - %s.' %
                  (item.artist, item.title))
        return

    # Add an extension to the filename.
    ext = imghdr.what(None, h=art)
    if not ext:
        log.error('Unknown image type.')
        return
    outpath += '.' + ext

    log.info('Extracting album art from: %s - %s\n'
             'To: %s' % \
             (item.artist, item.title, outpath))
    with open(syspath(outpath), 'wb') as f:
        f.write(art)
Beispiel #5
0
    def write_gain(self, mf, track_data, album_data):
        try:
            mf.rg_track_gain = track_data[syspath(mf.path)].gain
            mf.rg_track_peak = track_data[syspath(mf.path)].peak

            if self.write_album and album_data:
                mf.rg_album_gain = album_data.gain
                mf.rg_album_peak = album_data.peak

                log.debug('Tagging ReplayGain for: %s - %s \n'
                         '\tTrack Gain = %f\n'
                         '\tTrack Peak = %f\n'
                         '\tAlbum Gain = %f\n'
                         '\tAlbum Peak = %f' % \
                         (mf.artist,
                          mf.title,
                          mf.rg_track_gain,
                          mf.rg_track_peak,
                          mf.rg_album_gain,
                          mf.rg_album_peak))
            else:
                log.debug('Tagging ReplayGain for: %s - %s \n'
                         '\tTrack Gain = %f\n'
                         '\tTrack Peak = %f\n' % \
                         (mf.artist,
                         mf.title,
                         mf.rg_track_gain,
                         mf.rg_track_peak))

            mf.save()
        except (FileTypeError, UnreadableFileError, TypeError, ValueError), e:
            log.error("failed to write replaygain: %s" % (mf.title))
Beispiel #6
0
    def move(self, copy=False):
        """Moves (or copies) all items to their destination. Any
        album art moves along with them.
        """
        # Move items.
        items = list(self.items())
        for item in items:
            item.move(self._library, copy)
        newdir = os.path.dirname(items[0].path)

        # Move art.
        old_art = self.artpath
        if old_art:
            new_art = self.art_destination(old_art, newdir)
            if new_art != old_art:
                if copy:
                    shutil.copy(syspath(old_art), syspath(new_art))
                else:
                    shutil.move(syspath(old_art), syspath(new_art))
                self.artpath = new_art

        # Store new item paths. We do this at the end to avoid
        # locking the database for too long while files are copied.
        for item in items:
            self._library.store(item)
Beispiel #7
0
def _record_items(lib, basename, items):
    """Records relative paths to the given items for each feed format
    """
    feedsdir = bytestring_path(config['importfeeds']['dir'].as_filename())
    formats = config['importfeeds']['formats'].as_str_seq()
    relative_to = config['importfeeds']['relative_to'].get() \
            or config['importfeeds']['dir'].as_filename()
    relative_to = bytestring_path(relative_to)

    paths = []
    for item in items:
        paths.append(os.path.relpath(
            item.path, relative_to
        ))

    if 'm3u' in formats:
        basename = bytestring_path(
            config['importfeeds']['m3u_name'].get(unicode)
        )
        m3u_path = os.path.join(feedsdir, basename)
        _write_m3u(m3u_path, paths)

    if 'm3u_multi' in formats:
        m3u_path = _build_m3u_filename(basename)
        _write_m3u(m3u_path, paths)

    if 'link' in formats:
        for path in paths:
            dest = os.path.join(feedsdir, os.path.basename(path))
            if not os.path.exists(syspath(dest)):
                os.symlink(syspath(path), syspath(dest))
Beispiel #8
0
def _embed(path, items, maxwidth=0):
    """Embed an image file, located at `path`, into each item.
    """
    if maxwidth:
        path = ArtResizer.shared.resize(maxwidth, syspath(path))

    try:
        with open(syspath(path), 'rb') as f:
            data = f.read()
    except IOError as exc:
        log.error(u'embedart: could not read image file: {0}'.format(exc))
        return
    image = mediafile.Image(data, type=mediafile.ImageType.front)

    # Add art to each file.
    log.debug('Embedding album art.')

    for item in items:
        try:
            f = mediafile.MediaFile(syspath(item.path))
        except (mediafile.UnreadableFileError, IOError) as exc:
            log.warn('Could not embed art in {0}: {1}'.format(
                displayable_path(item.path), exc
            ))
            continue
        f.images = [image]
        f.save(config['id3v23'].get(bool))
Beispiel #9
0
def extract(lib, outpath, query):
    item = lib.items(query).get()
    if not item:
        log.error('No item matches query.')
        return

    # Extract the art.
    try:
        mf = mediafile.MediaFile(syspath(item.path))
    except mediafile.UnreadableFileError as exc:
        log.error(u'Could not extract art from {0}: {1}'.format(
            displayable_path(item.path), exc
        ))
        return

    art = mf.art
    if not art:
        log.error('No album art present in %s - %s.' %
                  (item.artist, item.title))
        return

    # Add an extension to the filename.
    ext = imghdr.what(None, h=art)
    if not ext:
        log.error('Unknown image type.')
        return
    outpath += '.' + ext

    log.info(u'Extracting album art from: {0.artist} - {0.title}\n'
             u'To: {1}'.format(item, displayable_path(outpath)))
    with open(syspath(outpath), 'wb') as f:
        f.write(art)
Beispiel #10
0
        def scrub_func(lib, opts, args):
            # This is a little bit hacky, but we set a global flag to
            # avoid autoscrubbing when we're also explicitly scrubbing.
            global scrubbing
            scrubbing = True

            # Walk through matching files and remove tags.
            for item in lib.items(ui.decargs(args)):
                self._log.info(u'scrubbing: {0}',
                               util.displayable_path(item.path))

                # Get album art if we need to restore it.
                if opts.write:
                    try:
                        mf = mediafile.MediaFile(util.syspath(item.path),
                                                 config['id3v23'].get(bool))
                    except IOError as exc:
                        self._log.error(u'could not open file to scrub: {0}',
                                        exc)
                    art = mf.art

                # Remove all tags.
                self._scrub(item.path)

                # Restore tags, if enabled.
                if opts.write:
                    self._log.debug(u'writing new tags after scrub')
                    item.try_write()
                    if art:
                        self._log.info(u'restoring art')
                        mf = mediafile.MediaFile(util.syspath(item.path))
                        mf.art = art
                        mf.save()

            scrubbing = False
Beispiel #11
0
    def __init__(self):
        super(ImportFeedsPlugin, self).__init__()

        self.config.add({
            'formats': [],
            'm3u_name': u'imported.m3u',
            'dir': None,
            'relative_to': None,
            'absolute_path': False,
        })

        feeds_dir = self.config['dir'].get()
        if feeds_dir:
            feeds_dir = os.path.expanduser(bytestring_path(feeds_dir))
            self.config['dir'] = feeds_dir
            if not os.path.exists(syspath(feeds_dir)):
                os.makedirs(syspath(feeds_dir))

        relative_to = self.config['relative_to'].get()
        if relative_to:
            self.config['relative_to'] = normpath(relative_to)
        else:
            self.config['relative_to'] = feeds_dir

        self.register_listener('library_opened', self.library_opened)
        self.register_listener('album_imported', self.album_imported)
        self.register_listener('item_imported', self.item_imported)
Beispiel #12
0
def im_resize(maxwidth, path_in, path_out=None):
    """Resize using ImageMagick's ``convert`` tool.
    Return the output path of resized image.
    """
    path_out = path_out or temp_file_for(path_in)
    log.debug(
        u"artresizer: ImageMagick resizing {0} to {1}", util.displayable_path(path_in), util.displayable_path(path_out)
    )

    # "-resize widthxheight>" shrinks images with dimension(s) larger
    # than the corresponding width and/or height dimension(s). The >
    # "only shrink" flag is prefixed by ^ escape char for Windows
    # compatibility.
    try:
        util.command_output(
            [
                b"convert",
                util.syspath(path_in, prefix=False),
                b"-resize",
                b"{0}x^>".format(maxwidth),
                util.syspath(path_out, prefix=False),
            ]
        )
    except subprocess.CalledProcessError:
        log.warn(u"artresizer: IM convert failed for {0}", util.displayable_path(path_in))
        return path_in
    return path_out
Beispiel #13
0
    def _scrub_item(self, item, restore=True):
        """Remove tags from an Item's associated file and, if `restore`
        is enabled, write the database's tags back to the file.
        """
        # Get album art if we need to restore it.
        if restore:
            try:
                mf = mediafile.MediaFile(util.syspath(item.path),
                                         config['id3v23'].get(bool))
            except IOError as exc:
                self._log.error(u'could not open file to scrub: {0}',
                                exc)
            art = mf.art

        # Remove all tags.
        self._scrub(item.path)

        # Restore tags, if enabled.
        if restore:
            self._log.debug(u'writing new tags after scrub')
            item.try_write()
            if art:
                self._log.debug(u'restoring art')
                mf = mediafile.MediaFile(util.syspath(item.path))
                mf.art = art
                mf.save()
Beispiel #14
0
    def test_extracted_extension(self):
        resource_path = os.path.join(_common.RSRC, b'image-jpeg.mp3')
        album = self.add_album_fixture()
        trackpath = album.items()[0].path
        albumpath = album.path
        shutil.copy(syspath(resource_path), syspath(trackpath))

        self.run_command('extractart', '-n', 'extracted')

        self.assertExists(os.path.join(albumpath, b'extracted.jpg'))
Beispiel #15
0
    def test_non_ascii_album_path(self):
        resource_path = os.path.join(_common.RSRC, b"image.mp3")
        album = self.add_album_fixture()
        trackpath = album.items()[0].path
        albumpath = album.path
        shutil.copy(syspath(resource_path), syspath(trackpath))

        self.run_command("extractart", "-n", "extracted")

        self.assertExists(os.path.join(albumpath, b"extracted.png"))
Beispiel #16
0
    def test_non_ascii_album_path(self):
        resource_path = os.path.join(_common.RSRC, 'image.mp3').encode('utf8')
        album = self.add_album_fixture()
        trackpath = album.items()[0].path
        albumpath = album.path
        shutil.copy(syspath(resource_path), syspath(trackpath))

        self.run_command('extractart', '-n', 'extracted')

        self.assertExists(syspath(os.path.join(albumpath, b'extracted.png')))
 def assertSymlink(self, link, target):
     link = syspath(link)
     target = syspath(target)
     self.assertTrue(os.path.islink(link),
                     msg=u'Path is not a symbolic link: {0}'.format(link))
     self.assertTrue(os.path.isfile(target),
                     msg=u'Path is not a file: {0}'.format(link))
     link_target = os.readlink(link)
     link_target = os.path.join(os.path.dirname(link), link_target)
     self.assertEqual(target, link_target)
Beispiel #18
0
def _mediafile_image(image_path, maxwidth=None):
    """Return a `mediafile.Image` object for the path.

    If maxwidth is set the image is resized if necessary.
    """
    if maxwidth:
        image_path = ArtResizer.shared.resize(maxwidth, syspath(image_path))

    with open(syspath(image_path), 'rb') as f:
        data = f.read()
    return mediafile.Image(data, type=mediafile.ImageType.front)
Beispiel #19
0
def _get_feeds_dir(lib):
    """Given a Library object, return the path to the feeds directory to be
    used (either in the library directory or an explicitly configured
    path). Ensures that the directory exists.
    """
    # Inside library directory.
    dirpath = lib.directory

    # Ensure directory exists.
    if not os.path.exists(syspath(dirpath)):
        os.makedirs(syspath(dirpath))
    return dirpath
Beispiel #20
0
    def test_write_with_custom_path(self):
        item = self.add_item_fixture()
        custom_path = os.path.join(self.temp_dir, b"custom.mp3")
        shutil.copy(syspath(item.path), syspath(custom_path))

        item["artist"] = "new artist"
        self.assertNotEqual(MediaFile(custom_path).artist, "new artist")
        self.assertNotEqual(MediaFile(item.path).artist, "new artist")

        item.write(custom_path)
        self.assertEqual(MediaFile(custom_path).artist, "new artist")
        self.assertNotEqual(MediaFile(item.path).artist, "new artist")
Beispiel #21
0
    def test_write_with_custom_path(self):
        item = self.add_item_fixture()
        custom_path = os.path.join(self.temp_dir, 'custom.mp3')
        shutil.copy(syspath(item.path), syspath(custom_path))

        item['artist'] = 'new artist'
        self.assertNotEqual(MediaFile(custom_path).artist, 'new artist')
        self.assertNotEqual(MediaFile(item.path).artist, 'new artist')

        item.write(custom_path)
        self.assertEqual(MediaFile(custom_path).artist, 'new artist')
        self.assertNotEqual(MediaFile(item.path).artist, 'new artist')
Beispiel #22
0
 def set_art(self, path):
     """Sets the album's cover art to the image at the given path.
     The image is copied into place, replacing any existing art.
     """
     path = bytestring_path(path)
     oldart = self.artpath
     artdest = self.art_destination(path)
     if oldart == artdest:
         os.soft_remove(oldart)
     
     shutil.copyfile(syspath(path), syspath(artdest))
     self.artpath = artdest
Beispiel #23
0
    def test_delete_initial_key_tag(self):
        item = self.lib.items().get()
        item.initial_key = u'C#m'
        item.write()
        item.store()

        mediafile = MediaFile(syspath(item.path))
        self.assertEqual(mediafile.initial_key, u'C#m')

        self.modify(u"initial_key!")
        mediafile = MediaFile(syspath(item.path))
        self.assertIsNone(mediafile.initial_key)
Beispiel #24
0
    def emitter():
        fields = list(mediafile.MediaFile.readable_fields())
        fields.remove('images')
        mf = mediafile.MediaFile(syspath(path))
        tags = {}
        for field in fields:
            tags[field] = getattr(mf, field)
        tags['art'] = mf.art is not None
        # create a temporary Item to take advantage of __format__
        item = Item.from_path(syspath(path))

        return tags, item
 def matched_item_action(self, item):
     path = self.get_path(item)
     if path and os.path.isfile(path):
         dest = self.destination(item)
         if path != dest:
             return (item, self.MOVE)
         elif (os.path.getmtime(syspath(dest))
               < os.path.getmtime(syspath(item.path))):
             return (item, self.WRITE)
         else:
             return (item, self.NOOP)
     else:
         return (item, self.ADD)
Beispiel #26
0
    def test_do_not_change_database(self):
        item = self.add_item_fixture(year=2000)
        item.write()
        mediafile = MediaFile(syspath(item.path))
        self.assertEqual(2000, mediafile.year)

        config['zero'] = {'fields': ['year']}
        self.load_plugins('zero')

        item.write()
        mediafile = MediaFile(syspath(item.path))
        self.assertEqual(item['year'], 2000)
        self.assertIsNone(mediafile.year)
Beispiel #27
0
    def item_imported(self, lib, item, config):
        try:
            self.write_album = False

            mf = MediaFile(syspath(item.path))

            if self.requires_gain(mf):
                track_data, album_data = rgcalc.calculate([syspath(mf.path)],
                                                          True,
                                                          self.ref_level)
                self.write_gain(mf, track_data, None)
        except (FileTypeError, UnreadableFileError, TypeError, ValueError), e:
            log.error("failed to calculate replaygain:  %s ", e)
Beispiel #28
0
    def test_album_art(self):
        path = self.create_mediafile_fixture(images=['jpg'])
        item = Item.from_path(path)

        mediafile = MediaFile(syspath(item.path))
        self.assertNotEqual(0, len(mediafile.images))

        config['zero'] = {'fields': [u'images']}
        self.load_plugins('zero')

        item.write()
        mediafile = MediaFile(syspath(item.path))
        self.assertEqual(0, len(mediafile.images))
Beispiel #29
0
    def fix(self, lib, item=None, album=None):
        """Fix the permissions for an imported Item or Album.
        """
        # Get the configured permissions. The user can specify this either a
        # string (in YAML quotes) or, for convenience, as an integer so the
        # quotes can be omitted. In the latter case, we need to reinterpret the
        # integer as octal, not decimal.
        file_perm = config['permissions']['file'].get()
        dir_perm = config['permissions']['dir'].get()
        file_perm = convert_perm(file_perm)
        dir_perm = convert_perm(dir_perm)

        # Create chmod_queue.
        file_chmod_queue = []
        if item:
            file_chmod_queue.append(item.path)
        elif album:
            for album_item in album.items():
                file_chmod_queue.append(album_item.path)

        # A set of directories to change permissions for.
        dir_chmod_queue = set()

        for path in file_chmod_queue:
            # Changing permissions on the destination file.
            self._log.debug(
                u'setting file permissions on {}',
                util.displayable_path(path),
            )
            os.chmod(util.syspath(path), file_perm)

            # Checks if the destination path has the permissions configured.
            assert_permissions(path, file_perm, self._log)

            # Adding directories to the directory chmod queue.
            dir_chmod_queue.update(
                dirs_in_library(lib.directory,
                                path))

        # Change permissions for the directories.
        for path in dir_chmod_queue:
            # Chaning permissions on the destination directory.
            self._log.debug(
                u'setting directory permissions on {}',
                util.displayable_path(path),
            )
            os.chmod(util.syspath(path), dir_perm)

            # Checks if the destination path has the permissions configured.
            assert_permissions(path, dir_perm, self._log)
Beispiel #30
0
    def __init__(self):
        super(ImportFeedsPlugin, self).__init__()

        self.config.add({
            'formats': [],
            'm3u_name': u'imported.m3u',
            'dir': None,
        })
        
        feeds_dir = self.config['dir'].get()
        if feeds_dir: 
            self.config['dir'] = os.path.expanduser(bytestring_path(feeds_dir))
            if not os.path.exists(syspath(feeds_dir)):
                os.makedirs(syspath(feeds_dir))
Beispiel #31
0
def _write_m3u(m3u_path, items_paths):
    """Append relative paths to items into m3u file.
    """
    with open(syspath(m3u_path), 'a') as f:
        for path in items_paths:
            f.write(path + '\n')
Beispiel #32
0
    def copy_album_art(self, album, dest_dir, path_formats, pretend=False):
        """Copies or converts the associated cover art of the album. Album must
        have at least one track.
        """
        if not album or not album.artpath:
            return

        album_item = album.items().get()
        # Album shouldn't be empty.
        if not album_item:
            return

        # Get the destination of the first item (track) of the album, we use
        # this function to format the path accordingly to path_formats.
        dest = album_item.destination(basedir=dest_dir,
                                      path_formats=path_formats)

        # Remove item from the path.
        dest = os.path.join(*util.components(dest)[:-1])

        dest = album.art_destination(album.artpath, item_dir=dest)
        if album.artpath == dest:
            return

        if not pretend:
            util.mkdirall(dest)

        if os.path.exists(util.syspath(dest)):
            self._log.info(u'Skipping {0} (target file exists)',
                           util.displayable_path(album.artpath))
            return

        # Decide whether we need to resize the cover-art image.
        resize = False
        maxwidth = None
        if self.config['album_art_maxwidth']:
            maxwidth = self.config['album_art_maxwidth'].get(int)
            size = ArtResizer.shared.get_size(album.artpath)
            self._log.debug('image size: {}', size)
            if size:
                resize = size[0] > maxwidth
            else:
                self._log.warning(u'Could not get size of image (please see '
                                  u'documentation for dependencies).')

        # Either copy or resize (while copying) the image.
        if resize:
            self._log.info(u'Resizing cover art from {0} to {1}',
                           util.displayable_path(album.artpath),
                           util.displayable_path(dest))
            if not pretend:
                ArtResizer.shared.resize(maxwidth, album.artpath, dest)
        else:
            if pretend:
                self._log.info(u'cp {0} {1}',
                               util.displayable_path(album.artpath),
                               util.displayable_path(dest))
            else:
                self._log.info(u'Copying cover art to {0}',
                               util.displayable_path(album.artpath),
                               util.displayable_path(dest))
                util.copy(album.artpath, dest)
Beispiel #33
0
    def convert_item(self, dest_dir, keep_new, path_formats, format,
                     pretend=False):
        command, ext = get_format(format)
        item, original, converted = None, None, None
        while True:
            item = yield (item, original, converted)
            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 = item.path
                if should_transcode(item, format):
                    converted = replace_ext(converted, ext)
            else:
                original = item.path
                if should_transcode(item, format):
                    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.)
            if not pretend:
                with _fs_lock:
                    util.mkdirall(dest)

            if os.path.exists(util.syspath(dest)):
                self._log.info(u'Skipping {0} (target file exists)',
                               util.displayable_path(item.path))
                continue

            if keep_new:
                if pretend:
                    self._log.info(u'mv {0} {1}',
                                   util.displayable_path(item.path),
                                   util.displayable_path(original))
                else:
                    self._log.info(u'Moving to {0}',
                                   util.displayable_path(original))
                    util.move(item.path, original)

            if should_transcode(item, format):
                try:
                    self.encode(command, original, converted, pretend)
                except subprocess.CalledProcessError:
                    continue
            else:
                if pretend:
                    self._log.info(u'cp {0} {1}',
                                   util.displayable_path(original),
                                   util.displayable_path(converted))
                else:
                    # No transcoding necessary.
                    self._log.info(u'Copying {0}',
                                   util.displayable_path(item.path))
                    util.copy(original, converted)

            if pretend:
                continue

            # Write tags from the database to the converted file.
            item.try_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 self.config['embed']:
                album = item.get_album()
                if album and album.artpath:
                    EmbedCoverArtPlugin().embed_item(item, album.artpath,
                                                     itempath=converted)

            if keep_new:
                plugins.send('after_convert', item=item,
                             dest=dest, keepnew=True)
            else:
                plugins.send('after_convert', item=item,
                             dest=converted, keepnew=False)
Beispiel #34
0
 def test_syspath_posix_unchanged(self):
     with _common.platform_posix():
         path = os.path.join('a', 'b', 'c')
         outpath = util.syspath(path)
     self.assertEqual(path, outpath)
Beispiel #35
0
def compute_checksum(item):
    hash = sha256()
    with open(syspath(item.path), 'rb') as file:
        hash.update(file.read())
    return hash.hexdigest()
Beispiel #36
0
 def check_cover_is_stored(self):
     self.assertEqual(self.album['artpath'], self.cover_path)
     with open(util.syspath(self.cover_path), 'r') as f:
         self.assertEqual(f.read(), 'IMAGE')
Beispiel #37
0
 def assertNotExists(self, path):  # noqa
     self.assertFalse(os.path.exists(util.syspath(path)),
                      u'file exists: {!r}'.format((path)))
Beispiel #38
0
 def test_write_initial_key_tag(self):
     self.modify(u"initial_key=C#m")
     item = self.lib.items().get()
     mediafile = MediaFile(syspath(item.path))
     self.assertEqual(mediafile.initial_key, u'C#m')
Beispiel #39
0
 def current_mtime(self):
     """Returns the current mtime of the file, rounded to the nearest
     integer.
     """
     return int(os.path.getmtime(syspath(self.path)))
Beispiel #40
0
 def test_syspath_windows_format(self):
     path = ntpath.join('a', 'b', 'c')
     outpath = util.syspath(path, ntpath)
     self.assertTrue(isinstance(outpath, unicode))
     self.assertTrue(outpath.startswith(u'\\\\?\\'))
Beispiel #41
0
def read_tasks(session):
    """A generator yielding all the albums (as ImportTask objects) found
    in the user-specified list of paths. In the case of a singleton
    import, yields single-item tasks instead.
    """
    skipped = 0
    for toppath in session.paths:
        task_factory = ImportTaskFactory(toppath, session)

        # Determine if we want to resume import of the toppath
        session.ask_resume(toppath)

        # Extract archives.
        archive_task = None
        if ArchiveImportTask.is_archive(syspath(toppath)):
            if not (session.config['move'] or session.config['copy']):
                log.warn(u"Archive importing requires either "
                         "'copy' or 'move' to be enabled.")
                continue

            log.debug(u'extracting archive {0}'.format(
                displayable_path(toppath)))
            archive_task = ArchiveImportTask(toppath)
            try:
                archive_task.extract()
            except Exception as exc:
                log.error(u'extraction failed: {0}'.format(exc))
                continue

            # Continue reading albums from the extracted directory.
            toppath = archive_task.toppath

        # Check whether the path is to a file.
        if not os.path.isdir(syspath(toppath)):
            if session.config['singletons']:
                task = task_factory.singleton(toppath)
            else:
                task = task_factory.album([toppath], dir=toppath)

            if task:
                yield task
                yield task_factory.sentinel()
            continue

        # A flat album import merges all items into one album.
        if session.config['flat'] and not session.config['singletons']:
            paths = []
            for _, item_paths in albums_in_dir(toppath):
                paths += item_paths
            task = task_factory.album(paths)
            if task:
                yield task
                yield task_factory.sentinel()
            continue

        # Produce paths under this directory.
        for dirs, paths in albums_in_dir(toppath):
            if session.config['singletons']:
                for path in paths:
                    task = task_factory.singleton(path)
                    if task:
                        yield task
                yield task_factory.sentinel(dirs)

            else:
                task = task_factory.album(paths)
                if task:
                    yield task

        # Indicate the directory is finished.
        # FIXME hack to delete extracted archives
        if archive_task is None:
            yield task_factory.sentinel()
        else:
            yield archive_task

    # Show skipped directories.
    if skipped:
        log.info(u'Skipped {0} directories.'.format(skipped))
Beispiel #42
0
    def test_external(self):
        external_dir = os.path.join(self.mkdtemp(), 'myplayer')
        self.config['convert']['formats'] = {
            'aac': {
                'command': 'bash -c "cp \'$source\' \'$dest\';' +
                           'printf ISAAC >> \'$dest\'"',
                'extension': 'm4a'
            },
        }
        self.config['alternatives'] = {
            'myplayer': {
                'directory': external_dir,
                'paths': {'default': u'$artist/$title'},
                'formats': u'aac mp3',
                'query': u'onplayer:true',
                'removable': True,
            }
        }

        self.add_album(artist='Bach', title='was mp3', format='mp3')
        self.add_album(artist='Bach', title='was m4a', format='m4a')
        self.add_album(artist='Bach', title='was ogg', format='ogg')
        self.add_album(artist='Beethoven', title='was ogg', format='ogg')

        external_from_mp3 = bytestring_path(
            os.path.join(external_dir, 'Bach', 'was mp3.mp3'))
        external_from_m4a = bytestring_path(
            os.path.join(external_dir, 'Bach', 'was m4a.m4a'))
        external_from_ogg = bytestring_path(
            os.path.join(external_dir, 'Bach', 'was ogg.m4a'))
        external_beet = bytestring_path(
            os.path.join(external_dir, 'Beethoven', 'was ogg.m4a'))

        self.runcli('modify', '--yes', 'onplayer=true', 'artist:Bach')
        with control_stdin('y'):
            out = self.runcli('alt', 'update', 'myplayer')
            self.assertIn('Do you want to create the collection?', out)

        self.assertNotFileTag(external_from_mp3, b'ISAAC')
        self.assertNotFileTag(external_from_m4a, b'ISAAC')
        self.assertFileTag(external_from_ogg, b'ISAAC')
        self.assertFalse(os.path.isfile(external_beet))

        self.runcli('modify', '--yes', 'composer=JSB', 'artist:Bach')

        list_output = self.runcli(
            'alt', 'list-tracks', 'myplayer', '--format', '$artist $title')
        self.assertEqual(list_output, '\n'.join(
            ['Bach was mp3', 'Bach was m4a', 'Bach was ogg', '']))

        self.runcli('alt', 'update', 'myplayer')
        mediafile = MediaFile(syspath(external_from_ogg))
        self.assertEqual(mediafile.composer, 'JSB')

        self.runcli('modify', '--yes', 'onplayer!', 'artist:Bach')
        self.runcli('modify', '--album', '--yes',
                    'onplayer=true', 'albumartist:Beethoven')
        self.runcli('alt', 'update', 'myplayer')

        list_output = self.runcli(
            'alt', 'list-tracks', 'myplayer', '--format', '$artist')
        self.assertEqual(list_output, 'Beethoven\n')

        self.assertFalse(os.path.isfile(external_from_mp3))
        self.assertFalse(os.path.isfile(external_from_m4a))
        self.assertFalse(os.path.isfile(external_from_ogg))
        self.assertFileTag(external_beet, b'ISAAC')
Beispiel #43
0
def main(args=None, configfh=None):
    """Run the main command-line interface for beets."""
    # Get the default subcommands.
    from beets.ui.commands import default_commands

    # Get default file paths.
    default_config, default_libpath, default_dir = default_paths()

    # Read defaults from config file.
    config = ConfigParser.SafeConfigParser()
    if configfh:
        configpath = None
    elif CONFIG_PATH_VAR in os.environ:
        configpath = os.path.expanduser(os.environ[CONFIG_PATH_VAR])
    else:
        configpath = default_config
    if configpath:
        configpath = util.syspath(configpath)
        if os.path.exists(util.syspath(configpath)):
            configfh = open(configpath)
        else:
            configfh = None
    if configfh:
        config.readfp(configfh)

    # Add plugin paths.
    plugpaths = config_val(config, 'beets', 'pluginpath', '')
    for plugpath in plugpaths.split(':'):
        sys.path.append(os.path.expanduser(plugpath))
    # Load requested plugins.
    plugnames = config_val(config, 'beets', 'plugins', '')
    plugins.load_plugins(plugnames.split())
    plugins.load_listeners()
    plugins.send("pluginload")
    plugins.configure(config)

    # Construct the root parser.
    commands = list(default_commands)
    commands += plugins.commands()
    parser = SubcommandsOptionParser(subcommands=commands)
    parser.add_option('-l',
                      '--library',
                      dest='libpath',
                      help='library database file to use')
    parser.add_option('-d',
                      '--directory',
                      dest='directory',
                      help="destination music directory")
    parser.add_option('-p',
                      '--pathformat',
                      dest='path_format',
                      help="destination path format string")
    parser.add_option('-v',
                      '--verbose',
                      dest='verbose',
                      action='store_true',
                      help='print debugging information')

    # Parse the command-line!
    options, subcommand, suboptions, subargs = parser.parse_args(args)

    # Open library file.
    libpath = options.libpath or \
        config_val(config, 'beets', 'library', default_libpath)
    directory = options.directory or \
        config_val(config, 'beets', 'directory', default_dir)
    legacy_path_format = config_val(config, 'beets', 'path_format', None)
    if options.path_format:
        # If given, -p overrides all path format settings
        path_formats = {'default': options.path_format}
    else:
        if legacy_path_format:
            # Old path formats override the default values.
            path_formats = {'default': legacy_path_format}
        else:
            # If no legacy path format, use the defaults instead.
            path_formats = DEFAULT_PATH_FORMATS
        if config.has_section('paths'):
            path_formats.update(config.items('paths'))
    art_filename = \
        config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME)
    lib_timeout = config_val(config, 'beets', 'timeout', DEFAULT_TIMEOUT)
    try:
        lib_timeout = float(lib_timeout)
    except ValueError:
        lib_timeout = DEFAULT_TIMEOUT
    db_path = os.path.expanduser(libpath)
    try:
        lib = library.Library(db_path, directory, path_formats, art_filename,
                              lib_timeout)
    except sqlite3.OperationalError:
        raise UserError("database file %s could not be opened" % db_path)

    # Configure the logger.
    log = logging.getLogger('beets')
    if options.verbose:
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(logging.INFO)
    log.debug(u'config file: %s' % configpath)
    log.debug(u'library database: %s' % lib.path)
    log.debug(u'library directory: %s' % lib.directory)

    # Invoke the subcommand.
    try:
        subcommand.func(lib, config, suboptions, subargs)
    except UserError, exc:
        message = exc.args[0] if exc.args else None
        subcommand.parser.error(message)
Beispiel #44
0
 def test_syspath_posix_unchanged(self):
     path = posixpath.join('a', 'b', 'c')
     outpath = util.syspath(path, posixpath)
     self.assertEqual(path, outpath)
Beispiel #45
0
    def update_playlists(self, lib):
        self._log.info(u"Updating {0} smart playlists...",
                       len(self._matched_playlists))

        playlist_dir = self.config['playlist_dir'].as_filename()
        playlist_dir = bytestring_path(playlist_dir)
        relative_to = self.config['relative_to'].get()
        if relative_to:
            relative_to = normpath(relative_to)

        # Maps playlist filenames to lists of track filenames.
        m3us = {}

        for playlist in self._matched_playlists:
            name, (query, q_sort), (album_query, a_q_sort) = playlist
            self._log.debug(u"Creating playlist {0}", name)
            items = []

            if query:
                items.extend(lib.items(query, q_sort))
            if album_query:
                for album in lib.albums(album_query, a_q_sort):
                    items.extend(album.items())

            # As we allow tags in the m3u names, we'll need to iterate through
            # the items and generate the correct m3u file names.
            for item in items:
                m3u_name = item.evaluate_template(name, True)
                m3u_name = sanitize_path(m3u_name, lib.replacements)
                if m3u_name not in m3us:
                    m3us[m3u_name] = []
                item_path = item.path
                if relative_to:
                    item_path = os.path.relpath(item.path, relative_to)
                if item_path not in m3us[m3u_name]:
                    m3us[m3u_name].append(item_path)

        # Write all of the accumulated track lists to files.
        for m3u in m3us:
            # CUSTOM PART FROM JONES TO ADD TRACKS THAT AREN'T ANY MORE FETCHED WITH THE QUERY
            PLroot = normpath(os.path.join(playlist_dir, bytestring_path(m3u)))
            oldfile = PLroot+".query"
            newfile = PLroot+".new"
            removefile = PLroot+".remove"
            mkdirall(newfile)
            newpaths = []
            with open(syspath(newfile), 'wb') as newf:
                # create newfile
                for path in m3us[m3u]:
                    newf.write(path + b'\n')
                    newpaths.append(path)
                # check if oldfile exists
            if os.path.isfile(oldfile):
                with open(syspath(oldfile), 'r') as oldf:
                    oldpaths = oldf.read().splitlines()
                    # check foreach path in oldfile if it is not in newpaths and write it to the removefile then
                    for path in oldpaths:
                        #if not path in newf.read():
                        if not any(path in newpath for newpath in newpaths):
                            print("oldpath: "+path+" is not in newf.read()")
                            # write path to removefile
                            with open(syspath(removefile), 'ab') as remf:
                                remf.write(path + b'\n')
                # delete the oldfile
                os.remove(oldfile)
            #rename newfile to syntax of oldfile
            os.rename(newfile, oldfile)
            # END OF JONES CUSTOM PART

        self._log.info(u"{0} playlists updated", len(self._matched_playlists))
Beispiel #46
0
 def write_file_mtime(self, path, mtime):
     """Write the given mtime to the destination path.
     """
     stat = os.stat(util.syspath(path))
     os.utime(util.syspath(path), (stat.st_atime, mtime))
Beispiel #47
0
 def assertExists(self, path):  # noqa
     self.assertTrue(os.path.exists(util.syspath(path)),
                     u'file does not exist: {!r}'.format(path))
Beispiel #48
0
def read_tasks(session):
    """A generator yielding all the albums (as ImportTask objects) found
    in the user-specified list of paths. In the case of a singleton
    import, yields single-item tasks instead.
    """
    # Look for saved incremental directories.
    if session.config['incremental']:
        incremental_skipped = 0
        history_dirs = history_get()

    for toppath in session.paths:
        # Extract archives.
        archive_task = None
        if ArchiveImportTask.is_archive(syspath(toppath)):
            if not (session.config['move'] or session.config['copy']):
                log.warn("Archive importing requires either "
                         "'copy' or 'move' to be enabled.")
                continue

            log.debug('extracting archive {0}'.format(
                displayable_path(toppath)))
            archive_task = ArchiveImportTask(toppath)
            try:
                archive_task.extract()
            except Exception as exc:
                log.error('extraction failed: {0}'.format(exc))
                continue

            # Continue reading albums from the extracted directory.
            toppath = archive_task.toppath

        # Check whether the path is to a file.
        if not os.path.isdir(syspath(toppath)):
            try:
                item = library.Item.from_path(toppath)
            except mediafile.UnreadableFileError:
                log.warn(u'unreadable file: {0}'.format(
                    util.displayable_path(toppath)))
                continue
            if session.config['singletons']:
                yield SingletonImportTask(item)
            else:
                yield ImportTask(toppath, [toppath], [item])
            continue

        # A flat album import merges all items into one album.
        if session.config['flat'] and not session.config['singletons']:
            all_items = []
            for _, items in autotag.albums_in_dir(toppath):
                all_items += items
            if all_items:
                yield ImportTask(toppath, [toppath], all_items)
                yield SentinelImportTask(toppath)
            continue

        resume_dir = None
        if session.want_resume:
            resume_dir = progress_get(toppath)
            if resume_dir:
                # Either accept immediately or prompt for input to decide.
                if session.want_resume is True or \
                   session.should_resume(toppath):
                    log.warn('Resuming interrupted import of %s' % toppath)
                else:
                    # Clear progress; we're starting from the top.
                    resume_dir = None
                    progress_set(toppath, None)

        # Produce paths under this directory.
        for paths, items in autotag.albums_in_dir(toppath):
            # Skip according to progress.
            if session.want_resume and resume_dir:
                # We're fast-forwarding to resume a previous tagging.
                if paths == resume_dir:
                    # We've hit the last good path! Turn off the
                    # fast-forwarding.
                    resume_dir = None
                continue

            # When incremental, skip paths in the history.
            if session.config['incremental'] \
               and tuple(paths) in history_dirs:
                log.debug(u'Skipping previously-imported path: %s' %
                          displayable_path(paths))
                incremental_skipped += 1
                continue

            # Yield all the necessary tasks.
            if session.config['singletons']:
                for item in items:
                    yield SingletonImportTask(item)
                yield SentinelImportTask(toppath, paths)
            else:
                yield ImportTask(toppath, paths, items)

        # Indicate the directory is finished.
        # FIXME hack to delete extracted archives
        if archive_task is None:
            yield SentinelImportTask(toppath)
        else:
            yield archive_task

    # Show skipped directories.
    if session.config['incremental'] and incremental_skipped:
        log.info(u'Incremental import: skipped %i directories.' %
                 incremental_skipped)
Beispiel #49
0
 def test_write_custom_tags(self):
     item = self.add_item_fixture(artist='old artist')
     item.write(tags={'artist': 'new artist'})
     self.assertNotEqual(item.artist, 'new artist')
     self.assertEqual(MediaFile(syspath(item.path)).artist, 'new artist')
Beispiel #50
0
    def copy_album_art(self,
                       album,
                       dest_dir,
                       path_formats,
                       pretend=False,
                       link=False,
                       hardlink=False):
        """Copies or converts the associated cover art of the album. Album must
        have at least one track.
        """
        if not album or not album.artpath:
            return

        album_item = album.items().get()
        # Album shouldn't be empty.
        if not album_item:
            return

        # Get the destination of the first item (track) of the album, we use
        # this function to format the path accordingly to path_formats.
        dest = album_item.destination(basedir=dest_dir,
                                      path_formats=path_formats)

        # Remove item from the path.
        dest = os.path.join(*util.components(dest)[:-1])

        dest = album.art_destination(album.artpath, item_dir=dest)
        if album.artpath == dest:
            return

        if not pretend:
            util.mkdirall(dest)

        if os.path.exists(util.syspath(dest)):
            self._log.info('Skipping {0} (target file exists)',
                           util.displayable_path(album.artpath))
            return

        # Decide whether we need to resize the cover-art image.
        maxwidth = self._get_art_resize(album.artpath)

        # Either copy or resize (while copying) the image.
        if maxwidth is not None:
            self._log.info('Resizing cover art from {0} to {1}',
                           util.displayable_path(album.artpath),
                           util.displayable_path(dest))
            if not pretend:
                ArtResizer.shared.resize(maxwidth, album.artpath, dest)
        else:
            if pretend:
                msg = 'ln' if hardlink else ('ln -s' if link else 'cp')

                self._log.info('{2} {0} {1}',
                               util.displayable_path(album.artpath),
                               util.displayable_path(dest), msg)
            else:
                msg = 'Hardlinking' if hardlink \
                    else ('Linking' if link else 'Copying')

                self._log.info('{2} cover art from {0} to {1}',
                               util.displayable_path(album.artpath),
                               util.displayable_path(dest), msg)
                if hardlink:
                    util.hardlink(album.artpath, dest)
                elif link:
                    util.link(album.artpath, dest)
                else:
                    util.copy(album.artpath, dest)
Beispiel #51
0
def update_items(lib, query, album, move, pretend):
    """For all the items matched by the query, update the library to
    reflect the item's embedded tags.
    """
    with lib.transaction():
        items, _ = _do_query(lib, query, album)

        # Walk through the items and pick up their changes.
        affected_albums = set()
        for item in items:
            # Item deleted?
            if not os.path.exists(syspath(item.path)):
                ui.print_obj(item, lib)
                if not pretend:
                    lib.remove(item, True)
                affected_albums.add(item.album_id)
                continue

            # Did the item change since last checked?
            if item.current_mtime() <= item.mtime:
                log.debug(u'skipping %s because mtime is up to date (%i)' %
                          (displayable_path(item.path), item.mtime))
                continue

            # Read new data.
            old_data = dict(item)
            try:
                item.read()
            except Exception as exc:
                log.error(u'error reading {0}: {1}'.format(
                    displayable_path(item.path), exc))
                continue

            # Special-case album artist when it matches track artist. (Hacky
            # but necessary for preserving album-level metadata for non-
            # autotagged imports.)
            if not item.albumartist and \
                    old_data['albumartist'] == old_data['artist'] == \
                        item.artist:
                item.albumartist = old_data['albumartist']
                item._dirty.remove('albumartist')

            # Get and save metadata changes.
            changes = {}
            for key in library.ITEM_KEYS_META:
                if key in item._dirty:
                    changes[key] = old_data[key], getattr(item, key)
            if changes:
                # Something changed.
                ui.print_obj(item, lib)
                for key, (oldval, newval) in changes.iteritems():
                    _showdiff(key, oldval, newval)

                # If we're just pretending, then don't move or save.
                if pretend:
                    continue

                # Move the item if it's in the library.
                if move and lib.directory in ancestry(item.path):
                    lib.move(item)

                item.store()
                affected_albums.add(item.album_id)
            elif not pretend:
                # The file's mtime was different, but there were no changes
                # to the metadata. Store the new mtime, which is set in the
                # call to read(), so we don't check this again in the
                # future.
                item.store()

        # Skip album changes while pretending.
        if pretend:
            return

        # Modify affected albums to reflect changes in their items.
        for album_id in affected_albums:
            if album_id is None:  # Singletons.
                continue
            album = lib.get_album(album_id)
            if not album:  # Empty albums have already been removed.
                log.debug('emptied album %i' % album_id)
                continue
            first_item = album.items().get()

            # Update album structure to reflect an item in it.
            for key in library.ALBUM_KEYS_ITEM:
                album[key] = first_item[key]
            album.store()

            # Move album art (and any inconsistent items).
            if move and lib.directory in ancestry(first_item.path):
                log.debug('moving album %i' % album_id)
                album.move()
Beispiel #52
0
def read_tasks(session):
    """A generator yielding all the albums (as ImportTask objects) found
    in the user-specified list of paths. In the case of a singleton
    import, yields single-item tasks instead.
    """
    # Look for saved progress.
    if _resume():
        resume_dirs = {}
        for path in session.paths:
            resume_dir = progress_get(path)
            if resume_dir:

                # Either accept immediately or prompt for input to decide.
                if _resume() == True:
                    do_resume = True
                    log.warn('Resuming interrupted import of %s' % path)
                else:
                    do_resume = session.should_resume(path)

                if do_resume:
                    resume_dirs[path] = resume_dir
                else:
                    # Clear progress; we're starting from the top.
                    progress_set(path, None)

    # Look for saved incremental directories.
    if config['import']['incremental']:
        incremental_skipped = 0
        history_dirs = history_get()

    for toppath in session.paths:
        # Check whether the path is to a file.
        if config['import']['singletons'] and \
                not os.path.isdir(syspath(toppath)):
            try:
                item = library.Item.from_path(toppath)
            except UnreadableFileError:
                log.warn(u'unreadable file: {0}'.format(
                    util.displayable_path(toppath)
                ))
                continue
            yield ImportTask.item_task(item)
            continue

        # A flat album import merges all items into one album.
        if config['import']['flat'] and not config['import']['singletons']:
            all_items = []
            for _, items in autotag.albums_in_dir(toppath):
                all_items += items
            yield ImportTask(toppath, toppath, all_items)
            yield ImportTask.done_sentinel(toppath)
            continue

        # Produce paths under this directory.
        if _resume():
            resume_dir = resume_dirs.get(toppath)
        for path, items in autotag.albums_in_dir(toppath):
            # Skip according to progress.
            if _resume() and resume_dir:
                # We're fast-forwarding to resume a previous tagging.
                if path == resume_dir:
                    # We've hit the last good path! Turn off the
                    # fast-forwarding.
                    resume_dir = None
                continue

            # When incremental, skip paths in the history.
            if config['import']['incremental'] and tuple(path) in history_dirs:
                log.debug(u'Skipping previously-imported path: %s' %
                          displayable_path(path))
                incremental_skipped += 1
                continue

            # Yield all the necessary tasks.
            if config['import']['singletons']:
                for item in items:
                    yield ImportTask.item_task(item)
                yield ImportTask.progress_sentinel(toppath, path)
            else:
                yield ImportTask(toppath, path, items)

        # Indicate the directory is finished.
        yield ImportTask.done_sentinel(toppath)

    # Show skipped directories.
    if config['import']['incremental'] and incremental_skipped:
        log.info(u'Incremental import: skipped %i directories.' %
                 incremental_skipped)
Beispiel #53
0
    def convert_item(self,
                     dest_dir,
                     keep_new,
                     path_formats,
                     fmt,
                     pretend=False,
                     link=False,
                     hardlink=False):
        """A pipeline thread that converts `Item` objects from a
        library.
        """
        command, ext = get_format(fmt)
        item, original, converted = None, None, None
        while True:
            item = yield (item, original, converted)
            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 = item.path
                if should_transcode(item, fmt):
                    converted = replace_ext(converted, ext)
            else:
                original = item.path
                if should_transcode(item, fmt):
                    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.)
            if not pretend:
                with _fs_lock:
                    util.mkdirall(dest)

            if os.path.exists(util.syspath(dest)):
                self._log.info(u'Skipping {0} (target file exists)',
                               util.displayable_path(item.path))
                continue

            if keep_new:
                if pretend:
                    self._log.info(u'mv {0} {1}',
                                   util.displayable_path(item.path),
                                   util.displayable_path(original))
                else:
                    self._log.info(u'Moving to {0}',
                                   util.displayable_path(original))
                    util.move(item.path, original)

            if should_transcode(item, fmt):
                linked = False
                try:
                    self.encode(command, original, converted, pretend)
                except subprocess.CalledProcessError:
                    continue
            else:
                linked = link or hardlink
                if pretend:
                    msg = 'ln' if hardlink else ('ln -s' if link else 'cp')

                    self._log.info(u'{2} {0} {1}',
                                   util.displayable_path(original),
                                   util.displayable_path(converted), msg)
                else:
                    # No transcoding necessary.
                    msg = 'Hardlinking' if hardlink \
                        else ('Linking' if link else 'Copying')

                    self._log.info(u'{1} {0}',
                                   util.displayable_path(item.path), msg)

                    if hardlink:
                        util.hardlink(original, converted)
                    elif link:
                        util.link(original, converted)
                    else:
                        util.copy(original, converted)

            if pretend:
                continue

            id3v23 = self.config['id3v23'].as_choice([True, False, 'inherit'])
            if id3v23 == 'inherit':
                id3v23 = None

            # Write tags from the database to the converted file.
            item.try_write(path=converted, id3v23=id3v23)

            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 self.config['embed'] and not linked:
                album = item.get_album()
                if album and album.artpath:
                    self._log.debug(u'embedding album art from {}',
                                    util.displayable_path(album.artpath))
                    art.embed_item(self._log,
                                   item,
                                   album.artpath,
                                   itempath=converted,
                                   id3v23=id3v23)

            if keep_new:
                plugins.send('after_convert',
                             item=item,
                             dest=dest,
                             keepnew=True)
            else:
                plugins.send('after_convert',
                             item=item,
                             dest=converted,
                             keepnew=False)
Beispiel #54
0
def check_art_similarity(log, item, imagepath, compare_threshold):
    """A boolean indicating if an image is similar to embedded item art.
    """
    with NamedTemporaryFile(delete=True) as f:
        art = extract(log, f.name, item)

        if art:
            is_windows = platform.system() == "Windows"

            # Converting images to grayscale tends to minimize the weight
            # of colors in the diff score. So we first convert both images
            # to grayscale and then pipe them into the `compare` command.
            # On Windows, ImageMagick doesn't support the magic \\?\ prefix
            # on paths, so we pass `prefix=False` to `syspath`.
            convert_cmd = [
                'convert',
                syspath(imagepath, prefix=False),
                syspath(art, prefix=False), '-colorspace', 'gray', 'MIFF:-'
            ]
            compare_cmd = ['compare', '-metric', 'PHASH', '-', 'null:']
            log.debug(u'comparing images with pipeline {} | {}', convert_cmd,
                      compare_cmd)
            convert_proc = subprocess.Popen(
                convert_cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                close_fds=not is_windows,
            )
            compare_proc = subprocess.Popen(
                compare_cmd,
                stdin=convert_proc.stdout,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                close_fds=not is_windows,
            )

            # Check the convert output. We're not interested in the
            # standard output; that gets piped to the next stage.
            convert_proc.stdout.close()
            convert_stderr = convert_proc.stderr.read()
            convert_proc.stderr.close()
            convert_proc.wait()
            if convert_proc.returncode:
                log.debug(
                    u'ImageMagick convert failed with status {}: {!r}',
                    convert_proc.returncode,
                    convert_stderr,
                )
                return

            # Check the compare output.
            stdout, stderr = compare_proc.communicate()
            if compare_proc.returncode:
                if compare_proc.returncode != 1:
                    log.debug(u'ImageMagick compare failed: {0}, {1}',
                              displayable_path(imagepath),
                              displayable_path(art))
                    return
                out_str = stderr
            else:
                out_str = stdout

            try:
                phash_diff = float(out_str)
            except ValueError:
                log.debug(u'IM output is not a number: {0!r}', out_str)
                return

            log.debug(u'ImageMagick compare score: {0}', phash_diff)
            return phash_diff <= compare_threshold

    return True
Beispiel #55
0
def import_files(lib, paths, copy, move, write, autot, logpath, art, threaded,
                 color, delete, quiet, resume, quiet_fallback, singletons,
                 timid, query, incremental, ignore, per_disc_numbering):
    """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 ASIS or SKIP and indicates what
    should happen in quiet mode when the recommendation is not strong.
    """
    # Check the user-specified directories.
    for path in paths:
        if not singletons and not os.path.isdir(syspath(path)):
            raise ui.UserError('not a directory: ' + path)
        elif singletons and not os.path.exists(syspath(path)):
            raise ui.UserError('no such file: ' + path)

    # Check parameter consistency.
    if quiet and timid:
        raise ui.UserError("can't be both quiet and timid")

    # Open the log.
    if logpath:
        logpath = normpath(logpath)
        try:
            logfile = open(syspath(logpath), 'a')
        except IOError:
            raise ui.UserError(u"could not open log file for writing: %s" %
                               displayable_path(logpath))
        print('import started', time.asctime(), file=logfile)
    else:
        logfile = None

    # Never ask for input in quiet mode.
    if resume is None and quiet:
        resume = False

    try:
        # Perform the import.
        importer.run_import(
            lib=lib,
            paths=paths,
            resume=resume,
            logfile=logfile,
            color=color,
            quiet=quiet,
            quiet_fallback=quiet_fallback,
            copy=copy,
            move=move,
            write=write,
            art=art,
            delete=delete,
            threaded=threaded,
            autot=autot,
            choose_match_func=choose_match,
            should_resume_func=should_resume,
            singletons=singletons,
            timid=timid,
            choose_item_func=choose_item,
            query=query,
            incremental=incremental,
            ignore=ignore,
            resolve_duplicate_func=resolve_duplicate,
            per_disc_numbering=per_disc_numbering,
        )

    finally:
        # If we were logging, close the file.
        if logfile:
            print('', file=logfile)
            logfile.close()

    # Emit event.
    plugins.send('import', lib=lib, paths=paths)
Beispiel #56
0
def resize_image(log, imagepath, maxwidth):
    """Returns path to an image resized to maxwidth.
    """
    log.debug(u'Resizing album art to {0} pixels wide', maxwidth)
    imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath))
    return imagepath
Beispiel #57
0
 def test_syspath_windows_format(self):
     with _common.platform_windows():
         path = os.path.join(u'a', u'b', u'c')
         outpath = util.syspath(path)
     self.assertTrue(isinstance(outpath, unicode))
     self.assertTrue(outpath.startswith(u'\\\\?\\'))
Beispiel #58
0
def read_tasks(config):
    """A generator yielding all the albums (as ImportTask objects) found
    in the user-specified list of paths. In the case of a singleton
    import, yields single-item tasks instead.
    """
    # Look for saved progress.
    progress = config.resume is not False
    if progress:
        resume_dirs = {}
        for path in config.paths:
            resume_dir = progress_get(path)
            if resume_dir:

                # Either accept immediately or prompt for input to decide.
                if config.resume:
                    do_resume = True
                    log.warn('Resuming interrupted import of %s' % path)
                else:
                    do_resume = config.should_resume_func(config, path)

                if do_resume:
                    resume_dirs[path] = resume_dir
                else:
                    # Clear progress; we're starting from the top.
                    progress_set(path, None)

    # Look for saved incremental directories.
    if config.incremental:
        incremental_skipped = 0
        history_dirs = history_get()

    for toppath in config.paths:
        # Check whether the path is to a file.
        if config.singletons and not os.path.isdir(syspath(toppath)):
            item = library.Item.from_path(toppath)
            yield ImportTask.item_task(item)
            continue

        # Produce paths under this directory.
        if progress:
            resume_dir = resume_dirs.get(toppath)
        for path, items in autotag.albums_in_dir(toppath, config.ignore):
            # Skip according to progress.
            if progress and resume_dir:
                # We're fast-forwarding to resume a previous tagging.
                if path == resume_dir:
                    # We've hit the last good path! Turn off the
                    # fast-forwarding.
                    resume_dir = None
                continue

            # When incremental, skip paths in the history.
            if config.incremental and path in history_dirs:
                log.debug(u'Skipping previously-imported path: %s' %
                          displayable_path(path))
                incremental_skipped += 1
                continue

            # Yield all the necessary tasks.
            if config.singletons:
                for item in items:
                    yield ImportTask.item_task(item)
                yield ImportTask.progress_sentinel(toppath, path)
            else:
                yield ImportTask(toppath, path, items)

        # Indicate the directory is finished.
        yield ImportTask.done_sentinel(toppath)

    # Show skipped directories.
    if config.incremental and incremental_skipped:
        log.info(u'Incremental import: skipped %i directories.' %
                 incremental_skipped)
Beispiel #59
0
 def _setup_data(self, artpath=None):
     if not artpath:
         artpath = self.small_artpath
     with open(syspath(artpath)) as f:
         self.image_data = f.read()
Beispiel #60
0
 def assertIsFile(self: TestCase, path):
     self.assertTrue(os.path.isfile(syspath(path)),
                     msg=u'Path is not a file: {0}'.format(
                         displayable_path(path)))