Skip to content

Commit

Permalink
Merge branch 'master' into duplicate
Browse files Browse the repository at this point in the history
  • Loading branch information
sampsyo committed Aug 21, 2022
2 parents 3c945cb + 6e0f7a1 commit 1054b72
Show file tree
Hide file tree
Showing 65 changed files with 2,530 additions and 726 deletions.
18 changes: 9 additions & 9 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest, windows-latest]
python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-rc.2]
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev']

env:
PY_COLORS: 1
Expand Down Expand Up @@ -45,17 +45,17 @@ jobs:
sudo apt install ffmpeg # For replaygain
- name: Test older Python versions with tox
if: matrix.python-version != '3.9' && matrix.python-version != '3.10.0-rc.2'
if: matrix.python-version != '3.10' && matrix.python-version != '3.11-dev'
run: |
tox -e py-test
- name: Test latest Python version with tox and get coverage
if: matrix.python-version == '3.9'
if: matrix.python-version == '3.10'
run: |
tox -vv -e py-cov
- name: Test nightly Python version with tox
if: matrix.python-version == '3.10.0-rc.2'
if: matrix.python-version == '3.11-dev'
# continue-on-error is not ideal since it doesn't give a visible
# warning, but there doesn't seem to be anything better:
# https://github.com/actions/toolkit/issues/399
Expand All @@ -64,7 +64,7 @@ jobs:
tox -e py-test
- name: Upload code coverage
if: matrix.python-version == '3.9'
if: matrix.python-version == '3.10'
run: |
pip install codecov || true
codecov || true
Expand All @@ -78,10 +78,10 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Set up Python 3.9
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: '3.10'

- name: Install base dependencies
run: |
Expand All @@ -100,10 +100,10 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Set up Python 3.9
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: '3.10'

- name: Install base dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ shockingly simple if you know a little Python.
.. _writing your own plugin:
https://beets.readthedocs.org/page/dev/plugins.html
.. _HTML5 Audio:
http://www.w3.org/TR/html-markup/audio.html
https://html.spec.whatwg.org/multipage/media.html#the-audio-element
.. _albums that are missing tracks:
https://beets.readthedocs.org/page/plugins/missing.html
.. _duplicate tracks and albums:
Expand Down
2 changes: 1 addition & 1 deletion README_kr.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Beets는 라이브러리로 디자인 되었기 때문에, 당신이 음악들
.. _writing your own plugin:
https://beets.readthedocs.org/page/dev/plugins.html
.. _HTML5 Audio:
http://www.w3.org/TR/html-markup/audio.html
https://html.spec.whatwg.org/multipage/media.html#the-audio-element
.. _albums that are missing tracks:
https://beets.readthedocs.org/page/plugins/missing.html
.. _duplicate tracks and albums:
Expand Down
2 changes: 1 addition & 1 deletion beets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import confuse
from sys import stderr

__version__ = '1.6.0'
__version__ = '1.6.1'
__author__ = 'Adrian Sampson <adrian@radbox.org>'


Expand Down
98 changes: 29 additions & 69 deletions beets/art.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
"""


import subprocess
import platform
from tempfile import NamedTemporaryFile
import os

Expand Down Expand Up @@ -53,14 +51,22 @@ def embed_item(log, item, imagepath, maxwidth=None, itempath=None,
quality=0):
"""Embed an image into the item's media file.
"""
# Conditions and filters.
# Conditions.
if compare_threshold:
if not check_art_similarity(log, item, imagepath, compare_threshold):
is_similar = check_art_similarity(
log, item, imagepath, compare_threshold)
if is_similar is None:
log.warning('Error while checking art similarity; skipping.')
return
elif not is_similar:
log.info('Image not similar; skipping.')
return

if ifempty and get_art(log, item):
log.info('media file already contained art')
return

# Filters.
if maxwidth and not as_album:
imagepath = resize_image(log, imagepath, maxwidth, quality)

Expand Down Expand Up @@ -115,76 +121,30 @@ def resize_image(log, imagepath, maxwidth, quality):
return imagepath


def check_art_similarity(log, item, imagepath, compare_threshold):
def check_art_similarity(
log,
item,
imagepath,
compare_threshold,
artresizer=None,
):
"""A boolean indicating if an image is similar to embedded item art.
If no embedded art exists, always return `True`. If the comparison fails
for some reason, the return value is `None`.
This must only be called if `ArtResizer.shared.can_compare` is `True`.
"""
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('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(
'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('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('IM output is not a number: {0!r}', out_str)
return

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

return True
if not art:
return True

if artresizer is None:
artresizer = ArtResizer.shared

return artresizer.compare(art, imagepath, compare_threshold)


def extract(log, outpath, item):
Expand Down
43 changes: 21 additions & 22 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,14 @@ def tracks_for_id(track_id):
yield t


def invoke_mb(call_func, *args):
try:
return call_func(*args)
except mb.MusicBrainzAPIError as exc:
exc.log(log)
return ()


@plugins.notify_info_yielded('albuminfo_received')
def album_candidates(items, artist, album, va_likely, extra_tags):
"""Search for album matches. ``items`` is a list of Item objects
Expand All @@ -609,25 +617,19 @@ def album_candidates(items, artist, album, va_likely, extra_tags):
constrain the search.
"""

# Base candidates if we have album and artist to match.
if artist and album:
try:
yield from mb.match_album(artist, album, len(items),
extra_tags)
except mb.MusicBrainzAPIError as exc:
exc.log(log)

# Also add VA matches from MusicBrainz where appropriate.
if va_likely and album:
try:
yield from mb.match_album(None, album, len(items),
extra_tags)
except mb.MusicBrainzAPIError as exc:
exc.log(log)
if config["musicbrainz"]["enabled"]:
# Base candidates if we have album and artist to match.
if artist and album:
yield from invoke_mb(mb.match_album, artist, album, len(items),
extra_tags)

# Also add VA matches from MusicBrainz where appropriate.
if va_likely and album:
yield from invoke_mb(mb.match_album, None, album, len(items),
extra_tags)

# Candidates from plugins.
yield from plugins.candidates(items, artist, album, va_likely,
extra_tags)
yield from plugins.candidates(items, artist, album, va_likely, extra_tags)


@plugins.notify_info_yielded('trackinfo_received')
Expand All @@ -638,11 +640,8 @@ def item_candidates(item, artist, title):
"""

# MusicBrainz candidates.
if artist and title:
try:
yield from mb.match_track(artist, title)
except mb.MusicBrainzAPIError as exc:
exc.log(log)
if config["musicbrainz"]["enabled"] and artist and title:
yield from invoke_mb(mb.match_track, artist, title)

# Plugin candidates.
yield from plugins.item_candidates(item, artist, title)
4 changes: 2 additions & 2 deletions beets/autotag/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ def _add_candidate(items, results, info):
log.debug('No tracks.')
return

# Don't duplicate.
if info.album_id in results:
# Prevent duplicates.
if info.album_id and info.album_id in results:
log.debug('Duplicate.')
return

Expand Down
6 changes: 6 additions & 0 deletions beets/config_default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ aunique:
disambiguators: albumtype year label catalognum albumdisambig releasegroupdisambig
bracket: '[]'

sunique:
keys: artist title
disambiguators: year trackdisambig
bracket: '[]'

overwrite_null:
album: []
track: []
Expand Down Expand Up @@ -104,6 +109,7 @@ paths:
statefile: state.pickle

musicbrainz:
enabled: yes
host: musicbrainz.org
https: no
ratelimit: 1
Expand Down
10 changes: 5 additions & 5 deletions beets/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,9 +585,9 @@ def set_fields(self, lib):
displayable_path(self.paths),
field,
value)
self.album[field] = value
self.album.set_parse(field, format(self.album, value))
for item in items:
item[field] = value
item.set_parse(field, format(item, value))
with lib.transaction():
for item in items:
item.store()
Expand Down Expand Up @@ -735,8 +735,8 @@ def align_album_level_fields(self):
item.update(changes)

def manipulate_files(self, operation=None, write=False, session=None):
""" Copy, move, link, hardlink or reflink (depending on `operation`) the files
as well as write metadata.
""" Copy, move, link, hardlink or reflink (depending on `operation`)
the files as well as write metadata.
`operation` should be an instance of `util.MoveOperation`.
Expand Down Expand Up @@ -982,7 +982,7 @@ def set_fields(self, lib):
displayable_path(self.paths),
field,
value)
self.item[field] = value
self.item.set_parse(field, format(self.item, value))
self.item.store()


Expand Down
Loading

0 comments on commit 1054b72

Please sign in to comment.