mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 00:53:08 +01:00
Merge branch 'beetbox:master' into arm-info
This commit is contained in:
commit
eafae03560
32 changed files with 1027 additions and 906 deletions
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
name: Test
|
name: Test
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
@ -75,4 +75,4 @@ jobs:
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
use_oidc: true
|
use_oidc: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }}
|
||||||
|
|
|
||||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
name: Lint check
|
name: Lint check
|
||||||
run-name: Lint code
|
run-name: Lint code
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
@ -125,7 +125,7 @@ jobs:
|
||||||
cache: poetry
|
cache: poetry
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: poetry install --only=docs
|
run: poetry install --extras=docs
|
||||||
|
|
||||||
- name: Add Sphinx problem matcher
|
- name: Add Sphinx problem matcher
|
||||||
run: echo "::add-matcher::.github/sphinx-problem-matcher.json"
|
run: echo "::add-matcher::.github/sphinx-problem-matcher.json"
|
||||||
|
|
|
||||||
28
.github/workflows/make_release.yaml
vendored
28
.github/workflows/make_release.yaml
vendored
|
|
@ -40,8 +40,13 @@ jobs:
|
||||||
name: Get changelog and build the distribution package
|
name: Get changelog and build the distribution package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: increment-version
|
needs: increment-version
|
||||||
|
outputs:
|
||||||
|
changelog: ${{ steps.generate_changelog.outputs.changelog }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
|
||||||
- name: Install Python tools
|
- name: Install Python tools
|
||||||
uses: BrandonLWhite/pipx-install-action@v0.1.1
|
uses: BrandonLWhite/pipx-install-action@v0.1.1
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
|
|
@ -50,16 +55,22 @@ jobs:
|
||||||
cache: poetry
|
cache: poetry
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: poetry install --only=release
|
run: poetry install --with=release --extras=docs
|
||||||
|
|
||||||
- name: Install pandoc
|
- name: Install pandoc
|
||||||
run: sudo apt update && sudo apt install pandoc -y
|
run: sudo apt update && sudo apt install pandoc -y
|
||||||
|
|
||||||
- name: Obtain the changelog
|
- name: Obtain the changelog
|
||||||
run: echo "changelog=$(poe changelog)" >> $GITHUB_OUTPUT
|
id: generate_changelog
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo 'changelog<<EOF'
|
||||||
|
poe changelog
|
||||||
|
echo EOF
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Build a binary wheel and a source tarball
|
- name: Build a binary wheel and a source tarball
|
||||||
run: poetry build
|
run: poe build
|
||||||
|
|
||||||
- name: Store the distribution packages
|
- name: Store the distribution packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
@ -88,19 +99,23 @@ jobs:
|
||||||
make-github-release:
|
make-github-release:
|
||||||
name: Create GitHub release
|
name: Create GitHub release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: publish-to-pypi
|
needs: [build, publish-to-pypi]
|
||||||
env:
|
env:
|
||||||
CHANGELOG: ${{ needs.build.outputs.changelog }}
|
CHANGELOG: ${{ needs.build.outputs.changelog }}
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
|
||||||
- name: Tag the commit
|
- name: Tag the commit
|
||||||
id: tag_version
|
id: tag_version
|
||||||
uses: mathieudutour/github-tag-action@v6
|
uses: mathieudutour/github-tag-action@v6.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
custom_tag: ${{ env.NEW_VERSION }}
|
custom_tag: ${{ env.NEW_VERSION }}
|
||||||
|
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|
@ -117,6 +132,7 @@ jobs:
|
||||||
artifacts: dist/*
|
artifacts: dist/*
|
||||||
- name: Send release toot to Fosstodon
|
- name: Send release toot to Fosstodon
|
||||||
uses: cbrgm/mastodon-github-action@v2
|
uses: cbrgm/mastodon-github-action@v2
|
||||||
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }}
|
access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }}
|
||||||
url: ${{ secrets.MASTODON_URL }}
|
url: ${{ secrets.MASTODON_URL }}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.6.6
|
rev: v0.8.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from sys import stderr
|
||||||
|
|
||||||
import confuse
|
import confuse
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.2.0"
|
||||||
__author__ = "Adrian Sampson <adrian@radbox.org>"
|
__author__ = "Adrian Sampson <adrian@radbox.org>"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,10 @@ def track_distance(
|
||||||
if item.mb_trackid:
|
if item.mb_trackid:
|
||||||
dist.add_expr("track_id", item.mb_trackid != track_info.track_id)
|
dist.add_expr("track_id", item.mb_trackid != track_info.track_id)
|
||||||
|
|
||||||
|
# Penalize mismatching disc numbers.
|
||||||
|
if track_info.medium and item.disc:
|
||||||
|
dist.add_expr("medium", item.disc != track_info.medium)
|
||||||
|
|
||||||
# Plugins.
|
# Plugins.
|
||||||
dist.update(plugins.track_distance(item, track_info))
|
dist.update(plugins.track_distance(item, track_info))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,7 @@ match:
|
||||||
track_index: 1.0
|
track_index: 1.0
|
||||||
track_length: 2.0
|
track_length: 2.0
|
||||||
track_id: 5.0
|
track_id: 5.0
|
||||||
|
medium: 1.0
|
||||||
preferred:
|
preferred:
|
||||||
countries: []
|
countries: []
|
||||||
media: []
|
media: []
|
||||||
|
|
|
||||||
|
|
@ -605,7 +605,7 @@ class ImportTask(BaseImportTask):
|
||||||
"""
|
"""
|
||||||
items = self.imported_items()
|
items = self.imported_items()
|
||||||
for field, view in config["import"]["set_fields"].items():
|
for field, view in config["import"]["set_fields"].items():
|
||||||
value = view.get()
|
value = str(view.get())
|
||||||
log.debug(
|
log.debug(
|
||||||
"Set field {1}={2} for {0}",
|
"Set field {1}={2} for {0}",
|
||||||
displayable_path(self.paths),
|
displayable_path(self.paths),
|
||||||
|
|
@ -1062,7 +1062,7 @@ class SingletonImportTask(ImportTask):
|
||||||
values, for the singleton item.
|
values, for the singleton item.
|
||||||
"""
|
"""
|
||||||
for field, view in config["import"]["set_fields"].items():
|
for field, view in config["import"]["set_fields"].items():
|
||||||
value = view.get()
|
value = str(view.get())
|
||||||
log.debug(
|
log.debug(
|
||||||
"Set field {1}={2} for {0}",
|
"Set field {1}={2} for {0}",
|
||||||
displayable_path(self.paths),
|
displayable_path(self.paths),
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,12 @@ log = logging.getLogger("beets")
|
||||||
log.propagate = True
|
log.propagate = True
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Dummy item creation.
|
|
||||||
_item_ident = 0
|
|
||||||
|
|
||||||
# OS feature test.
|
# OS feature test.
|
||||||
HAVE_SYMLINK = sys.platform != "win32"
|
HAVE_SYMLINK = sys.platform != "win32"
|
||||||
HAVE_HARDLINK = sys.platform != "win32"
|
HAVE_HARDLINK = sys.platform != "win32"
|
||||||
|
|
||||||
|
|
||||||
def item(lib=None):
|
def item(lib=None):
|
||||||
global _item_ident
|
|
||||||
_item_ident += 1
|
|
||||||
i = beets.library.Item(
|
i = beets.library.Item(
|
||||||
title="the title",
|
title="the title",
|
||||||
artist="the artist",
|
artist="the artist",
|
||||||
|
|
@ -93,7 +88,6 @@ def item(lib=None):
|
||||||
comments="the comments",
|
comments="the comments",
|
||||||
bpm=8,
|
bpm=8,
|
||||||
comp=True,
|
comp=True,
|
||||||
path=f"somepath{_item_ident}",
|
|
||||||
length=60.0,
|
length=60.0,
|
||||||
bitrate=128000,
|
bitrate=128000,
|
||||||
format="FLAC",
|
format="FLAC",
|
||||||
|
|
@ -110,30 +104,6 @@ def item(lib=None):
|
||||||
return i
|
return i
|
||||||
|
|
||||||
|
|
||||||
def album(lib=None):
|
|
||||||
global _item_ident
|
|
||||||
_item_ident += 1
|
|
||||||
i = beets.library.Album(
|
|
||||||
artpath=None,
|
|
||||||
albumartist="some album artist",
|
|
||||||
albumartist_sort="some sort album artist",
|
|
||||||
albumartist_credit="some album artist credit",
|
|
||||||
album="the album",
|
|
||||||
genre="the genre",
|
|
||||||
year=2014,
|
|
||||||
month=2,
|
|
||||||
day=5,
|
|
||||||
tracktotal=0,
|
|
||||||
disctotal=1,
|
|
||||||
comp=False,
|
|
||||||
mb_albumid="someID-1",
|
|
||||||
mb_albumartistid="someID-1",
|
|
||||||
)
|
|
||||||
if lib:
|
|
||||||
lib.add(i)
|
|
||||||
return i
|
|
||||||
|
|
||||||
|
|
||||||
# Dummy import session.
|
# Dummy import session.
|
||||||
def import_session(lib=None, loghandler=None, paths=[], query=[], cli=False):
|
def import_session(lib=None, loghandler=None, paths=[], query=[], cli=False):
|
||||||
cls = commands.TerminalImportSession if cli else importer.ImportSession
|
cls = commands.TerminalImportSession if cli else importer.ImportSession
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,6 @@ information or mock the environment.
|
||||||
|
|
||||||
- `has_program` checks the presence of a command on the system.
|
- `has_program` checks the presence of a command on the system.
|
||||||
|
|
||||||
- The `generate_album_info` and `generate_track_info` functions return
|
|
||||||
fixtures to be used when mocking the autotagger.
|
|
||||||
|
|
||||||
- The `ImportSessionFixture` allows one to run importer code while
|
- The `ImportSessionFixture` allows one to run importer code while
|
||||||
controlling the interactions through code.
|
controlling the interactions through code.
|
||||||
|
|
||||||
|
|
@ -249,16 +246,15 @@ class TestHelper(_common.Assertions):
|
||||||
|
|
||||||
The item is attached to the database from `self.lib`.
|
The item is attached to the database from `self.lib`.
|
||||||
"""
|
"""
|
||||||
item_count = self._get_item_count()
|
|
||||||
values_ = {
|
values_ = {
|
||||||
"title": "t\u00eftle {0}",
|
"title": "t\u00eftle {0}",
|
||||||
"artist": "the \u00e4rtist",
|
"artist": "the \u00e4rtist",
|
||||||
"album": "the \u00e4lbum",
|
"album": "the \u00e4lbum",
|
||||||
"track": item_count,
|
"track": 1,
|
||||||
"format": "MP3",
|
"format": "MP3",
|
||||||
}
|
}
|
||||||
values_.update(values)
|
values_.update(values)
|
||||||
values_["title"] = values_["title"].format(item_count)
|
values_["title"] = values_["title"].format(1)
|
||||||
values_["db"] = self.lib
|
values_["db"] = self.lib
|
||||||
item = Item(**values_)
|
item = Item(**values_)
|
||||||
if "path" not in values:
|
if "path" not in values:
|
||||||
|
|
@ -375,12 +371,6 @@ class TestHelper(_common.Assertions):
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _get_item_count(self):
|
|
||||||
if not hasattr(self, "__item_count"):
|
|
||||||
count = 0
|
|
||||||
self.__item_count = count + 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
# Running beets commands
|
# Running beets commands
|
||||||
|
|
||||||
def run_command(self, *args, **kwargs):
|
def run_command(self, *args, **kwargs):
|
||||||
|
|
@ -513,12 +503,8 @@ class PluginMixin:
|
||||||
Album._queries = getattr(Album, "_original_queries", {})
|
Album._queries = getattr(Album, "_original_queries", {})
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def configure_plugin(self, config: list[Any] | dict[str, Any]):
|
def configure_plugin(self, config: Any):
|
||||||
if isinstance(config, list):
|
beets.config[self.plugin].set(config)
|
||||||
beets.config[self.plugin] = config
|
|
||||||
else:
|
|
||||||
for key, value in config.items():
|
|
||||||
beets.config[self.plugin][key] = value
|
|
||||||
self.load_plugins(self.plugin)
|
self.load_plugins(self.plugin)
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
@ -723,10 +709,6 @@ class ImportSessionFixture(ImportSession):
|
||||||
|
|
||||||
default_resolution = "REMOVE"
|
default_resolution = "REMOVE"
|
||||||
|
|
||||||
def add_resolution(self, resolution):
|
|
||||||
assert isinstance(resolution, self.Resolution)
|
|
||||||
self._resolutions.append(resolution)
|
|
||||||
|
|
||||||
def resolve_duplicate(self, task, found_duplicates):
|
def resolve_duplicate(self, task, found_duplicates):
|
||||||
try:
|
try:
|
||||||
res = self._resolutions.pop(0)
|
res = self._resolutions.pop(0)
|
||||||
|
|
@ -779,12 +761,10 @@ class TerminalImportSessionFixture(TerminalImportSession):
|
||||||
self.io.addinput("T")
|
self.io.addinput("T")
|
||||||
elif choice == importer.action.SKIP:
|
elif choice == importer.action.SKIP:
|
||||||
self.io.addinput("S")
|
self.io.addinput("S")
|
||||||
elif isinstance(choice, int):
|
else:
|
||||||
self.io.addinput("M")
|
self.io.addinput("M")
|
||||||
self.io.addinput(str(choice))
|
self.io.addinput(str(choice))
|
||||||
self._add_choice_input()
|
self._add_choice_input()
|
||||||
else:
|
|
||||||
raise Exception("Unknown choice %s" % choice)
|
|
||||||
|
|
||||||
|
|
||||||
class TerminalImportMixin(ImportHelper):
|
class TerminalImportMixin(ImportHelper):
|
||||||
|
|
@ -803,82 +783,6 @@ class TerminalImportMixin(ImportHelper):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_album_info(album_id, track_values):
|
|
||||||
"""Return `AlbumInfo` populated with mock data.
|
|
||||||
|
|
||||||
Sets the album info's `album_id` field is set to the corresponding
|
|
||||||
argument. For each pair (`id`, `values`) in `track_values` the `TrackInfo`
|
|
||||||
from `generate_track_info` is added to the album info's `tracks` field.
|
|
||||||
Most other fields of the album and track info are set to "album
|
|
||||||
info" and "track info", respectively.
|
|
||||||
"""
|
|
||||||
tracks = [generate_track_info(id, values) for id, values in track_values]
|
|
||||||
album = AlbumInfo(
|
|
||||||
album_id="album info",
|
|
||||||
album="album info",
|
|
||||||
artist="album info",
|
|
||||||
artist_id="album info",
|
|
||||||
tracks=tracks,
|
|
||||||
)
|
|
||||||
for field in ALBUM_INFO_FIELDS:
|
|
||||||
setattr(album, field, "album info")
|
|
||||||
|
|
||||||
return album
|
|
||||||
|
|
||||||
|
|
||||||
ALBUM_INFO_FIELDS = [
|
|
||||||
"album",
|
|
||||||
"album_id",
|
|
||||||
"artist",
|
|
||||||
"artist_id",
|
|
||||||
"asin",
|
|
||||||
"albumtype",
|
|
||||||
"va",
|
|
||||||
"label",
|
|
||||||
"barcode",
|
|
||||||
"artist_sort",
|
|
||||||
"releasegroup_id",
|
|
||||||
"catalognum",
|
|
||||||
"language",
|
|
||||||
"country",
|
|
||||||
"albumstatus",
|
|
||||||
"media",
|
|
||||||
"albumdisambig",
|
|
||||||
"releasegroupdisambig",
|
|
||||||
"artist_credit",
|
|
||||||
"data_source",
|
|
||||||
"data_url",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_track_info(track_id="track info", values={}):
|
|
||||||
"""Return `TrackInfo` populated with mock data.
|
|
||||||
|
|
||||||
The `track_id` field is set to the corresponding argument. All other
|
|
||||||
string fields are set to "track info".
|
|
||||||
"""
|
|
||||||
track = TrackInfo(
|
|
||||||
title="track info",
|
|
||||||
track_id=track_id,
|
|
||||||
)
|
|
||||||
for field in TRACK_INFO_FIELDS:
|
|
||||||
setattr(track, field, "track info")
|
|
||||||
for field, value in values.items():
|
|
||||||
setattr(track, field, value)
|
|
||||||
return track
|
|
||||||
|
|
||||||
|
|
||||||
TRACK_INFO_FIELDS = [
|
|
||||||
"artist",
|
|
||||||
"artist_id",
|
|
||||||
"artist_sort",
|
|
||||||
"disctitle",
|
|
||||||
"artist_credit",
|
|
||||||
"data_source",
|
|
||||||
"data_url",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AutotagStub:
|
class AutotagStub:
|
||||||
"""Stub out MusicBrainz album and track matcher and control what the
|
"""Stub out MusicBrainz album and track matcher and control what the
|
||||||
autotagger returns.
|
autotagger returns.
|
||||||
|
|
@ -992,7 +896,7 @@ class FetchImageHelper:
|
||||||
super().run(*args, **kwargs)
|
super().run(*args, **kwargs)
|
||||||
|
|
||||||
IMAGEHEADER = {
|
IMAGEHEADER = {
|
||||||
"image/jpeg": b"\x00" * 6 + b"JFIF",
|
"image/jpeg": b"\xff\xd8\xff" + b"\x00" * 3 + b"JFIF",
|
||||||
"image/png": b"\211PNG\r\n\032\n",
|
"image/png": b"\211PNG\r\n\032\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1858,6 +1858,14 @@ def main(args=None):
|
||||||
"""Run the main command-line interface for beets. Includes top-level
|
"""Run the main command-line interface for beets. Includes top-level
|
||||||
exception handlers that print friendly error messages.
|
exception handlers that print friendly error messages.
|
||||||
"""
|
"""
|
||||||
|
if "AppData\\Local\\Microsoft\\WindowsApps" in sys.exec_prefix:
|
||||||
|
log.error(
|
||||||
|
"error: beets is unable to use the Microsoft Store version of "
|
||||||
|
"Python. Please install Python from https://python.org.\n"
|
||||||
|
"error: More details can be found here "
|
||||||
|
"https://beets.readthedocs.io/en/stable/guides/main.html"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
try:
|
try:
|
||||||
_raw_main(args)
|
_raw_main(args)
|
||||||
except UserError as exc:
|
except UserError as exc:
|
||||||
|
|
|
||||||
|
|
@ -85,17 +85,21 @@ def get_format(fmt=None):
|
||||||
return (command.encode("utf-8"), extension.encode("utf-8"))
|
return (command.encode("utf-8"), extension.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def in_no_convert(item: Item) -> bool:
|
||||||
|
no_convert_query = config["convert"]["no_convert"].as_str()
|
||||||
|
|
||||||
|
if no_convert_query:
|
||||||
|
query, _ = parse_query_string(no_convert_query, Item)
|
||||||
|
return query.match(item)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def should_transcode(item, fmt):
|
def should_transcode(item, fmt):
|
||||||
"""Determine whether the item should be transcoded as part of
|
"""Determine whether the item should be transcoded as part of
|
||||||
conversion (i.e., its bitrate is high or it has the wrong format).
|
conversion (i.e., its bitrate is high or it has the wrong format).
|
||||||
"""
|
"""
|
||||||
no_convert_queries = config["convert"]["no_convert"].as_str_seq()
|
if in_no_convert(item) or (
|
||||||
if no_convert_queries:
|
|
||||||
for query_string in no_convert_queries:
|
|
||||||
query, _ = parse_query_string(query_string, Item)
|
|
||||||
if query.match(item):
|
|
||||||
return False
|
|
||||||
if (
|
|
||||||
config["convert"]["never_convert_lossy_files"]
|
config["convert"]["never_convert_lossy_files"]
|
||||||
and item.format.lower() not in LOSSLESS_FORMATS
|
and item.format.lower() not in LOSSLESS_FORMATS
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,6 @@ class ListenBrainzPlugin(BeetsPlugin):
|
||||||
return self._make_request(url)
|
return self._make_request(url)
|
||||||
|
|
||||||
def get_listenbrainz_playlists(self):
|
def get_listenbrainz_playlists(self):
|
||||||
"""Returns a list of playlists created by ListenBrainz."""
|
|
||||||
import re
|
|
||||||
|
|
||||||
resp = self.get_playlists_createdfor(self.username)
|
resp = self.get_playlists_createdfor(self.username)
|
||||||
playlists = resp.get("playlists")
|
playlists = resp.get("playlists")
|
||||||
listenbrainz_playlists = []
|
listenbrainz_playlists = []
|
||||||
|
|
@ -159,34 +156,31 @@ class ListenBrainzPlugin(BeetsPlugin):
|
||||||
playlist_info = playlist.get("playlist")
|
playlist_info = playlist.get("playlist")
|
||||||
if playlist_info.get("creator") == "listenbrainz":
|
if playlist_info.get("creator") == "listenbrainz":
|
||||||
title = playlist_info.get("title")
|
title = playlist_info.get("title")
|
||||||
match = re.search(
|
self._log.debug(f"Playlist title: {title}")
|
||||||
r"(Missed Recordings of \d{4}|Discoveries of \d{4})", title
|
playlist_type = (
|
||||||
|
"Exploration" if "Exploration" in title else "Jams"
|
||||||
)
|
)
|
||||||
if "Exploration" in title:
|
if "week of" in title:
|
||||||
playlist_type = "Exploration"
|
|
||||||
elif "Jams" in title:
|
|
||||||
playlist_type = "Jams"
|
|
||||||
elif match:
|
|
||||||
playlist_type = match.group(1)
|
|
||||||
else:
|
|
||||||
playlist_type = None
|
|
||||||
if "week of " in title:
|
|
||||||
date_str = title.split("week of ")[1].split(" ")[0]
|
date_str = title.split("week of ")[1].split(" ")[0]
|
||||||
date = datetime.datetime.strptime(
|
date = datetime.datetime.strptime(
|
||||||
date_str, "%Y-%m-%d"
|
date_str, "%Y-%m-%d"
|
||||||
).date()
|
).date()
|
||||||
else:
|
else:
|
||||||
date = None
|
continue
|
||||||
identifier = playlist_info.get("identifier")
|
identifier = playlist_info.get("identifier")
|
||||||
id = identifier.split("/")[-1]
|
id = identifier.split("/")[-1]
|
||||||
if playlist_type in ["Jams", "Exploration"]:
|
|
||||||
listenbrainz_playlists.append(
|
listenbrainz_playlists.append(
|
||||||
{
|
{"type": playlist_type, "date": date, "identifier": id}
|
||||||
"type": playlist_type,
|
)
|
||||||
"date": date,
|
listenbrainz_playlists = sorted(
|
||||||
"identifier": id,
|
listenbrainz_playlists, key=lambda x: x["type"]
|
||||||
"title": title,
|
)
|
||||||
}
|
listenbrainz_playlists = sorted(
|
||||||
|
listenbrainz_playlists, key=lambda x: x["date"], reverse=True
|
||||||
|
)
|
||||||
|
for playlist in listenbrainz_playlists:
|
||||||
|
self._log.debug(
|
||||||
|
f'Playlist: {playlist["type"]} - {playlist["date"]}'
|
||||||
)
|
)
|
||||||
return listenbrainz_playlists
|
return listenbrainz_playlists
|
||||||
|
|
||||||
|
|
@ -199,17 +193,20 @@ class ListenBrainzPlugin(BeetsPlugin):
|
||||||
"""This function returns a list of tracks in the playlist."""
|
"""This function returns a list of tracks in the playlist."""
|
||||||
tracks = []
|
tracks = []
|
||||||
for track in playlist.get("playlist").get("track"):
|
for track in playlist.get("playlist").get("track"):
|
||||||
|
identifier = track.get("identifier")
|
||||||
|
if isinstance(identifier, list):
|
||||||
|
identifier = identifier[0]
|
||||||
|
|
||||||
tracks.append(
|
tracks.append(
|
||||||
{
|
{
|
||||||
"artist": track.get("creator"),
|
"artist": track.get("creator", "Unknown artist"),
|
||||||
"identifier": track.get("identifier").split("/")[-1],
|
"identifier": identifier.split("/")[-1],
|
||||||
"title": track.get("title"),
|
"title": track.get("title"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.get_track_info(tracks)
|
return self.get_track_info(tracks)
|
||||||
|
|
||||||
def get_track_info(self, tracks):
|
def get_track_info(self, tracks):
|
||||||
"""Returns a list of track info."""
|
|
||||||
track_info = []
|
track_info = []
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
identifier = track.get("identifier")
|
identifier = track.get("identifier")
|
||||||
|
|
@ -242,25 +239,37 @@ class ListenBrainzPlugin(BeetsPlugin):
|
||||||
)
|
)
|
||||||
return track_info
|
return track_info
|
||||||
|
|
||||||
def get_weekly_playlist(self, index):
|
def get_weekly_playlist(self, playlist_type, most_recent=True):
|
||||||
"""Returns a list of weekly playlists based on the index."""
|
# Fetch all playlists
|
||||||
playlists = self.get_listenbrainz_playlists()
|
playlists = self.get_listenbrainz_playlists()
|
||||||
playlist = self.get_playlist(playlists[index].get("identifier"))
|
# Filter playlists by type
|
||||||
self._log.info(f"Getting {playlist.get('playlist').get('title')}")
|
filtered_playlists = [
|
||||||
|
p for p in playlists if p["type"] == playlist_type
|
||||||
|
]
|
||||||
|
# Sort playlists by date in descending order
|
||||||
|
sorted_playlists = sorted(
|
||||||
|
filtered_playlists, key=lambda x: x["date"], reverse=True
|
||||||
|
)
|
||||||
|
# Select the most recent or older playlist based on the most_recent flag
|
||||||
|
selected_playlist = (
|
||||||
|
sorted_playlists[0] if most_recent else sorted_playlists[1]
|
||||||
|
)
|
||||||
|
self._log.debug(
|
||||||
|
f"Selected playlist: {selected_playlist['type']} "
|
||||||
|
f"- {selected_playlist['date']}"
|
||||||
|
)
|
||||||
|
# Fetch and return tracks from the selected playlist
|
||||||
|
playlist = self.get_playlist(selected_playlist.get("identifier"))
|
||||||
return self.get_tracks_from_playlist(playlist)
|
return self.get_tracks_from_playlist(playlist)
|
||||||
|
|
||||||
def get_weekly_exploration(self):
|
def get_weekly_exploration(self):
|
||||||
"""Returns a list of weekly exploration."""
|
return self.get_weekly_playlist("Exploration", most_recent=True)
|
||||||
return self.get_weekly_playlist(0)
|
|
||||||
|
|
||||||
def get_weekly_jams(self):
|
def get_weekly_jams(self):
|
||||||
"""Returns a list of weekly jams."""
|
return self.get_weekly_playlist("Jams", most_recent=True)
|
||||||
return self.get_weekly_playlist(1)
|
|
||||||
|
|
||||||
def get_last_weekly_exploration(self):
|
def get_last_weekly_exploration(self):
|
||||||
"""Returns a list of weekly exploration."""
|
return self.get_weekly_playlist("Exploration", most_recent=False)
|
||||||
return self.get_weekly_playlist(3)
|
|
||||||
|
|
||||||
def get_last_weekly_jams(self):
|
def get_last_weekly_jams(self):
|
||||||
"""Returns a list of weekly jams."""
|
return self.get_weekly_playlist("Jams", most_recent=False)
|
||||||
return self.get_weekly_playlist(3)
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,7 @@ class Substitute(BeetsPlugin):
|
||||||
"""Do the actual replacing."""
|
"""Do the actual replacing."""
|
||||||
if text:
|
if text:
|
||||||
for pattern, replacement in self.substitute_rules:
|
for pattern, replacement in self.substitute_rules:
|
||||||
if pattern.match(text.lower()):
|
text = pattern.sub(replacement, text)
|
||||||
return replacement
|
|
||||||
return text
|
return text
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -47,10 +46,8 @@ class Substitute(BeetsPlugin):
|
||||||
substitute rules.
|
substitute rules.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.substitute_rules = []
|
|
||||||
self.template_funcs["substitute"] = self.tmpl_substitute
|
self.template_funcs["substitute"] = self.tmpl_substitute
|
||||||
|
self.substitute_rules = [
|
||||||
for key, view in self.config.items():
|
(re.compile(key, flags=re.IGNORECASE), value)
|
||||||
value = view.as_str()
|
for key, value in self.config.flatten().items()
|
||||||
pattern = re.compile(key.lower())
|
]
|
||||||
self.substitute_rules.append((pattern, value))
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,57 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
Changelog goes here! Please add your entry to the bottom of one of the lists below!
|
||||||
|
|
||||||
Unreleased
|
Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Changelog goes here! Please add your entry to the bottom of one of the lists below!
|
New features:
|
||||||
|
Bug fixes:
|
||||||
|
For packagers:
|
||||||
|
Other changes:
|
||||||
|
|
||||||
|
2.2.0 (December 02, 2024)
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
|
||||||
|
* :doc:`/plugins/substitute`: Allow the replacement string to use capture groups
|
||||||
|
from the match. It is thus possible to create more general rules, applying to
|
||||||
|
many different artists at once.
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
* Check if running python from the Microsoft Store and provide feedback to install
|
||||||
|
from python.org.
|
||||||
|
:bug:`5467`
|
||||||
|
* Fix bug where matcher doesn't consider medium number when importing. This makes
|
||||||
|
it difficult to import hybrid SACDs and other releases with duplicate tracks.
|
||||||
|
:bug:`5148`
|
||||||
|
* Bring back test files and the manual to the source distribution tarball.
|
||||||
|
:bug:`5513`
|
||||||
|
|
||||||
|
For packagers:
|
||||||
|
|
||||||
|
Other changes:
|
||||||
|
|
||||||
|
* Changed `bitesize` label to `good first issue`. Our `contribute`_ page is now
|
||||||
|
automatically populated with these issues. :bug:`4855`
|
||||||
|
|
||||||
|
.. _contribute: https://github.com/beetbox/beets/contribute
|
||||||
|
|
||||||
|
2.1.0 (November 22, 2024)
|
||||||
|
-------------------------
|
||||||
|
|
||||||
New features:
|
New features:
|
||||||
|
|
||||||
* New template function added: ``%capitalize``. Converts the first letter of
|
* New template function added: ``%capitalize``. Converts the first letter of
|
||||||
the text to uppercase and the rest to lowercase.
|
the text to uppercase and the rest to lowercase.
|
||||||
* Ability to query albums with track db fields and vice-versa, for example
|
* Ability to query albums with track db fields and vice-versa, for example
|
||||||
`beet list -a title:something` or `beet list artpath:cover`. Consequently
|
``beet list -a title:something`` or ``beet list artpath:cover``. Consequently
|
||||||
album queries involving `path` field have been sped up, like `beet list -a
|
album queries involving ``path`` field have been sped up, like ``beet list -a
|
||||||
path:/path/`.
|
path:/path/``.
|
||||||
* New `keep_in_artist` option for the :doc:`plugins/ftintitle` plugin, which
|
* :doc:`plugins/ftintitle`: New ``keep_in_artist`` option for the plugin, which
|
||||||
allows keeping the "feat." part in the artist metadata while still changing
|
allows keeping the "feat." part in the artist metadata while still changing
|
||||||
the title.
|
the title.
|
||||||
* :doc:`plugins/autobpm`: Add new configuration option ``beat_track_kwargs``
|
* :doc:`plugins/autobpm`: Add new configuration option ``beat_track_kwargs``
|
||||||
|
|
@ -42,31 +79,36 @@ Bug fixes:
|
||||||
issues in the future.
|
issues in the future.
|
||||||
:bug:`5289`
|
:bug:`5289`
|
||||||
* :doc:`plugins/discogs`: Fix the ``TypeError`` when there is no description.
|
* :doc:`plugins/discogs`: Fix the ``TypeError`` when there is no description.
|
||||||
* Remove single quotes from all SQL queries
|
* Use single quotes in all SQL queries
|
||||||
:bug:`4709`
|
:bug:`4709`
|
||||||
* :doc:`plugins/lyrics`: Update ``tekstowo`` backend to fetch lyrics directly
|
* :doc:`plugins/lyrics`: Update ``tekstowo`` backend to fetch lyrics directly
|
||||||
since recent updates to their website made it unsearchable.
|
since recent updates to their website made it unsearchable.
|
||||||
:bug:`5456`
|
:bug:`5456`
|
||||||
|
* :doc:`plugins/convert`: Fixed the convert plugin ``no_convert`` option so
|
||||||
|
that it no longer treats "and" and "or" queries the same. To maintain
|
||||||
|
previous behaviour add commas between your query keywords. For help see
|
||||||
|
:ref:`combiningqueries`.
|
||||||
|
* Fix the ``TypeError`` when :ref:`set_fields` is provided non-string values. :bug:`4840`
|
||||||
|
|
||||||
For packagers:
|
For packagers:
|
||||||
|
|
||||||
* The minimum supported Python version is now 3.8.
|
* The minimum supported Python version is now 3.8.
|
||||||
* The `beet` script has been removed from the repository.
|
* The ``beet`` script has been removed from the repository.
|
||||||
* The `typing_extensions` is required for Python 3.10 and below.
|
* The ``typing_extensions`` is required for Python 3.10 and below.
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
|
|
||||||
* :doc:`contributing`: The project now uses `poetry` for packaging and
|
* :doc:`contributing`: The project now uses ``poetry`` for packaging and
|
||||||
dependency management. This change affects project management and mostly
|
dependency management. This change affects project management and mostly
|
||||||
affects beets developers. Please see updates in :ref:`getting-the-source` and
|
affects beets developers. Please see updates in :ref:`getting-the-source` and
|
||||||
:ref:`testing` for more information.
|
:ref:`testing` for more information.
|
||||||
* :doc:`contributing`: Since `poetry` now manages local virtual environments,
|
* :doc:`contributing`: Since ``poetry`` now manages local virtual environments,
|
||||||
`tox` has been replaced by a task runner `poethepoet`. This change affects
|
`tox` has been replaced by a task runner ``poethepoet``. This change affects
|
||||||
beets developers and contributors. Please see updates in the
|
beets developers and contributors. Please see updates in the
|
||||||
:ref:`development-tools` section for more details. Type ``poe`` while in
|
:ref:`development-tools` section for more details. Type ``poe`` while in
|
||||||
the project directory to see the available commands.
|
the project directory to see the available commands.
|
||||||
* Installation instructions have been made consistent across plugins
|
* Installation instructions have been made consistent across plugins
|
||||||
documentation. Users should simply install `beets` with an `extra` of the
|
documentation. Users should simply install ``beets`` with an ``extra`` of the
|
||||||
corresponding plugin name in order to install extra dependencies for that
|
corresponding plugin name in order to install extra dependencies for that
|
||||||
plugin.
|
plugin.
|
||||||
* GitHub workflows have been reorganised for clarity: style, linting, type and
|
* GitHub workflows have been reorganised for clarity: style, linting, type and
|
||||||
|
|
@ -77,10 +119,10 @@ Other changes:
|
||||||
documentation is changed, and they only check the changed files. When
|
documentation is changed, and they only check the changed files. When
|
||||||
dependencies are updated (``poetry.lock``), then the entire code base is
|
dependencies are updated (``poetry.lock``), then the entire code base is
|
||||||
checked.
|
checked.
|
||||||
* The long-deprecated `beets.util.confit` module has been removed. This may
|
* The long-deprecated ``beets.util.confit`` module has been removed. This may
|
||||||
cause extremely outdated external plugins to fail to load.
|
cause extremely outdated external plugins to fail to load.
|
||||||
* :doc:`plugins/autobpm`: Add plugin dependencies to `pyproject.toml` under
|
* :doc:`plugins/autobpm`: Add plugin dependencies to ``pyproject.toml`` under
|
||||||
the `autobpm` extra and update the plugin installation instructions in the
|
the ``autobpm`` extra and update the plugin installation instructions in the
|
||||||
docs.
|
docs.
|
||||||
Since importing the bpm calculation functionality from ``librosa`` takes
|
Since importing the bpm calculation functionality from ``librosa`` takes
|
||||||
around 4 seconds, update the plugin to only do so when it actually needs to
|
around 4 seconds, update the plugin to only do so when it actually needs to
|
||||||
|
|
@ -253,6 +295,8 @@ New features:
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
|
||||||
|
* Improve ListenBrainz error handling.
|
||||||
|
:bug:`5459`
|
||||||
* :doc:`/plugins/deezer`: Improve requests error handling.
|
* :doc:`/plugins/deezer`: Improve requests error handling.
|
||||||
* :doc:`/plugins/lastimport`: Improve error handling in the `process_tracks` function and enable it to be used with other plugins.
|
* :doc:`/plugins/lastimport`: Improve error handling in the `process_tracks` function and enable it to be used with other plugins.
|
||||||
* :doc:`/plugins/spotify`: Improve handling of ConnectionError.
|
* :doc:`/plugins/spotify`: Improve handling of ConnectionError.
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ master_doc = "index"
|
||||||
project = "beets"
|
project = "beets"
|
||||||
copyright = "2016, Adrian Sampson"
|
copyright = "2016, Adrian Sampson"
|
||||||
|
|
||||||
version = "2.0"
|
version = "2.2"
|
||||||
release = "2.0.0"
|
release = "2.2.0"
|
||||||
|
|
||||||
pygments_style = "sphinx"
|
pygments_style = "sphinx"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ get it right:
|
||||||
should open the "System Properties" screen, then select the "Advanced" tab,
|
should open the "System Properties" screen, then select the "Advanced" tab,
|
||||||
then hit the "Environmental Variables..." button, and then look for the PATH
|
then hit the "Environmental Variables..." button, and then look for the PATH
|
||||||
variable in the table. Add the following to the end of the variable's value:
|
variable in the table. Add the following to the end of the variable's value:
|
||||||
``;C:\Python37;C:\Python37\Scripts``. You may need to adjust these paths to
|
``;C:\Python38;C:\Python38\Scripts``. You may need to adjust these paths to
|
||||||
point to your Python installation.
|
point to your Python installation.
|
||||||
|
|
||||||
3. Now install beets by running: ``pip install beets``
|
3. Now install beets by running: ``pip install beets``
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,34 @@ the ``rewrite`` plugin modifies the metadata, this plugin does not.
|
||||||
|
|
||||||
Enable the ``substitute`` plugin (see :ref:`using-plugins`), then make a ``substitute:`` section in your config file to contain your rules.
|
Enable the ``substitute`` plugin (see :ref:`using-plugins`), then make a ``substitute:`` section in your config file to contain your rules.
|
||||||
Each rule consists of a case-insensitive regular expression pattern, and a
|
Each rule consists of a case-insensitive regular expression pattern, and a
|
||||||
replacement value. For example, you might use:
|
replacement string. For example, you might use:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
substitute:
|
substitute:
|
||||||
.*jimi hendrix.*: Jimi Hendrix
|
.*jimi hendrix.*: Jimi Hendrix
|
||||||
|
|
||||||
|
The replacement can be an expression utilising the matched regex, allowing us
|
||||||
|
to create more general rules. Say for example, we want to sort all albums by
|
||||||
|
multiple artists into the directory of the first artist. We can thus capture
|
||||||
|
everything before the first ``,``, `` &`` or `` and``, and use this capture
|
||||||
|
group in the output, discarding the rest of the string.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
substitute:
|
||||||
|
^(.*?)(,| &| and).*: \1
|
||||||
|
|
||||||
|
This would handle all the below cases in a single rule:
|
||||||
|
|
||||||
|
Bob Dylan and The Band -> Bob Dylan
|
||||||
|
Neil Young & Crazy Horse -> Neil Young
|
||||||
|
James Yorkston, Nina Persson & The Second Hand Orchestra -> James Yorkston
|
||||||
|
|
||||||
|
|
||||||
To apply the substitution, you have to call the function ``%substitute{}`` in the paths section. For example:
|
To apply the substitution, you have to call the function ``%substitute{}`` in the paths section. For example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
default: %substitute{$albumartist}/$year - $album%aunique{}/$track - $title
|
default: \%substitute{$albumartist}/$year - $album\%aunique{}/$track - $title
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,10 @@ fields to nullify and the conditions for nullifying them:
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
zero:
|
zero:
|
||||||
fields: month day genre comments
|
fields: month day genre genres comments
|
||||||
comments: [EAC, LAME, from.+collection, 'ripped by']
|
comments: [EAC, LAME, from.+collection, 'ripped by']
|
||||||
genre: [rnb, 'power metal']
|
genre: [rnb, 'power metal']
|
||||||
|
genres: [rnb, 'power metal']
|
||||||
update_database: true
|
update_database: true
|
||||||
|
|
||||||
If a custom pattern is not defined for a given field, the field will be nulled
|
If a custom pattern is not defined for a given field, the field will be nulled
|
||||||
|
|
@ -60,4 +61,4 @@ art from files' tags unless you tell it not to. To keep the album art, include
|
||||||
the special field ``images`` in the list. For example::
|
the special field ``images`` in the list. For example::
|
||||||
|
|
||||||
zero:
|
zero:
|
||||||
keep_fields: title artist album year track genre images
|
keep_fields: title artist album year track genre genres images
|
||||||
|
|
|
||||||
BIN
extra/beets.reg
BIN
extra/beets.reg
Binary file not shown.
|
|
@ -40,7 +40,10 @@ def update_changelog(text: str, new: Version) -> str:
|
||||||
Unreleased
|
Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Changelog goes here! Please add your entry to the bottom of one of the lists below!
|
New features:
|
||||||
|
Bug fixes:
|
||||||
|
For packagers:
|
||||||
|
Other changes:
|
||||||
|
|
||||||
{new_header}
|
{new_header}
|
||||||
{'-' * len(new_header)}
|
{'-' * len(new_header)}
|
||||||
|
|
|
||||||
1094
poetry.lock
generated
1094
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "beets"
|
name = "beets"
|
||||||
version = "2.0.0"
|
version = "2.2.0"
|
||||||
description = "music tagger and library organizer"
|
description = "music tagger and library organizer"
|
||||||
authors = ["Adrian Sampson <adrian@radbox.org>"]
|
authors = ["Adrian Sampson <adrian@radbox.org>"]
|
||||||
maintainers = ["Serene-Arc"]
|
maintainers = ["Serene-Arc"]
|
||||||
|
|
@ -26,6 +26,7 @@ packages = [
|
||||||
{ include = "beets" },
|
{ include = "beets" },
|
||||||
{ include = "beetsplug" },
|
{ include = "beetsplug" },
|
||||||
]
|
]
|
||||||
|
include = ["test", "man/**/*"] # extra files to include in the sdist
|
||||||
|
|
||||||
[tool.poetry.urls]
|
[tool.poetry.urls]
|
||||||
Changelog = "https://github.com/beetbox/beets/blob/master/docs/changelog.rst"
|
Changelog = "https://github.com/beetbox/beets/blob/master/docs/changelog.rst"
|
||||||
|
|
@ -67,6 +68,9 @@ resampy = { version = ">=0.4.3", optional = true }
|
||||||
requests-oauthlib = { version = ">=0.6.1", optional = true }
|
requests-oauthlib = { version = ">=0.6.1", optional = true }
|
||||||
soco = { version = "*", optional = true }
|
soco = { version = "*", optional = true }
|
||||||
|
|
||||||
|
pydata-sphinx-theme = { version = "*", optional = true }
|
||||||
|
sphinx = { version = "*", optional = true }
|
||||||
|
|
||||||
[tool.poetry.group.test.dependencies]
|
[tool.poetry.group.test.dependencies]
|
||||||
beautifulsoup4 = "*"
|
beautifulsoup4 = "*"
|
||||||
codecov = ">=2.1.13"
|
codecov = ">=2.1.13"
|
||||||
|
|
@ -96,10 +100,6 @@ types-PyYAML = "*"
|
||||||
types-requests = "*"
|
types-requests = "*"
|
||||||
types-urllib3 = "*"
|
types-urllib3 = "*"
|
||||||
|
|
||||||
[tool.poetry.group.docs.dependencies]
|
|
||||||
pydata-sphinx-theme = "*"
|
|
||||||
sphinx = "*"
|
|
||||||
|
|
||||||
[tool.poetry.group.release.dependencies]
|
[tool.poetry.group.release.dependencies]
|
||||||
click = ">=8.1.7"
|
click = ">=8.1.7"
|
||||||
packaging = ">=24.0"
|
packaging = ">=24.0"
|
||||||
|
|
@ -115,6 +115,7 @@ beatport = ["requests-oauthlib"]
|
||||||
bpd = ["PyGObject"] # python-gi and GStreamer 1.0+
|
bpd = ["PyGObject"] # python-gi and GStreamer 1.0+
|
||||||
chroma = ["pyacoustid"] # chromaprint or fpcalc
|
chroma = ["pyacoustid"] # chromaprint or fpcalc
|
||||||
# convert # ffmpeg
|
# convert # ffmpeg
|
||||||
|
docs = ["pydata-sphinx-theme", "sphinx"]
|
||||||
discogs = ["python3-discogs-client"]
|
discogs = ["python3-discogs-client"]
|
||||||
embedart = ["Pillow"] # ImageMagick
|
embedart = ["Pillow"] # ImageMagick
|
||||||
embyupdate = ["requests"]
|
embyupdate = ["requests"]
|
||||||
|
|
@ -149,6 +150,15 @@ build-backend = "poetry.core.masonry.api"
|
||||||
poethepoet = ">=0.26"
|
poethepoet = ">=0.26"
|
||||||
poetry = ">=1.8"
|
poetry = ">=1.8"
|
||||||
|
|
||||||
|
[tool.poe.tasks.build]
|
||||||
|
help = "Build the package"
|
||||||
|
shell = """
|
||||||
|
make -C docs man
|
||||||
|
rm -rf man
|
||||||
|
mv docs/_build/man .
|
||||||
|
poetry build
|
||||||
|
"""
|
||||||
|
|
||||||
[tool.poe.tasks.bump]
|
[tool.poe.tasks.bump]
|
||||||
help = "Bump project version and update relevant files"
|
help = "Bump project version and update relevant files"
|
||||||
cmd = "python ./extra/release.py bump $version"
|
cmd = "python ./extra/release.py bump $version"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ markers =
|
||||||
data_file = .reports/coverage/data
|
data_file = .reports/coverage/data
|
||||||
branch = true
|
branch = true
|
||||||
relative_files = true
|
relative_files = true
|
||||||
|
omit = beets/test/*
|
||||||
|
|
||||||
[coverage:report]
|
[coverage:report]
|
||||||
precision = 2
|
precision = 2
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ class TestAuraResponse:
|
||||||
"artist": item.artist,
|
"artist": item.artist,
|
||||||
"size": Path(os.fsdecode(item.path)).stat().st_size,
|
"size": Path(os.fsdecode(item.path)).stat().st_size,
|
||||||
"title": item.title,
|
"title": item.title,
|
||||||
|
"track": 1,
|
||||||
},
|
},
|
||||||
"relationships": {
|
"relationships": {
|
||||||
"albums": {"data": [{"id": str(album.id), "type": "album"}]},
|
"albums": {"data": [{"id": str(album.id), "type": "album"}]},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from beets.test.helper import ImportHelper, PluginMixin
|
from beets.test.helper import ImportHelper, PluginMixin
|
||||||
|
|
||||||
|
github_ci = os.environ.get("GITHUB_ACTIONS") == "true"
|
||||||
|
if not github_ci and not importlib.util.find_spec("librosa"):
|
||||||
|
pytest.skip("librosa isn't available", allow_module_level=True)
|
||||||
|
|
||||||
|
|
||||||
class TestAutoBPMPlugin(PluginMixin, ImportHelper):
|
class TestAutoBPMPlugin(PluginMixin, ImportHelper):
|
||||||
plugin = "autobpm"
|
plugin = "autobpm"
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import pytest
|
||||||
from mediafile import MediaFile
|
from mediafile import MediaFile
|
||||||
|
|
||||||
from beets import util
|
from beets import util
|
||||||
|
from beets.library import Item
|
||||||
from beets.test import _common
|
from beets.test import _common
|
||||||
from beets.test.helper import (
|
from beets.test.helper import (
|
||||||
AsIsImporterMixin,
|
AsIsImporterMixin,
|
||||||
|
|
@ -31,6 +33,7 @@ from beets.test.helper import (
|
||||||
control_stdin,
|
control_stdin,
|
||||||
)
|
)
|
||||||
from beets.util import bytestring_path, displayable_path
|
from beets.util import bytestring_path, displayable_path
|
||||||
|
from beetsplug import convert
|
||||||
|
|
||||||
|
|
||||||
def shell_quote(text):
|
def shell_quote(text):
|
||||||
|
|
@ -335,3 +338,21 @@ class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand):
|
||||||
self.run_convert_path(item.path)
|
self.run_convert_path(item.path)
|
||||||
converted = os.path.join(self.convert_dest, b"converted.ogg")
|
converted = os.path.join(self.convert_dest, b"converted.ogg")
|
||||||
self.assertNoFileTag(converted, "mp3")
|
self.assertNoFileTag(converted, "mp3")
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoConvert:
|
||||||
|
"""Test the effect of the `no_convert` option."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"config_value, should_skip",
|
||||||
|
[
|
||||||
|
("", False),
|
||||||
|
("bitrate:320", False),
|
||||||
|
("bitrate:320 format:ogg", False),
|
||||||
|
("bitrate:320 , format:ogg", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_no_convert_skip(self, config_value, should_skip):
|
||||||
|
item = Item(format="ogg", bitrate=256)
|
||||||
|
convert.config["convert"]["no_convert"] = config_value
|
||||||
|
assert convert.in_no_convert(item) == should_skip
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,11 @@
|
||||||
# The above copyright notice and this permission notice shall be
|
# The above copyright notice and this permission notice shall be
|
||||||
# included in all copies or substantial portions of the Software.
|
# included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from beets import config
|
from beets.autotag.hooks import AlbumInfo, TrackInfo
|
||||||
from beets.library import Item
|
from beets.library import Item
|
||||||
from beets.test.helper import (
|
from beets.test.helper import PluginTestCase, capture_log
|
||||||
PluginTestCase,
|
|
||||||
capture_log,
|
|
||||||
generate_album_info,
|
|
||||||
generate_track_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MbsyncCliTest(PluginTestCase):
|
class MbsyncCliTest(PluginTestCase):
|
||||||
|
|
@ -31,28 +25,28 @@ class MbsyncCliTest(PluginTestCase):
|
||||||
@patch("beets.autotag.mb.album_for_id")
|
@patch("beets.autotag.mb.album_for_id")
|
||||||
@patch("beets.autotag.mb.track_for_id")
|
@patch("beets.autotag.mb.track_for_id")
|
||||||
def test_update_library(self, track_for_id, album_for_id):
|
def test_update_library(self, track_for_id, album_for_id):
|
||||||
album_for_id.return_value = generate_album_info(
|
|
||||||
"album id", [("track id", {"release_track_id": "release track id"})]
|
|
||||||
)
|
|
||||||
track_for_id.return_value = generate_track_info(
|
|
||||||
"singleton track id", {"title": "singleton info"}
|
|
||||||
)
|
|
||||||
|
|
||||||
album_item = Item(
|
album_item = Item(
|
||||||
album="old title",
|
album="old album",
|
||||||
mb_albumid="81ae60d4-5b75-38df-903a-db2cfa51c2c6",
|
mb_albumid="81ae60d4-5b75-38df-903a-db2cfa51c2c6",
|
||||||
mb_trackid="old track id",
|
mb_trackid="track id",
|
||||||
mb_releasetrackid="release track id",
|
|
||||||
path="",
|
|
||||||
)
|
)
|
||||||
album = self.lib.add_album([album_item])
|
self.lib.add_album([album_item])
|
||||||
|
|
||||||
item = Item(
|
singleton = Item(
|
||||||
title="old title",
|
title="old title", mb_trackid="b8c2cf90-83f9-3b5f-8ccd-31fb866fcf37"
|
||||||
mb_trackid="b8c2cf90-83f9-3b5f-8ccd-31fb866fcf37",
|
)
|
||||||
path="",
|
self.lib.add(singleton)
|
||||||
|
|
||||||
|
album_for_id.return_value = AlbumInfo(
|
||||||
|
album_id="album id",
|
||||||
|
album="new album",
|
||||||
|
tracks=[
|
||||||
|
TrackInfo(track_id=album_item.mb_trackid, title="new title")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
track_for_id.return_value = TrackInfo(
|
||||||
|
track_id=singleton.mb_trackid, title="new title"
|
||||||
)
|
)
|
||||||
self.lib.add(item)
|
|
||||||
|
|
||||||
with capture_log() as logs:
|
with capture_log() as logs:
|
||||||
self.run_command("mbsync")
|
self.run_command("mbsync")
|
||||||
|
|
@ -60,130 +54,36 @@ class MbsyncCliTest(PluginTestCase):
|
||||||
assert "Sending event: albuminfo_received" in logs
|
assert "Sending event: albuminfo_received" in logs
|
||||||
assert "Sending event: trackinfo_received" in logs
|
assert "Sending event: trackinfo_received" in logs
|
||||||
|
|
||||||
item.load()
|
singleton.load()
|
||||||
assert item.title == "singleton info"
|
assert singleton.title == "new title"
|
||||||
|
|
||||||
album_item.load()
|
album_item.load()
|
||||||
assert album_item.title == "track info"
|
assert album_item.title == "new title"
|
||||||
assert album_item.mb_trackid == "track id"
|
assert album_item.mb_trackid == "track id"
|
||||||
|
assert album_item.get_album().album == "new album"
|
||||||
|
|
||||||
album.load()
|
def test_custom_format(self):
|
||||||
assert album.album == "album info"
|
for item in [
|
||||||
|
Item(artist="albumartist", album="no id"),
|
||||||
def test_message_when_skipping(self):
|
Item(
|
||||||
config["format_item"] = "$artist - $album - $title"
|
artist="albumartist",
|
||||||
config["format_album"] = "$albumartist - $album"
|
album="invalid id",
|
||||||
|
|
||||||
# Test album with no mb_albumid.
|
|
||||||
# The default format for an album include $albumartist so
|
|
||||||
# set that here, too.
|
|
||||||
album_invalid = Item(
|
|
||||||
albumartist="album info", album="album info", path=""
|
|
||||||
)
|
|
||||||
self.lib.add_album([album_invalid])
|
|
||||||
|
|
||||||
# default format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
|
||||||
self.run_command("mbsync")
|
|
||||||
e = (
|
|
||||||
"mbsync: Skipping album with no mb_albumid: "
|
|
||||||
+ "album info - album info"
|
|
||||||
)
|
|
||||||
assert e == logs[0]
|
|
||||||
|
|
||||||
# custom format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
|
||||||
self.run_command("mbsync", "-f", "'$album'")
|
|
||||||
e = "mbsync: Skipping album with no mb_albumid: 'album info'"
|
|
||||||
assert e == logs[0]
|
|
||||||
|
|
||||||
# restore the config
|
|
||||||
config["format_item"] = "$artist - $album - $title"
|
|
||||||
config["format_album"] = "$albumartist - $album"
|
|
||||||
|
|
||||||
# Test singleton with no mb_trackid.
|
|
||||||
# The default singleton format includes $artist and $album
|
|
||||||
# so we need to stub them here
|
|
||||||
item_invalid = Item(
|
|
||||||
artist="album info",
|
|
||||||
album="album info",
|
|
||||||
title="old title",
|
|
||||||
path="",
|
|
||||||
)
|
|
||||||
self.lib.add(item_invalid)
|
|
||||||
|
|
||||||
# default format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
|
||||||
self.run_command("mbsync")
|
|
||||||
e = (
|
|
||||||
"mbsync: Skipping singleton with no mb_trackid: "
|
|
||||||
+ "album info - album info - old title"
|
|
||||||
)
|
|
||||||
assert e == logs[0]
|
|
||||||
|
|
||||||
# custom format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
|
||||||
self.run_command("mbsync", "-f", "'$title'")
|
|
||||||
e = "mbsync: Skipping singleton with no mb_trackid: 'old title'"
|
|
||||||
assert e == logs[0]
|
|
||||||
|
|
||||||
def test_message_when_invalid(self):
|
|
||||||
config["format_item"] = "$artist - $album - $title"
|
|
||||||
config["format_album"] = "$albumartist - $album"
|
|
||||||
|
|
||||||
# Test album with invalid mb_albumid.
|
|
||||||
# The default format for an album include $albumartist so
|
|
||||||
# set that here, too.
|
|
||||||
album_invalid = Item(
|
|
||||||
albumartist="album info",
|
|
||||||
album="album info",
|
|
||||||
mb_albumid="a1b2c3d4",
|
mb_albumid="a1b2c3d4",
|
||||||
path="",
|
),
|
||||||
)
|
]:
|
||||||
self.lib.add_album([album_invalid])
|
self.lib.add_album([item])
|
||||||
|
|
||||||
|
for item in [
|
||||||
|
Item(artist="artist", title="no id"),
|
||||||
|
Item(artist="artist", title="invalid id", mb_trackid="a1b2c3d4"),
|
||||||
|
]:
|
||||||
|
self.lib.add(item)
|
||||||
|
|
||||||
# default format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
with capture_log("beets.mbsync") as logs:
|
||||||
self.run_command("mbsync")
|
self.run_command("mbsync", "-f", "'%if{$album,$album,$title}'")
|
||||||
e = (
|
assert set(logs) == {
|
||||||
"mbsync: Skipping album with invalid mb_albumid: "
|
"mbsync: Skipping album with no mb_albumid: 'no id'",
|
||||||
+ "album info - album info"
|
"mbsync: Skipping album with invalid mb_albumid: 'invalid id'",
|
||||||
)
|
"mbsync: Skipping singleton with no mb_trackid: 'no id'",
|
||||||
assert e == logs[0]
|
"mbsync: Skipping singleton with invalid mb_trackid: 'invalid id'",
|
||||||
|
}
|
||||||
# custom format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
|
||||||
self.run_command("mbsync", "-f", "'$album'")
|
|
||||||
e = "mbsync: Skipping album with invalid mb_albumid: 'album info'"
|
|
||||||
assert e == logs[0]
|
|
||||||
|
|
||||||
# restore the config
|
|
||||||
config["format_item"] = "$artist - $album - $title"
|
|
||||||
config["format_album"] = "$albumartist - $album"
|
|
||||||
|
|
||||||
# Test singleton with invalid mb_trackid.
|
|
||||||
# The default singleton format includes $artist and $album
|
|
||||||
# so we need to stub them here
|
|
||||||
item_invalid = Item(
|
|
||||||
artist="album info",
|
|
||||||
album="album info",
|
|
||||||
title="old title",
|
|
||||||
mb_trackid="a1b2c3d4",
|
|
||||||
path="",
|
|
||||||
)
|
|
||||||
self.lib.add(item_invalid)
|
|
||||||
|
|
||||||
# default format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
|
||||||
self.run_command("mbsync")
|
|
||||||
e = (
|
|
||||||
"mbsync: Skipping singleton with invalid mb_trackid: "
|
|
||||||
+ "album info - album info - old title"
|
|
||||||
)
|
|
||||||
assert e == logs[0]
|
|
||||||
|
|
||||||
# custom format
|
|
||||||
with capture_log("beets.mbsync") as logs:
|
|
||||||
self.run_command("mbsync", "-f", "'$title'")
|
|
||||||
e = "mbsync: Skipping singleton with invalid mb_trackid: 'old title'"
|
|
||||||
assert e == logs[0]
|
|
||||||
|
|
|
||||||
90
test/plugins/test_substitute.py
Normal file
90
test/plugins/test_substitute.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# This file is part of beets.
|
||||||
|
# Copyright 2024, Nicholas Boyd Isacsson.
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
"""Test the substitute plugin regex functionality."""
|
||||||
|
|
||||||
|
from beets.test.helper import PluginTestCase
|
||||||
|
from beetsplug.substitute import Substitute
|
||||||
|
|
||||||
|
|
||||||
|
class SubstitutePluginTest(PluginTestCase):
|
||||||
|
plugin = "substitute"
|
||||||
|
preload_plugin = False
|
||||||
|
|
||||||
|
def run_substitute(self, config, cases):
|
||||||
|
with self.configure_plugin(config):
|
||||||
|
for input, expected in cases:
|
||||||
|
assert Substitute().tmpl_substitute(input) == expected
|
||||||
|
|
||||||
|
def test_simple_substitute(self):
|
||||||
|
self.run_substitute(
|
||||||
|
{
|
||||||
|
"a": "x",
|
||||||
|
"b": "y",
|
||||||
|
"c": "z",
|
||||||
|
},
|
||||||
|
[("a", "x"), ("b", "y"), ("c", "z")],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_case_insensitivity(self):
|
||||||
|
self.run_substitute({"a": "x"}, [("A", "x")])
|
||||||
|
|
||||||
|
def test_unmatched_input_preserved(self):
|
||||||
|
self.run_substitute({"a": "x"}, [("c", "c")])
|
||||||
|
|
||||||
|
def test_regex_to_static(self):
|
||||||
|
self.run_substitute(
|
||||||
|
{".*jimi hendrix.*": "Jimi Hendrix"},
|
||||||
|
[("The Jimi Hendrix Experience", "Jimi Hendrix")],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_regex_capture_group(self):
|
||||||
|
self.run_substitute(
|
||||||
|
{"^(.*?)(,| &| and).*": r"\1"},
|
||||||
|
[
|
||||||
|
("King Creosote & Jon Hopkins", "King Creosote"),
|
||||||
|
(
|
||||||
|
"Michael Hurley, The Holy Modal Rounders, Jeffrey Frederick & "
|
||||||
|
+ "The Clamtones",
|
||||||
|
"Michael Hurley",
|
||||||
|
),
|
||||||
|
("James Yorkston and the Athletes", "James Yorkston"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_partial_substitution(self):
|
||||||
|
self.run_substitute({r"\.": ""}, [("U.N.P.O.C.", "UNPOC")])
|
||||||
|
|
||||||
|
def test_rules_applied_in_definition_order(self):
|
||||||
|
self.run_substitute(
|
||||||
|
{
|
||||||
|
"a": "x",
|
||||||
|
"[ab]": "y",
|
||||||
|
"b": "z",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
("a", "x"),
|
||||||
|
("b", "y"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rules_applied_in_sequence(self):
|
||||||
|
self.run_substitute(
|
||||||
|
{"a": "b", "b": "c", "d": "a"},
|
||||||
|
[
|
||||||
|
("a", "c"),
|
||||||
|
("b", "c"),
|
||||||
|
("d", "a"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
@ -395,11 +395,13 @@ class ImportSingletonTest(ImportTestCase):
|
||||||
def test_set_fields(self):
|
def test_set_fields(self):
|
||||||
genre = "\U0001f3b7 Jazz"
|
genre = "\U0001f3b7 Jazz"
|
||||||
collection = "To Listen"
|
collection = "To Listen"
|
||||||
|
disc = 0
|
||||||
|
|
||||||
config["import"]["set_fields"] = {
|
config["import"]["set_fields"] = {
|
||||||
"collection": collection,
|
"collection": collection,
|
||||||
"genre": genre,
|
"genre": genre,
|
||||||
"title": "$title - formatted",
|
"title": "$title - formatted",
|
||||||
|
"disc": disc,
|
||||||
}
|
}
|
||||||
|
|
||||||
# As-is item import.
|
# As-is item import.
|
||||||
|
|
@ -412,6 +414,7 @@ class ImportSingletonTest(ImportTestCase):
|
||||||
assert item.genre == genre
|
assert item.genre == genre
|
||||||
assert item.collection == collection
|
assert item.collection == collection
|
||||||
assert item.title == "Tag Track 1 - formatted"
|
assert item.title == "Tag Track 1 - formatted"
|
||||||
|
assert item.disc == disc
|
||||||
# Remove item from library to test again with APPLY choice.
|
# Remove item from library to test again with APPLY choice.
|
||||||
item.remove()
|
item.remove()
|
||||||
|
|
||||||
|
|
@ -426,6 +429,7 @@ class ImportSingletonTest(ImportTestCase):
|
||||||
assert item.genre == genre
|
assert item.genre == genre
|
||||||
assert item.collection == collection
|
assert item.collection == collection
|
||||||
assert item.title == "Applied Track 1 - formatted"
|
assert item.title == "Applied Track 1 - formatted"
|
||||||
|
assert item.disc == disc
|
||||||
|
|
||||||
|
|
||||||
class ImportTest(ImportTestCase):
|
class ImportTest(ImportTestCase):
|
||||||
|
|
@ -583,12 +587,14 @@ class ImportTest(ImportTestCase):
|
||||||
genre = "\U0001f3b7 Jazz"
|
genre = "\U0001f3b7 Jazz"
|
||||||
collection = "To Listen"
|
collection = "To Listen"
|
||||||
comments = "managed by beets"
|
comments = "managed by beets"
|
||||||
|
disc = 0
|
||||||
|
|
||||||
config["import"]["set_fields"] = {
|
config["import"]["set_fields"] = {
|
||||||
"genre": genre,
|
"genre": genre,
|
||||||
"collection": collection,
|
"collection": collection,
|
||||||
"comments": comments,
|
"comments": comments,
|
||||||
"album": "$album - formatted",
|
"album": "$album - formatted",
|
||||||
|
"disc": disc,
|
||||||
}
|
}
|
||||||
|
|
||||||
# As-is album import.
|
# As-is album import.
|
||||||
|
|
@ -608,6 +614,7 @@ class ImportTest(ImportTestCase):
|
||||||
item.get("album", with_album=False)
|
item.get("album", with_album=False)
|
||||||
== "Tag Album - formatted"
|
== "Tag Album - formatted"
|
||||||
)
|
)
|
||||||
|
assert item.disc == disc
|
||||||
# Remove album from library to test again with APPLY choice.
|
# Remove album from library to test again with APPLY choice.
|
||||||
album.remove()
|
album.remove()
|
||||||
|
|
||||||
|
|
@ -629,6 +636,7 @@ class ImportTest(ImportTestCase):
|
||||||
item.get("album", with_album=False)
|
item.get("album", with_album=False)
|
||||||
== "Applied Album - formatted"
|
== "Applied Album - formatted"
|
||||||
)
|
)
|
||||||
|
assert item.disc == disc
|
||||||
|
|
||||||
|
|
||||||
class ImportTracksTest(ImportTestCase):
|
class ImportTracksTest(ImportTestCase):
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ from mediafile import MediaFile, UnreadableFileError
|
||||||
import beets.dbcore.query
|
import beets.dbcore.query
|
||||||
import beets.library
|
import beets.library
|
||||||
from beets import config, plugins, util
|
from beets import config, plugins, util
|
||||||
|
from beets.library import Album
|
||||||
from beets.test import _common
|
from beets.test import _common
|
||||||
from beets.test._common import item
|
from beets.test._common import item
|
||||||
from beets.test.helper import BeetsTestCase, ItemInDBTestCase
|
from beets.test.helper import BeetsTestCase, ItemInDBTestCase
|
||||||
|
|
@ -81,8 +82,7 @@ class StoreTest(ItemInDBTestCase):
|
||||||
assert "composer" not in self.i._dirty
|
assert "composer" not in self.i._dirty
|
||||||
|
|
||||||
def test_store_album_cascades_flex_deletes(self):
|
def test_store_album_cascades_flex_deletes(self):
|
||||||
album = _common.album()
|
album = Album(flex1="Flex-1")
|
||||||
album.flex1 = "Flex-1"
|
|
||||||
self.lib.add(album)
|
self.lib.add(album)
|
||||||
item = _common.item()
|
item = _common.item()
|
||||||
item.album_id = album.id
|
item.album_id = album.id
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import beets.library
|
import beets.library
|
||||||
from beets import config, dbcore
|
from beets import config, dbcore
|
||||||
|
from beets.library import Album
|
||||||
from beets.test import _common
|
from beets.test import _common
|
||||||
from beets.test.helper import BeetsTestCase
|
from beets.test.helper import BeetsTestCase
|
||||||
|
|
||||||
|
|
@ -26,28 +27,32 @@ class DummyDataTestCase(BeetsTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
albums = [_common.album() for _ in range(3)]
|
albums = [
|
||||||
albums[0].album = "Album A"
|
Album(
|
||||||
albums[0].genre = "Rock"
|
album="Album A",
|
||||||
albums[0].year = 2001
|
genre="Rock",
|
||||||
albums[0].flex1 = "Flex1-1"
|
year=2001,
|
||||||
albums[0].flex2 = "Flex2-A"
|
flex1="Flex1-1",
|
||||||
albums[0].albumartist = "Foo"
|
flex2="Flex2-A",
|
||||||
albums[0].albumartist_sort = None
|
albumartist="Foo",
|
||||||
albums[1].album = "Album B"
|
),
|
||||||
albums[1].genre = "Rock"
|
Album(
|
||||||
albums[1].year = 2001
|
album="Album B",
|
||||||
albums[1].flex1 = "Flex1-2"
|
genre="Rock",
|
||||||
albums[1].flex2 = "Flex2-A"
|
year=2001,
|
||||||
albums[1].albumartist = "Bar"
|
flex1="Flex1-2",
|
||||||
albums[1].albumartist_sort = None
|
flex2="Flex2-A",
|
||||||
albums[2].album = "Album C"
|
albumartist="Bar",
|
||||||
albums[2].genre = "Jazz"
|
),
|
||||||
albums[2].year = 2005
|
Album(
|
||||||
albums[2].flex1 = "Flex1-1"
|
album="Album C",
|
||||||
albums[2].flex2 = "Flex2-B"
|
genre="Jazz",
|
||||||
albums[2].albumartist = "Baz"
|
year=2005,
|
||||||
albums[2].albumartist_sort = None
|
flex1="Flex1-1",
|
||||||
|
flex2="Flex2-B",
|
||||||
|
albumartist="Baz",
|
||||||
|
),
|
||||||
|
]
|
||||||
for album in albums:
|
for album in albums:
|
||||||
self.lib.add(album)
|
self.lib.add(album)
|
||||||
|
|
||||||
|
|
@ -378,14 +383,14 @@ class CaseSensitivityTest(DummyDataTestCase, BeetsTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
album = _common.album()
|
album = Album(
|
||||||
album.album = "album"
|
album="album",
|
||||||
album.genre = "alternative"
|
genre="alternative",
|
||||||
album.year = "2001"
|
year="2001",
|
||||||
album.flex1 = "flex1"
|
flex1="flex1",
|
||||||
album.flex2 = "flex2-A"
|
flex2="flex2-A",
|
||||||
album.albumartist = "bar"
|
albumartist="bar",
|
||||||
album.albumartist_sort = None
|
)
|
||||||
self.lib.add(album)
|
self.lib.add(album)
|
||||||
|
|
||||||
item = _common.item()
|
item = _common.item()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue