Prevents this crash:
```
$ beet import ~/Music/Music/_/[1405]/00.mp3
Traceback (most recent call last):
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/bin/.beet-wrapped", line 9, in <module>
sys.exit(main())
~~~~^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/ui/__init__.py", line 1859, in main
_raw_main(args)
~~~~~~~~~^^^^^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/ui/__init__.py", line 1838, in _raw_main
subcommand.func(lib, suboptions, subargs)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/ui/commands.py", line 1390, in import_func
import_files(lib, byte_paths, query)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/ui/commands.py", line 1330, in import_files
session.run()
~~~~~~~~~~~^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/importer/session.py", line 234, in run
pl.run_parallel(QUEUE_SIZE)
~~~~~~~~~~~~~~~^^^^^^^^^^^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/util/pipeline.py", line 471, in run_parallel
raise exc_info[1].with_traceback(exc_info[2])
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/util/pipeline.py", line 336, in run
out = self.coro.send(msg)
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/util/pipeline.py", line 219, in coro
func(*(args + (task,)))
~~~~^^^^^^^^^^^^^^^^^^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/importer/stages.py", line 141, in lookup_candidates
plugins.send("import_task_start", session=session, task=task)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/plugins.py", line 505, in send
result = handler(**arguments)
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beets/plugins.py", line 200, in wrapper
return func(*args, **kwargs)
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beetsplug/fromfilename.py", line 165, in filename_task
apply_matches(d, self._log)
~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/nix/store/lfv9ns20hz2bg6d44js378vcxjfm9261-beets-2.3.1/lib/python3.13/site-packages/beetsplug/fromfilename.py", line 124, in apply_matches
item.title = str(d[item][title_field])
~~~~~~~^^^^^^^^^^^^^
KeyError: 'title'
```
experimental, even though a tag last.fm very often returns (in top 20
tag charts!), it is too broad of a term to be pinned downed with any
particular genre, thus can't really be used for canonicalization.
Fixes to the beets default tree and whitlist files I collected over the
years; Includes Tags last.fm returns quite often; Also the
chart.getTopTags API endpoint was checked to make sure the top 100
charts are included in beets default tree and whitelist.
instead of codecs.open(), which most probably is a relict of beets' Python2/3 compatibility area.
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
To ensure proper fallback to the next stage, in each stage we do a full
combine/resolve/log.
Also we directly return if have satisfied results. As a bonus this
improves readability.
Some duplicate code on the label magic though...
- Remove "early whitelist check", since it breaks canonicalization of
actually unwanted genres (not whitelisted) resolving "up" to parent
genres.
- Remove the filter_valid_genres method entirely and get back to inline
list comprehensions. The caveat is that None genres are not catched
that way (see below, should be one of the last functions that finally
returns lists only)
- Along the way, fix _last_lookup's rearly return to empty list instead
of None.
Convert static _types dictionaries to dynamic cached class properties to
enable proper plugin type inheritance and avoid mutating shared state.
Key changes:
- Replace static _types dicts with @cached_classproperty decorators
- Update cached_classproperty to support proper caching with class names
- Remove manual _types mutation in plugin loading/unloading
- Add pluginload event and cache clearing for proper plugin integration
- Fix test to trigger type checking during item creation
This ensures plugin types are properly inherited through the class
hierarchy and eliminates issues with shared mutable state between
test runs.
Fixes an issue where each spotify query was converted to ascii before sending. Adds a
new config option to enable legacy behaviour.
A file called japanese_track_request.json was made to mimic the Spotify
API response since I don't have the credentials. Entries in that will
need to be modified with the actual entries.
Co-authored-by: Sebastian Mohr <sebastian@mohrenclan.de>
Co-authored-by: Sebastian Mohr <39738318+semohr@users.noreply.github.com>
Co-authored-by: J0J0 Todos <2733783+JOJ0@users.noreply.github.com>
Adds replace plugin. The plugin allows the user to replace the audio
file of a song, while keeping the tags and file name.
Some music servers keep track of favourite songs via paths and tags. Now
there won't be a need to 'refavourite'. Plus, this skips the
import/merge steps.
- Instead of checking for empty `artist` query, use `va_likely`
parameter to determine whether we should query for Various Artists or
not.
- `album` / `title` is always a truthy string - no need to handle empty
criteria case
- `tracks` list always has at least one track - no need to check for
`len(items)`
Background
The `_legalize_stage` function was causing issues with Mypy due to
inconsistent type usage between the `path` and `extension` parameters.
This inconsistency stemmed from the `fragment` parameter influencing the
types of these variables.
Key issues
1. `path` was defined as `str`, while `extension` was `bytes`.
2. Depending on `fragment`, `extension` could be either `str` or `bytes`.
3. `path` was sometimes converted to `bytes` within `_legalize_stage`.
Item.destination` method
- The `fragment` parameter determined the output format:
- `False`: Returned absolute path as bytes (default)
- `True`: Returned path relative to library directory as str
Thus
- Rename `fragment` parameter to `relative_to_libdir` for clarity
- Ensure `Item.destination` returns `bytes` in all cases
- Code expecting strings now converts the output to `str`
- Use only `str` type in `_legalize_stage` and `_legalize_path`
functions
- These functions are no longer dependent on `relative_to_libdir`
FtInTitle performs a library store operation for every item it
processes, whether or not the item has changed. By limiting the
`item.store()` call to only those cases when the item has changed, the
plugin’s performance when processing an entire library improves by two
to three orders of magnitude.
Using the correct function signature for g_file_new_for_path fixes the
tests on s390x.
I do not have the full story on why this failed consistently only on
s390x, but I guess the big endian might have something to play with
this.
Here is how the tests were failing:
```
169s ___________________________ ThumbnailsTest.test_uri ____________________________
169s
169s self = <test.plugins.test_thumbnails.ThumbnailsTest testMethod=test_uri>
169s
169s def test_uri(self):
169s gio = GioURI()
169s if not gio.available:
169s self.skipTest("GIO library not found")
169s
169s > assert gio.uri("/foo") == "file:///" # silent fail
169s E AssertionError: assert '' == 'file:///'
169s E
169s E - file:///
169s
169s test/plugins/test_thumbnails.py:268: AssertionError
```
You can see a full log here [1] and a history of consistent failure
here [2]. Both links are bound to expire at some point, sorry future
archeologist 🤷.
[1]: https://autopkgtest.ubuntu.com/results/autopkgtest-plucky/plucky/s390x/b/beets/20250403_162414_5d1da@/log.gz#S5
[2]: https://autopkgtest.ubuntu.com/packages/beets/plucky/s390x
This was not thought through clearly before. It now behaves as follows
which I suppose is least surprising to a user:
- force is on, keep_existing is on, but the whitelist is DISABLED
- no stage found anything on last.fm
- fall back to the original genre
If in this example the whitelist would be ENABLED, the behaviour
changes: Only if the existing genre passes the whitelist test the
original is kept.
- Revert/fix last.fm fetcher methods to validate genres.
- In past versions (<=2.2) _resolve_genres which included whitelist
checks ran instantly after fetching last.fm tags which made sure the
next stage is hit when nothing worthwhile was found (e.g fallback
album -> artist).
- Bring back this behavior but don't run a full _resolve_genres but a
quick valid (whitelist) check only!
- Introduce an extended config/CLI option that allows to really log what
each stage fetches (prior to validation/whitelist filtering).
- Since this potentially is verbose especially with VA albums (a lot
of artist tag fetches) for performance and debug log clutter reasons
this is disabled by default.
- Clarify final last.fm tags debug log message to "valid last.fm genres"