Skip to content

Commit

Permalink
Merge branch 'master' into formatted-modify
Browse files Browse the repository at this point in the history
  • Loading branch information
sampsyo authored Aug 20, 2022
2 parents 0456c8f + 4761c35 commit 7af40db
Show file tree
Hide file tree
Showing 95 changed files with 5,015 additions and 1,354 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ downloads/
eggs/
.eggs/
lib/
lib64/
lib64
parts/
sdist/
var/
Expand Down
3 changes: 1 addition & 2 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,10 @@ There are a few coding conventions we use in beets:
instead. In particular, we have our own logging shim, so you’ll see
``from beets import logging`` in most files.

- Always log Unicode strings (e.g., ``log.debug(u"hello world")``).
- The loggers use
`str.format <http://docs.python.org/library/stdtypes.html#str.format>`__-style
logging instead of ``%``-style, so you can type
``log.debug(u"{0}", obj)`` to do your formatting.
``log.debug("{0}", obj)`` to do your formatting.

- Exception handlers must use ``except A as B:`` instead of
``except A, B:``.
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
11 changes: 11 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Security Policy

## Supported Versions

We currently support only the latest release of beets.

## Reporting a Vulnerability

To report a security vulnerability, please send email to [our Zulip team][z].

[z]: mailto:email.218c36e48d78cf125c0a6219a6c2a417.show-sender@streams.zulipchat.com
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.5.1'
__version__ = '1.6.1'
__author__ = 'Adrian Sampson <[email protected]>'


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 @@ -55,6 +55,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 @@ -101,6 +106,7 @@ paths:
statefile: state.pickle

musicbrainz:
enabled: yes
host: musicbrainz.org
https: no
ratelimit: 1
Expand Down
21 changes: 19 additions & 2 deletions beets/dbcore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,23 @@ def string_match(cls, pattern, value):
raise NotImplementedError()


class StringQuery(StringFieldQuery):
"""A query that matches a whole string in a specific item field."""

def col_clause(self):
search = (self.pattern
.replace('\\', '\\\\')
.replace('%', '\\%')
.replace('_', '\\_'))
clause = self.field + " like ? escape '\\'"
subvals = [search]
return clause, subvals

@classmethod
def string_match(cls, pattern, value):
return pattern.lower() == value.lower()


class SubstringQuery(StringFieldQuery):
"""A query that matches a substring in a specific item field."""

Expand Down Expand Up @@ -443,7 +460,7 @@ def clause(self):
return self.clause_with_joiner('and')

def match(self, item):
return all([q.match(item) for q in self.subqueries])
return all(q.match(item) for q in self.subqueries)


class OrQuery(MutableCollectionQuery):
Expand All @@ -453,7 +470,7 @@ def clause(self):
return self.clause_with_joiner('or')

def match(self, item):
return any([q.match(item) for q in self.subqueries])
return any(q.match(item) for q in self.subqueries)


class NotQuery(Query):
Expand Down
Loading

0 comments on commit 7af40db

Please sign in to comment.