Showing preview only (5,177K chars total). Download the full file or copy to clipboard to get everything.
Repository: beetbox/beets
Branch: master
Commit: 1943b145657a
Files: 518
Total size: 4.9 MB
Directory structure:
gitextract_bueceyr9/
├── .git-blame-ignore-revs
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ ├── config.yml
│ │ └── feature-request.md
│ ├── copilot-instructions.md
│ ├── problem-matchers/
│ │ ├── sphinx-build.json
│ │ └── sphinx-lint.json
│ ├── pull_request_template.md
│ ├── stale.yml
│ └── workflows/
│ ├── changelog_reminder.yaml
│ ├── ci.yaml
│ ├── integration_test.yaml
│ ├── lint.yaml
│ └── make_release.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CODE_OF_CONDUCT.rst
├── CONTRIBUTING.rst
├── LICENSE
├── README.rst
├── README_kr.rst
├── SECURITY.md
├── beets/
│ ├── __init__.py
│ ├── __main__.py
│ ├── autotag/
│ │ ├── __init__.py
│ │ ├── distance.py
│ │ ├── hooks.py
│ │ └── match.py
│ ├── config_default.yaml
│ ├── dbcore/
│ │ ├── __init__.py
│ │ ├── db.py
│ │ ├── query.py
│ │ ├── queryparse.py
│ │ └── types.py
│ ├── importer/
│ │ ├── __init__.py
│ │ ├── session.py
│ │ ├── stages.py
│ │ ├── state.py
│ │ └── tasks.py
│ ├── library/
│ │ ├── __init__.py
│ │ ├── exceptions.py
│ │ ├── library.py
│ │ ├── migrations.py
│ │ ├── models.py
│ │ └── queries.py
│ ├── logging.py
│ ├── mediafile.py
│ ├── metadata_plugins.py
│ ├── plugins.py
│ ├── py.typed
│ ├── test/
│ │ ├── __init__.py
│ │ ├── _common.py
│ │ └── helper.py
│ ├── ui/
│ │ ├── __init__.py
│ │ └── commands/
│ │ ├── __init__.py
│ │ ├── completion.py
│ │ ├── completion_base.sh
│ │ ├── config.py
│ │ ├── fields.py
│ │ ├── help.py
│ │ ├── import_/
│ │ │ ├── __init__.py
│ │ │ ├── display.py
│ │ │ └── session.py
│ │ ├── list.py
│ │ ├── modify.py
│ │ ├── move.py
│ │ ├── remove.py
│ │ ├── stats.py
│ │ ├── update.py
│ │ ├── utils.py
│ │ ├── version.py
│ │ └── write.py
│ └── util/
│ ├── __init__.py
│ ├── artresizer.py
│ ├── bluelet.py
│ ├── color.py
│ ├── config.py
│ ├── deprecation.py
│ ├── diff.py
│ ├── functemplate.py
│ ├── hidden.py
│ ├── id_extractors.py
│ ├── layout.py
│ ├── lyrics.py
│ ├── m3u.py
│ ├── pipeline.py
│ └── units.py
├── beetsplug/
│ ├── _typing.py
│ ├── _utils/
│ │ ├── __init__.py
│ │ ├── art.py
│ │ ├── musicbrainz.py
│ │ ├── requests.py
│ │ └── vfs.py
│ ├── absubmit.py
│ ├── acousticbrainz.py
│ ├── advancedrewrite.py
│ ├── albumtypes.py
│ ├── aura.py
│ ├── autobpm.py
│ ├── badfiles.py
│ ├── bareasc.py
│ ├── beatport.py
│ ├── bench.py
│ ├── bpd/
│ │ ├── __init__.py
│ │ └── gstplayer.py
│ ├── bpm.py
│ ├── bpsync.py
│ ├── bucket.py
│ ├── chroma.py
│ ├── convert.py
│ ├── deezer.py
│ ├── discogs/
│ │ ├── __init__.py
│ │ ├── states.py
│ │ └── types.py
│ ├── duplicates.py
│ ├── edit.py
│ ├── embedart.py
│ ├── embyupdate.py
│ ├── export.py
│ ├── fetchart.py
│ ├── filefilter.py
│ ├── fish.py
│ ├── freedesktop.py
│ ├── fromfilename.py
│ ├── ftintitle.py
│ ├── fuzzy.py
│ ├── hook.py
│ ├── ihate.py
│ ├── importadded.py
│ ├── importfeeds.py
│ ├── importsource.py
│ ├── info.py
│ ├── inline.py
│ ├── ipfs.py
│ ├── keyfinder.py
│ ├── kodiupdate.py
│ ├── lastgenre/
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── genres-tree.yaml
│ │ └── genres.txt
│ ├── lastimport.py
│ ├── limit.py
│ ├── listenbrainz.py
│ ├── loadext.py
│ ├── lyrics.py
│ ├── mbcollection.py
│ ├── mbpseudo.py
│ ├── mbsubmit.py
│ ├── mbsync.py
│ ├── metasync/
│ │ ├── __init__.py
│ │ ├── amarok.py
│ │ └── itunes.py
│ ├── missing.py
│ ├── mpdstats.py
│ ├── mpdupdate.py
│ ├── musicbrainz.py
│ ├── parentwork.py
│ ├── permissions.py
│ ├── play.py
│ ├── playlist.py
│ ├── plexupdate.py
│ ├── random.py
│ ├── replace.py
│ ├── replaygain.py
│ ├── rewrite.py
│ ├── scrub.py
│ ├── smartplaylist.py
│ ├── sonosupdate.py
│ ├── spotify.py
│ ├── subsonicplaylist.py
│ ├── subsonicupdate.py
│ ├── substitute.py
│ ├── the.py
│ ├── thumbnails.py
│ ├── titlecase.py
│ ├── types.py
│ ├── unimported.py
│ ├── web/
│ │ ├── __init__.py
│ │ ├── static/
│ │ │ ├── backbone.js
│ │ │ ├── beets.css
│ │ │ ├── beets.js
│ │ │ ├── jquery.js
│ │ │ └── underscore.js
│ │ └── templates/
│ │ └── index.html
│ └── zero.py
├── codecov.yml
├── docs/
│ ├── .gitignore
│ ├── Makefile
│ ├── _static/
│ │ └── beets.css
│ ├── _templates/
│ │ └── autosummary/
│ │ ├── base.rst
│ │ ├── class.rst
│ │ ├── module.rst
│ │ └── namedtuple.rst
│ ├── api/
│ │ ├── database.rst
│ │ ├── index.rst
│ │ ├── plugin_utilities.rst
│ │ └── plugins.rst
│ ├── changelog.rst
│ ├── code_of_conduct.rst
│ ├── conf.py
│ ├── contributing.rst
│ ├── dev/
│ │ ├── cli.rst
│ │ ├── importer.rst
│ │ ├── index.rst
│ │ ├── library.rst
│ │ ├── paths.rst
│ │ └── plugins/
│ │ ├── autotagger.rst
│ │ ├── commands.rst
│ │ ├── events.rst
│ │ ├── index.rst
│ │ └── other/
│ │ ├── config.rst
│ │ ├── fields.rst
│ │ ├── import.rst
│ │ ├── index.rst
│ │ ├── logging.rst
│ │ ├── mediafile.rst
│ │ ├── prompts.rst
│ │ └── templates.rst
│ ├── extensions/
│ │ └── conf.py
│ ├── faq.rst
│ ├── guides/
│ │ ├── advanced.rst
│ │ ├── index.rst
│ │ ├── installation.rst
│ │ ├── main.rst
│ │ └── tagger.rst
│ ├── index.rst
│ ├── modd.conf
│ ├── plugins/
│ │ ├── absubmit.rst
│ │ ├── acousticbrainz.rst
│ │ ├── advancedrewrite.rst
│ │ ├── albumtypes.rst
│ │ ├── aura.rst
│ │ ├── autobpm.rst
│ │ ├── badfiles.rst
│ │ ├── bareasc.rst
│ │ ├── beatport.rst
│ │ ├── bpd.rst
│ │ ├── bpm.rst
│ │ ├── bpsync.rst
│ │ ├── bucket.rst
│ │ ├── chroma.rst
│ │ ├── convert.rst
│ │ ├── deezer.rst
│ │ ├── discogs.rst
│ │ ├── duplicates.rst
│ │ ├── edit.rst
│ │ ├── embedart.rst
│ │ ├── embyupdate.rst
│ │ ├── export.rst
│ │ ├── fetchart.rst
│ │ ├── filefilter.rst
│ │ ├── fish.rst
│ │ ├── freedesktop.rst
│ │ ├── fromfilename.rst
│ │ ├── ftintitle.rst
│ │ ├── fuzzy.rst
│ │ ├── hook.rst
│ │ ├── ihate.rst
│ │ ├── importadded.rst
│ │ ├── importfeeds.rst
│ │ ├── importsource.rst
│ │ ├── index.rst
│ │ ├── info.rst
│ │ ├── inline.rst
│ │ ├── ipfs.rst
│ │ ├── keyfinder.rst
│ │ ├── kodiupdate.rst
│ │ ├── lastgenre.rst
│ │ ├── lastimport.rst
│ │ ├── limit.rst
│ │ ├── listenbrainz.rst
│ │ ├── loadext.rst
│ │ ├── lyrics.rst
│ │ ├── mbcollection.rst
│ │ ├── mbpseudo.rst
│ │ ├── mbsubmit.rst
│ │ ├── mbsync.rst
│ │ ├── metasync.rst
│ │ ├── missing.rst
│ │ ├── mpdstats.rst
│ │ ├── mpdupdate.rst
│ │ ├── musicbrainz.rst
│ │ ├── parentwork.rst
│ │ ├── permissions.rst
│ │ ├── play.rst
│ │ ├── playlist.rst
│ │ ├── plexupdate.rst
│ │ ├── random.rst
│ │ ├── replace.rst
│ │ ├── replaygain.rst
│ │ ├── rewrite.rst
│ │ ├── scrub.rst
│ │ ├── shared_metadata_source_config.rst
│ │ ├── smartplaylist.rst
│ │ ├── sonosupdate.rst
│ │ ├── spotify.rst
│ │ ├── subsonicplaylist.rst
│ │ ├── subsonicupdate.rst
│ │ ├── substitute.rst
│ │ ├── the.rst
│ │ ├── thumbnails.rst
│ │ ├── titlecase.rst
│ │ ├── types.rst
│ │ ├── unimported.rst
│ │ ├── web.rst
│ │ └── zero.rst
│ ├── reference/
│ │ ├── cli.rst
│ │ ├── config.rst
│ │ ├── index.rst
│ │ ├── pathformat.rst
│ │ └── query.rst
│ └── team.rst
├── extra/
│ ├── _beet
│ ├── ascii_logo.txt
│ ├── beets.reg
│ └── release.py
├── pyproject.toml
├── setup.cfg
└── test/
├── __init__.py
├── autotag/
│ ├── __init__.py
│ ├── test_autotag.py
│ ├── test_distance.py
│ ├── test_hooks.py
│ └── test_match.py
├── conftest.py
├── library/
│ ├── __init__.py
│ └── test_migrations.py
├── plugins/
│ ├── __init__.py
│ ├── conftest.py
│ ├── lyrics_pages.py
│ ├── test_acousticbrainz.py
│ ├── test_advancedrewrite.py
│ ├── test_albumtypes.py
│ ├── test_art.py
│ ├── test_aura.py
│ ├── test_autobpm.py
│ ├── test_bareasc.py
│ ├── test_beatport.py
│ ├── test_bpd.py
│ ├── test_bucket.py
│ ├── test_convert.py
│ ├── test_discogs.py
│ ├── test_edit.py
│ ├── test_embedart.py
│ ├── test_embyupdate.py
│ ├── test_export.py
│ ├── test_fetchart.py
│ ├── test_filefilter.py
│ ├── test_fromfilename.py
│ ├── test_ftintitle.py
│ ├── test_fuzzy.py
│ ├── test_hook.py
│ ├── test_ihate.py
│ ├── test_importadded.py
│ ├── test_importfeeds.py
│ ├── test_importsource.py
│ ├── test_info.py
│ ├── test_inline.py
│ ├── test_ipfs.py
│ ├── test_keyfinder.py
│ ├── test_lastgenre.py
│ ├── test_limit.py
│ ├── test_listenbrainz.py
│ ├── test_lyrics.py
│ ├── test_mbcollection.py
│ ├── test_mbpseudo.py
│ ├── test_mbsubmit.py
│ ├── test_mbsync.py
│ ├── test_missing.py
│ ├── test_mpdstats.py
│ ├── test_musicbrainz.py
│ ├── test_parentwork.py
│ ├── test_permissions.py
│ ├── test_play.py
│ ├── test_playlist.py
│ ├── test_plexupdate.py
│ ├── test_plugin_mediafield.py
│ ├── test_random.py
│ ├── test_replace.py
│ ├── test_replaygain.py
│ ├── test_scrub.py
│ ├── test_smartplaylist.py
│ ├── test_spotify.py
│ ├── test_subsonicupdate.py
│ ├── test_substitute.py
│ ├── test_the.py
│ ├── test_thumbnails.py
│ ├── test_titlecase.py
│ ├── test_types_plugin.py
│ ├── test_web.py
│ ├── test_zero.py
│ └── utils/
│ ├── __init__.py
│ ├── test_musicbrainz.py
│ └── test_vfs.py
├── rsrc/
│ ├── acousticbrainz/
│ │ └── data.json
│ ├── beetsplug/
│ │ └── test.py
│ ├── convert_stub.py
│ ├── coverart.ogg
│ ├── date_with_slashes.ogg
│ ├── discc.ogg
│ ├── empty.aiff
│ ├── empty.alac.m4a
│ ├── empty.ape
│ ├── empty.dsf
│ ├── empty.flac
│ ├── empty.m4a
│ ├── empty.mpc
│ ├── empty.ogg
│ ├── empty.opus
│ ├── empty.wma
│ ├── empty.wv
│ ├── full.aiff
│ ├── full.alac.m4a
│ ├── full.ape
│ ├── full.dsf
│ ├── full.flac
│ ├── full.m4a
│ ├── full.mpc
│ ├── full.ogg
│ ├── full.opus
│ ├── full.wma
│ ├── full.wv
│ ├── image-2x3.tiff
│ ├── image.ape
│ ├── image.flac
│ ├── image.m4a
│ ├── image.ogg
│ ├── image.wma
│ ├── itunes_library_unix.xml
│ ├── itunes_library_windows.xml
│ ├── lyrics/
│ │ ├── examplecom/
│ │ │ └── beetssong.txt
│ │ ├── geniuscom/
│ │ │ ├── 2pacalleyezonmelyrics.txt
│ │ │ ├── Ttngchinchillalyrics.txt
│ │ │ └── sample.txt
│ │ └── tekstowopl/
│ │ ├── piosenka24kgoldncityofangels1.txt
│ │ ├── piosenkabaileybiggerblackeyedsusan.txt
│ │ └── piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement.txt
│ ├── mbpseudo/
│ │ ├── official_release.json
│ │ └── pseudo_release.json
│ ├── min.flac
│ ├── min.m4a
│ ├── oldape.ape
│ ├── partial.flac
│ ├── partial.m4a
│ ├── playlist.m3u
│ ├── playlist.m3u8
│ ├── playlist_non_ext.m3u
│ ├── playlist_windows.m3u8
│ ├── pure.wma
│ ├── soundcheck-nonascii.m4a
│ ├── spotify/
│ │ ├── album_info.json
│ │ ├── japanese_track_request.json
│ │ ├── missing_request.json
│ │ ├── multiartist_album.json
│ │ ├── multiartist_track.json
│ │ ├── track_info.json
│ │ └── track_request.json
│ ├── t_time.m4a
│ ├── test_completion.sh
│ ├── unparseable.aiff
│ ├── unparseable.alac.m4a
│ ├── unparseable.ape
│ ├── unparseable.dsf
│ ├── unparseable.flac
│ ├── unparseable.m4a
│ ├── unparseable.mpc
│ ├── unparseable.ogg
│ ├── unparseable.opus
│ ├── unparseable.wma
│ ├── unparseable.wv
│ ├── whitenoise.flac
│ ├── whitenoise.opus
│ └── year.ogg
├── test_art_resize.py
├── test_datequery.py
├── test_dbcore.py
├── test_files.py
├── test_hidden.py
├── test_importer.py
├── test_library.py
├── test_logging.py
├── test_m3ufile.py
├── test_metadata_plugins.py
├── test_metasync.py
├── test_pipeline.py
├── test_plugins.py
├── test_query.py
├── test_release.py
├── test_sort.py
├── test_template.py
├── test_types.py
├── test_util.py
├── testall.py
├── ui/
│ ├── __init__.py
│ ├── commands/
│ │ ├── __init__.py
│ │ ├── test_completion.py
│ │ ├── test_config.py
│ │ ├── test_fields.py
│ │ ├── test_import.py
│ │ ├── test_list.py
│ │ ├── test_modify.py
│ │ ├── test_move.py
│ │ ├── test_remove.py
│ │ ├── test_update.py
│ │ ├── test_utils.py
│ │ └── test_write.py
│ ├── test_ui.py
│ ├── test_ui_importer.py
│ └── test_ui_init.py
└── util/
├── test_color.py
├── test_config.py
├── test_diff.py
├── test_id_extractors.py
├── test_layout.py
├── test_lyrics.py
└── test_units.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .git-blame-ignore-revs
================================================
# 2014
# flake8-cleanliness in missing
e21c04e9125a28ae0452374acf03d93315eb4381
# 2016
# Removed unicode_literals from library, logging and mediafile
43572f50b0eb3522239d94149d91223e67d9a009
# Removed unicode_literals from plugins
53d2c8d9db87be4d4750ad879bf46176537be73f
# reformat flake8 errors
1db46dfeb6607c164afb247d8da82443677795c1
# 2021
# pyupgrade root
e26276658052947e9464d9726b703335304c7c13
# pyupgrade beets dir
6d1316f463cb7c9390f85bf35b220e250a35004a
# pyupgrade autotag dir
f8b8938fd8bbe91898d0982552bc75d35703d3ef
# pyupgrade dbcore dir
d288f872903c79a7ee7c5a7c9cc690809441196e
# pyupgrade ui directory
432fa557258d9ff01e23ed750f9a86a96239599e
# pyupgrade util dir
af102c3e2f1c7a49e99839e2825906fe01780eec
# fix unused import and flake8
910354a6c617ed5aa643cff666205b43e1557373
# pyupgrade beetsplug and tests
1ec87a3bdd737abe46c6e614051bf9e314db4619
# Updates docstrings in library.py.
8c5ced3ee11a353546034189736c6001115135a4
# Fixes inconsistencies in ending quote placements for single-line docstrings.
bbd32639b4c469fe3d6668f1e3bb17d8ba7a70ce
# Fixes linting errors by removing trailing whitespaces.
acf576c455e59e8197359d4517f8c0a5a9f362bb
# Alters docstrings in library.py to be imperative-style.
2f42c8b1c019a90448d33d940b609c18ba644cbc
# 2022
# Reformat flake8 config comments
abc3dfbf429b179fac25bd1dff72d577cd4d04c7
# 2023
# Apply formatting tools to all files
a6e5201ff3fad4c69bf24d17bace2ef744b9f51b
# 2024
# Replace assertTrue
0ecc345143cf89fabe74bb2e95eedfa1114857a3
# Replace assertFalse
cb82917fe0d5476c74bb946f91ea0d9a9f019c9b
# Replace assertIsNone
5d4911e905d3a89793332eb851035e6529c0725e
# Replace assertIsNotNone
2616bcc950e592745713f28db0192293410ed3e3
# Replace assertIn
11e948121cde969f9ea27caa545a6508145572fb
# Replace assertNotIn
6631b6aef6da3e09d3531de6df7995dd5396398f
# Replace assertEqual
9a05d27acfef3788d10dd0a8db72a6c8c15dfbe9
# Replace assertNotEqual
f9359df0d15ea8ee8e3c80bc198e779f185160cb
# Replace assertIsInstance
eda0ef11d67f482fe50bbe581685b8b6a284afb9
# Replace assertLess and assertLessEqual
6a3380bcb5e803e825bd9485fcc4b70d352947eb
# Replace assertGreater and assertGreaterEqual
46bdb84b464ffec3f0ce88d53467391be7b7046f
# Replace assertCountEqual
fdb8c28271e8b22d458330598a524067ca37026e
# Replace assertListEqual
fcc4d8481df295019945ac7973906f960c58c9fb
# Use f-string syntax
4b69b493d2630b723684f259ee9e7e07c480e8ee
# Reformat the codebase
85a17ee5039628a6f3cdcb7a03d7d1bd530fbe89
# Fix lint issues
f36bc497c8c8f89004f3f6879908d3f0b25123e1
# Remove some lint exclusions and fix the issues
5f78d1b82b2292d5ce0c99623ba0ec444b80d24c
# Use PEP585 lowercase collections typing annotations
51f9dd229e64f5106d69f87906a94e75604f346b
# Remove unnecessary quotes from types
fbfdfd54446fab6782ef0629da303f14f0a2ecdf
# Replace Union types by PEP604 pipe character
7ef1b61070ed4ed79c4720d019968baf38e38050
# Update deprecated imports
161b0522bbf7f4984173fee4128416b05f6cc5f3
# Move imports required for typing under the TYPE_CHECKING block
5c81f94cf7ced476673d0fa948cc7ecda00bae99
# 2025
# Fix formatting
c490ac5810b70f3cf5fd8649669838e8fdb19f4d
# Importer restructure
9147577b2b19f43ca827e9650261a86fb0450cef
# Move functionality under MusicBrainz plugin
529aaac7dced71266c6d69866748a7d044ec20ff
# musicbrainz: reorder methods
5dc6f45110b99f0cc8dbb94251f9b1f6d69583fa
# Copy paste query, types from library to dbcore
1a045c91668c771686f4c871c84f1680af2e944b
# Library restructure (split library.py into multiple modules)
0ad4e19d4f870db757373f44d12ff3be2441363a
# Split library file into different files inside library folder.
98377ab5f6fc1829d79211b376bfd8d82bafaf33
# Use pathlib.Path in test_smartplaylist.py
d017270196dc8e0e2a4051afa5d05213946cbbbc
# Replace assertIsFile
ca4fa6ba10807f4a48a428d23e45c023c15dfa7d
# Replace assertIsDir
43b8cce063b1a1ef079266f362272307fb328d73
# Replace assertFileTag and assertNoFileTag
c6b5b3bed31704f7fe8632a6aef1a2348028348f
# Replace assertAlbumImport
3c8179a762c4387f9c40a12e3b9e560ff1c194ec
# Replace assertCount
72caf0d2cdc8fcefe1c252bdb0ac9b11b90cc649
# Docs: fix linting issues
769dcdc88a1263638ae25944ba6b2be3e8933666
# Reformat all docs using docstrfmt
ab5acaabb3cd24c482adb7fa4800c89fd6a2f08d
# Replace format calls with f-strings
4a361bd501e85de12c91c2474c423559ca672852
# Replace percent formatting
9352a79e4108bd67f7e40b1e944c01e0a7353272
# Replace string concatenation (' + ')
1c16b2b3087e9c3635d68d41c9541c4319d0bdbe
# Do not use backslashes to deal with long strings
2fccf64efe82851861e195b521b14680b480a42a
# Do not use explicit indices for logging args when not needed
d93ddf8dd43e4f9ed072a03829e287c78d2570a2
# Moved dev docs
07549ed896d9649562d40b75cd30702e6fa6e975
# Moved plugin docs Further Reading chapter
33f1a5d0bef8ca08be79ee7a0d02a018d502680d
# Moved art.py utility module from beets into beetsplug
28aee0fde463f1e18dfdba1994e2bdb80833722f
# Refactor `ui/commands.py` into multiple modules
59c93e70139f70e9fd1c6f3c1bceb005945bec33
# Moved ui.commands._utils into ui.commands.utils
25ae330044abf04045e3f378f72bbaed739fb30d
# Refactor test_ui_command.py into multiple modules
a59e41a88365e414db3282658d2aa456e0b3468a
# pyupgrade Python 3.10
301637a1609831947cb5dd90270ed46c24b1ab1b
# Fix changelog formatting
658b184c59388635787b447983ecd3a575f4fe56
# Configure future-annotations
ac7f3d9da95c2d0a32e5c908ea68480518a1582d
# Configure ruff for py310
c46069654628040316dea9db85d01b263db3ba9e
# Enable RUF rules
4749599913a42e02e66b37db9190de11d6be2cdf
# Address RUF012
bc71ec308eb938df1d349f6857634ddf2a82e339
# 2026
# Replace http URLs with https
3d0d032987c4c2e9550529fd25e79581b06ade73
# Fix broken URLs
441c8383873d72d4bb0387cdf1863c0fe9e11098
# Fix redirect URLs
ae3a2e5729e3c0a5acbd8967ba2f11f4c53acd09
# Format docs
192217da5d70621089b06b06fff3dbcbeb4c0c4d
# Add a changelog note
b3f558584910ab4fc6aa185ec4a4c8554001ba24
# Fix references to color utils
a6fcb7ba0f237530ff394a423a7cbe2ac4853c91
# Fix diff references
1d54f2bf66506e7a45ce5e962106897e3c98f67a
# Fix layout references
ffb43290066c78cb72603b7e2a0a1c90056361dd
# lastgenre: Move fetching to client module
b4beee8ff3754b001e7504c05a2b838bfa689022
================================================
FILE: .github/CODEOWNERS
================================================
# assign the entire repo to the maintainers team
* @beetbox/maintainers
# Specific ownerships:
/beets/metadata_plugins.py @semohr
/beetsplug/titlecase.py @henry-oberholtzer
/beetsplug/mbpseudo.py @asardaes
/beetsplug/_utils/requests.py @snejus
/beetsplug/_utils/musicbrainz.py @snejus
/beetsplug/musicbrainz.py @snejus
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: "\U0001F41B Bug report"
about: Report a problem with beets
---
<!--
Describe your problem, feature request, or discussion topic here.
Please fill out this and the "Setup" section below and remember to include
enough detail so that other people can reproduce the problem.
-->
### Problem
Running this command in verbose (`-vv`) mode:
```sh
$ beet -vv (... paste here ...)
```
Led to this problem:
```
(paste here)
```
Here's a link to the music files that trigger the bug (if relevant):
### Setup
* OS:
* Python version:
* beets version:
* Turning off plugins made problem go away (yes/no):
<!--
You can turn off plugins temporarily by passing --plugins= on the command line:
$ beet --plugins= version
-->
My configuration (output of `beet config`) is:
```yaml
(paste here)
```
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: 💡 Have an idea for a new feature?
url: https://github.com/beetbox/beets/discussions
about: Create a new idea discussion!
- name: 🙇 Need help with beets?
url: https://github.com/beetbox/beets/discussions
about: Create a new help discussion if it hasn't been asked before!
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: "\U0001F680 Feature request"
about: "Formalize a feature request from GitHub Discussions"
---
<!--
If you're landing here as a user, we ask you bring up your idea in the
Discussions (https://github.com/beetbox/beets/discussions).
-->
### Proposed solution
<!-- What is solution to this feature request? -->
### Objective
<!-- Ref to Discussions -->
#### Goals
<!-- What is the purpose of feature request? -->
#### Non-goals
<!--
What else could be accomplished with this feature request, but is currently out
of scope?
-->
#### Anti-goals
<!--
What could go wrong (side effects) if we implement this feature request?
-->
================================================
FILE: .github/copilot-instructions.md
================================================
## PR Review Voice
When reviewing pull requests, respond entirely in the voice of the Grug Brained Developer.
Write all comments using grug's dialect: simple words, short sentences, third-person self-reference ("grug").
Core grug beliefs to apply when reviewing:
- Complexity very, very bad. Flag any complexity demon spirit entering codebase.
Say so plainly: "complexity demon spirit enter here, grug not like"
- Prefer small, concrete PRs. Large PR make grug nervous: "big change, many place for bug hide"
- Abstraction must earn its place. Early abstraction especially dangerous: wait for cut points to emerge
- DRY is good but not absolute — simple repeated code sometimes better than complex DRY solution
- Type systems good mostly for "hit dot, see what grug can do" — not for astral projection of platonic generic models
- Generics dangerous: "temptation generics very large, complexity demon love this trick"
- Prefer readable code over clever one-liners: name intermediate variables, easier debug
- Integration tests are sweet spot — not unit tests (break on refactor), not e2e (hard debug)
- When bug found, first write regression test, then fix — this case only where "first test" acceptable to grug
- Logging very important, especially in cloud: grug learn hard way
- No premature optimisation — always need concrete perf profile first
- Simple APIs good. Layered APIs ok. Java streams make grug reach for club
- SPA frameworks increase complexity demon surface area — be suspicious
- Saying "this too complex for grug" is senior developer superpower — remove Fear Of Looking Dumb (FOLD)
================================================
FILE: .github/problem-matchers/sphinx-build.json
================================================
{
"problemMatcher": [
{
"owner": "sphinx-build",
"severity": "error",
"pattern": [
{
"regexp": "^(/[^:]+):((\\d+):)?(\\sWARNING:)?\\s*(.+)$",
"file": 1,
"line": 3,
"message": 5
}
]
}
]
}
================================================
FILE: .github/problem-matchers/sphinx-lint.json
================================================
{
"problemMatcher": [
{
"owner": "sphinx-lint",
"severity": "error",
"pattern": [
{
"regexp": "^([^:]+):(\\d+):\\s+(.*)\\s\\(([a-z-]+)\\)$",
"file": 1,
"line": 2,
"message": 3,
"code": 4
}
]
}
]
}
================================================
FILE: .github/pull_request_template.md
================================================
## Description
Fixes #X. <!-- Insert issue number here if applicable. -->
(...)
## To Do
<!--
- If you believe one of below checkpoints is not required for the change you
are submitting, cross it out and check the box nonetheless to let us know.
For example: - [x] ~Changelog~
- Regarding the changelog, often it makes sense to add your entry only once
reviewing is finished. That way you might prevent conflicts from other PR's in
that file, as well as keep the chance high your description fits with the
latest revision of your feature/fix.
- Regarding documentation, bugfixes often don't require additions to the docs.
- Please remove the descriptive sentences in braces from the enumeration below,
which helps to unclutter your PR description.
-->
- [ ] Documentation. (If you've added a new command-line flag, for example, find the appropriate page under `docs/` to describe it.)
- [ ] Changelog. (Add an entry to `docs/changelog.rst` to the bottom of one of the lists near the top of the document.)
- [ ] Tests. (Very much encouraged but not strictly required.)
================================================
FILE: .github/stale.yml
================================================
# Configuration for probot-stale - https://github.com/probot/stale
daysUntilClose: 7
staleLabel: stale
issues:
daysUntilStale: 60
onlyLabels:
- needinfo
markComment: >
Is this still relevant? If so, what is blocking it?
Is there anything you can do to help move it forward?
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
pulls:
daysUntilStale: 120
markComment: >
Is this still relevant? If so, what is blocking it?
Is there anything you can do to help move it forward?
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
================================================
FILE: .github/workflows/changelog_reminder.yaml
================================================
name: Verify changelog updated
on:
pull_request_target:
types:
- opened
- ready_for_review
jobs:
check_changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Get all updated Python files
id: changed-python-files
uses: tj-actions/changed-files@v46
with:
files: |
**.py
- name: Check for the changelog update
id: changelog-update
uses: tj-actions/changed-files@v46
with:
files: docs/changelog.rst
- name: Comment under the PR with a reminder
if: steps.changed-python-files.outputs.any_changed == 'true' && steps.changelog-update.outputs.any_changed == 'false'
uses: thollander/actions-comment-pull-request@v2
with:
message: 'Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
================================================
FILE: .github/workflows/ci.yaml
================================================
name: Test
on:
pull_request:
push:
branches:
- master
concurrency:
# Cancel previous workflow run when a new commit is pushed to a feature branch
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
PY_COLORS: 1
jobs:
test:
name: Run tests
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.platform }}
env:
IS_MAIN_PYTHON: ${{ matrix.python-version == '3.10' && matrix.platform == 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v5
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- name: Setup Python with poetry caching
# poetry cache requires poetry to already be installed, weirdly
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: poetry
- name: Install system dependencies on Windows
if: matrix.platform == 'windows-latest'
run: |
choco install mp3gain -y
- name: Install system dependencies on Ubuntu
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt update
sudo apt install --yes --no-install-recommends \
ffmpeg \
gobject-introspection \
gstreamer1.0-plugins-base \
imagemagick \
libcairo2-dev \
libgirepository-2.0-dev \
mp3gain \
pandoc \
python3-gst-1.0
- name: Get changed lyrics files
id: lyrics-update
uses: tj-actions/changed-files@v46
with:
files: |
beetsplug/lyrics.py
test/plugins/test_lyrics.py
- name: Add pytest annotator
uses: liskin/gh-problem-matcher-wrap@v3
with:
linters: pytest
action: add
- if: ${{ env.IS_MAIN_PYTHON != 'true' }}
name: Test without coverage
run: |
poetry install --without=lint --extras=autobpm --extras=discogs --extras=lyrics --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate
poe test
- if: ${{ env.IS_MAIN_PYTHON == 'true' }}
name: Test with coverage
env:
LYRICS_UPDATED: ${{ steps.lyrics-update.outputs.any_changed }}
run: |
poetry install --extras=autobpm --extras=discogs --extras=lyrics --extras=docs --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate
poe docs
poe test-with-coverage
- if: ${{ !cancelled() }}
name: Upload test results to Codecov
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- if: ${{ env.IS_MAIN_PYTHON == 'true' }}
name: Store the coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: .reports/coverage.xml
upload-coverage:
name: Upload coverage report
needs: test
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v5
- name: Get the coverage report
uses: actions/download-artifact@v5
with:
name: coverage-report
- name: Upload code coverage
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
use_oidc: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }}
================================================
FILE: .github/workflows/integration_test.yaml
================================================
name: integration tests
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * SUN" # run every Sunday at midnight
env:
PYTHON_VERSION: "3.10"
jobs:
test_integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install
- name: Test
env:
INTEGRATION_TEST: 1
run: poe test
- name: Check external links in docs
run: poe check-docs-links
- name: Notify on failure
if: ${{ failure() }}
env:
ZULIP_BOT_CREDENTIALS: ${{ secrets.ZULIP_BOT_CREDENTIALS }}
run: |
if [ -z "${ZULIP_BOT_CREDENTIALS}" ]; then
echo "Skipping notify, ZULIP_BOT_CREDENTIALS is unset"
exit 0
fi
curl -X POST https://beets.zulipchat.com/api/v1/messages \
-u "${ZULIP_BOT_CREDENTIALS}" \
-d "type=stream" \
-d "to=github" \
-d "subject=${GITHUB_WORKFLOW} - $(date -u +%Y-%m-%d)" \
-d "content=[${GITHUB_WORKFLOW}#${GITHUB_RUN_NUMBER}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) failed."
================================================
FILE: .github/workflows/lint.yaml
================================================
name: Lint check
run-name: Lint code
on:
pull_request:
push:
branches:
- master
concurrency:
# Cancel previous workflow run when a new commit is pushed to a feature branch
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
PYTHON_VERSION: "3.10"
jobs:
changed-files:
runs-on: ubuntu-latest
name: Get changed files
outputs:
any_docs_changed: ${{ steps.changed-doc-files.outputs.any_changed }}
any_python_changed: ${{ steps.raw-changed-python-files.outputs.any_changed }}
changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }}
changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }}
steps:
- uses: actions/checkout@v5
- name: Get changed docs files
id: changed-doc-files
uses: tj-actions/changed-files@v46
with:
files: |
docs/**
- name: Get changed python files
id: raw-changed-python-files
uses: tj-actions/changed-files@v46
with:
files: |
**.py
poetry.lock
- name: Check changed python files
id: changed-python-files
env:
CHANGED_PYTHON_FILES: ${{ steps.raw-changed-python-files.outputs.all_changed_files }}
run: |
if [[ " $CHANGED_PYTHON_FILES " == *" poetry.lock "* ]]; then
# if poetry.lock is changed, we need to check everything
CHANGED_PYTHON_FILES="."
fi
echo "all_changed_files=$CHANGED_PYTHON_FILES" >> "$GITHUB_OUTPUT"
format:
if: needs.changed-files.outputs.any_python_changed == 'true'
runs-on: ubuntu-latest
name: Check formatting
needs: changed-files
steps:
- uses: actions/checkout@v5
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install --only=lint
- name: Check code formatting
# the job output will contain colored diffs with what needs adjusting
run: poe check-format
lint:
if: needs.changed-files.outputs.any_python_changed == 'true'
runs-on: ubuntu-latest
name: Check linting
needs: changed-files
steps:
- uses: actions/checkout@v5
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install --only=lint
- name: Lint code
run: poe lint --output-format=github ${{ needs.changed-files.outputs.changed_python_files }}
mypy:
if: needs.changed-files.outputs.any_python_changed == 'true'
runs-on: ubuntu-latest
name: Check types with mypy
needs: changed-files
steps:
- uses: actions/checkout@v5
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install --only=typing
- name: Type check code
uses: liskin/gh-problem-matcher-wrap@v3
with:
linters: mypy
run: poe check-types --show-column-numbers --no-error-summary .
docs:
if: needs.changed-files.outputs.any_docs_changed == 'true'
runs-on: ubuntu-latest
name: Check docs
needs: changed-files
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # needed to get the full git history for the changelog check
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install --extras=docs
- name: Add Sphinx problem matchers
run: |
echo "::add-matcher::.github/problem-matchers/sphinx-build.json"
echo "::add-matcher::.github/problem-matchers/sphinx-lint.json"
- name: Check docs formatting
run: poe format-docs --check
- name: Lint docs
run: poe lint-docs
- name: Check changelog entries are added under Unreleased section
run: |
git diff --word-diff=plain -U1000 origin/${{ github.base_ref }} -- docs/changelog.rst | awk '
# match the new version header
/^[0-9]+\.[0-9]+\.[0-9]+ \(/ { past_version=1 }
# match a line that starts with a new changelog entry
/^\{\+- / && past_version {
# NR gives the line number. Subtract 5 to skip the first 5 lines that have git diff headers
print "docs/changelog.rst:" NR - 5 ": Changelog entry must be added under Unreleased section above. (changelog-unreleased)"; exit 1
}
'
- name: Build docs
run: poe docs -- -e 'SPHINXOPTS=--fail-on-warning --keep-going'
================================================
FILE: .github/workflows/make_release.yaml
================================================
name: Make a Beets Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version of the new release, just as a number with no prepended "v"'
required: true
env:
PYTHON_VERSION: "3.10"
NEW_VERSION: ${{ inputs.version }}
NEW_TAG: v${{ inputs.version }}
jobs:
increment-version:
name: Bump version, commit and create tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install --with=release --extras=docs
- name: Bump project version
run: poe bump "${{ env.NEW_VERSION }}"
- uses: EndBug/add-and-commit@v9
id: commit_and_tag
name: Commit the changes and create tag
with:
message: "Increment version to ${{ env.NEW_VERSION }}"
tag: "${{ env.NEW_TAG }} --force"
build:
name: Get changelog and build the distribution package
runs-on: ubuntu-latest
needs: increment-version
outputs:
changelog: ${{ steps.generate_changelog.outputs.changelog }}
steps:
- uses: actions/checkout@v5
with:
ref: ${{ env.NEW_TAG }}
- name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
- name: Install dependencies
run: poetry install --with=release --extras=docs
- name: Install pandoc
run: sudo apt update && sudo apt install pandoc -y
- name: Obtain the changelog
id: generate_changelog
run: |
poe docs
{
echo 'changelog<<EOF'
poe --quiet changelog
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Build a binary wheel and a source tarball
run: poe build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
publish-to-pypi:
name: Publish distribution 📦 to PyPI
runs-on: ubuntu-latest
needs: build
environment:
name: pypi
url: https://pypi.org/p/beets
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
make-github-release:
name: Create GitHub release
runs-on: ubuntu-latest
needs: [build, publish-to-pypi]
env:
CHANGELOG: ${{ needs.build.outputs.changelog }}
steps:
- name: Download all the dists
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/
- name: Create a GitHub release
id: make_release
uses: ncipollo/release-action@v1
with:
tag: ${{ env.NEW_TAG }}
name: Release ${{ env.NEW_TAG }}
body: ${{ env.CHANGELOG }}
artifacts: dist/*
- name: Send release toot to Fosstodon
uses: cbrgm/mastodon-github-action@v2
continue-on-error: true
with:
access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }}
url: ${{ secrets.MASTODON_URL }}
message: "Version ${{ env.NEW_TAG }} of beets has been released! Check out all of the new changes at ${{ steps.make_release.outputs.html_url }}"
================================================
FILE: .gitignore
================================================
# general hidden files/directories
.DS_Store
.idea
# file patterns
*~
# Project Specific patterns
man
# The rest is from https://www.gitignore.io/api/python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
coverage.xml
*,cover
.hypothesis/
.reports
# Flask stuff:
instance/
.webassets-cache
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# dotenv
.env
# virtualenv
env/
venv/
.venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# PyDev and Eclipse project settings
/.project
/.pydevproject
/.settings
.vscode
# pyright
pyrightconfig.json
# Pyrefly
pyrefly.toml
================================================
FILE: .pre-commit-config.yaml
================================================
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: local
hooks:
- id: format
name: Format Python files
entry: poe format
language: system
files: '.*.py'
pass_filenames: true
- id: format-docs
name: Format docs
entry: poe format-docs
language: system
files: '.*.rst'
pass_filenames: true
================================================
FILE: .readthedocs.yaml
================================================
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs
================================================
FILE: CODE_OF_CONDUCT.rst
================================================
Contributor Covenant Code of Conduct
====================================
Our Pledge
----------
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
Our Standards
-------------
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
Enforcement Responsibilities
----------------------------
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
Scope
-----
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
Enforcement
-----------
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at here on Github.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
Enforcement Guidelines
----------------------
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
1. Correction
~~~~~~~~~~~~~
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
2. Warning
~~~~~~~~~~
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
3. Temporary Ban
~~~~~~~~~~~~~~~~
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
4. Permanent Ban
~~~~~~~~~~~~~~~~
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
Attribution
-----------
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available `here
<https://www.contributor-covenant.org/version/2/1/code_of_conduct/>`_.
Community Impact Guidelines were inspired by Mozilla's code of conduct
enforcement ladder.
For answers to common questions about this code of conduct, see the `FAQ
<https://www.contributor-covenant.org/faq>`_. Translations are available at
`translations <https://www.contributor-covenant.org/translations>`_.
================================================
FILE: CONTRIBUTING.rst
================================================
Contributing
============
.. contents::
:depth: 3
Thank you!
----------
First off, thank you for considering contributing to beets! It’s people like you
that make beets continue to succeed.
These guidelines describe how you can help most effectively. By following these
guidelines, you can make life easier for the development team as it indicates
you respect the maintainers’ time; in return, the maintainers will reciprocate
by helping to address your issue, review changes, and finalize pull requests.
Types of Contributions
----------------------
We love to get contributions from our community—you! There are many ways to
contribute, whether you’re a programmer or not.
The first thing to do, regardless of how you'd like to contribute to the
project, is to check out our :doc:`Code of Conduct <code_of_conduct>` and to
keep that in mind while interacting with other contributors and users.
Non-Programming
~~~~~~~~~~~~~~~
- Promote beets! Help get the word out by telling your friends, writing a blog
post, or discussing it on a forum you frequent.
- Improve the documentation_. It’s incredibly easy to contribute here: just find
a page you want to modify and hit the “Edit on GitHub” button in the
upper-right. You can automatically send us a pull request for your changes.
- GUI design. For the time being, beets is a command-line-only affair. But
that’s mostly because we don’t have any great ideas for what a good GUI should
look like. If you have those great ideas, please get in touch.
- Benchmarks. We’d like to have a consistent way of measuring speed improvements
in beets’ tagger and other functionality as well as a way of comparing beets’
performance to other tools. You can help by compiling a library of
freely-licensed music files (preferably with incorrect metadata) for testing
and measurement.
- Think you have a nice config or cool use-case for beets? We’d love to hear
about it! Submit a post to our `discussion board
<https://github.com/beetbox/beets/discussions/categories/show-and-tell>`__
under the “Show and Tell” category for a chance to get featured in `the docs
<https://beets.readthedocs.io/en/stable/guides/advanced.html>`__.
- Consider helping out fellow users by `responding to support requests
<https://github.com/beetbox/beets/discussions/categories/q-a>`__ .
Programming
~~~~~~~~~~~
- As a programmer (even if you’re just a beginner!), you have a ton of
opportunities to get your feet wet with beets.
- For developing plugins, or hacking away at beets, there’s some good
information in the `“For Developers” section of the docs
<https://beets.readthedocs.io/en/stable/dev/>`__.
.. _development-tools:
Development Tools
+++++++++++++++++
In order to develop beets, you will need a few tools installed:
- poetry_ for packaging, virtual environment and dependency management
- poethepoet_ to run tasks, such as linting, formatting, testing
Python community recommends using pipx_ to install stand-alone command-line
applications such as above. pipx_ installs each application in an isolated
virtual environment, where its dependencies will not interfere with your system
and other CLI tools.
If you do not have pipx_ installed in your system, follow `pipx installation
instructions <https://pipx.pypa.io/stable/installation/>`__ or
.. code-block:: sh
$ python3 -m pip install --user pipx
Install poetry_ and poethepoet_ using pipx_:
::
$ pipx install poetry poethepoet
.. admonition:: Check ``tool.pipx-install`` section in ``pyproject.toml`` to see supported versions
.. code-block:: toml
[tool.pipx-install]
poethepoet = ">=0.26"
poetry = "<2"
.. _getting-the-source:
Getting the Source
++++++++++++++++++
The easiest way to get started with the latest beets source is to clone the
repository and install ``beets`` in a local virtual environment using poetry_.
This can be done with:
.. code-block:: bash
$ git clone https://github.com/beetbox/beets.git
$ cd beets
$ poetry install
This will install ``beets`` and all development dependencies into its own
virtual environment in your ``$POETRY_CACHE_DIR``. See ``poetry install --help``
for installation options, including installing ``extra`` dependencies for
plugins.
In order to run something within this virtual environment, start the command
with ``poetry run`` to them, for example ``poetry run pytest``.
On the other hand, it may get tedious to type ``poetry run`` before every
command. Instead, you can activate the virtual environment in your shell with:
::
$ poetry shell
You should see ``(beets-py3.10)`` prefix in your shell prompt. Now you can run
commands directly, for example:
::
$ (beets-py3.10) pytest
Additionally, poethepoet_ task runner assists us with the most common
operations. Formatting, linting, testing are defined as ``poe`` tasks in
pyproject.toml_. Run:
::
$ poe
to see all available tasks. They can be used like this, for example
.. code-block:: sh
$ poe lint # check code style
$ poe format # fix formatting issues
$ poe test # run tests
# ... fix failing tests
$ poe test --lf # re-run failing tests (note the additional pytest option)
$ poe check-types --pretty # check types with an extra option for mypy
Code Contribution Ideas
+++++++++++++++++++++++
- We maintain a set of `issues marked as “good first issue”
<https://github.com/beetbox/beets/labels/good%20first%20issue>`__. These are
issues that would serve as a good introduction to the codebase. Claim one and
start exploring!
- Like testing? Our `test coverage
<https://app.codecov.io/github/beetbox/beets>`__ is somewhat low. You can help
out by finding low-coverage modules or checking out other `testing-related
issues <https://github.com/beetbox/beets/labels/testing>`__.
- There are several ways to improve the tests in general (see :ref:`testing` and
some places to think about performance optimization (see `Optimization
<https://github.com/beetbox/beets/wiki/Optimization>`__).
- Not all of our code is up to our coding conventions. In particular, the
`library API documentation
<https://beets.readthedocs.io/en/stable/dev/library.html>`__ are currently
quite sparse. You can help by adding to the docstrings in the code and to the
documentation pages themselves. beets follows `PEP-257
<https://peps.python.org/pep-0257/>`__ for docstrings and in some places, we
also sometimes use `ReST autodoc syntax for Sphinx
<https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`__ to,
for example, refer to a class name.
Your First Contribution
-----------------------
If this is your first time contributing to an open source project, welcome! If
you are confused at all about how to contribute or what to contribute, take a
look at `this great tutorial <https://makeapullrequest.com/>`__, or stop by our
`discussion board`_ if you have any questions.
We maintain a list of issues we reserved for those new to open source labeled
`first timers only`_. Since the goal of these issues is to get users comfortable
with contributing to an open source project, please do not hesitate to ask any
questions.
.. _first timers only: https://github.com/beetbox/beets/issues?q=is%3Aopen+is%3Aissue+label%3A%22first+timers+only%22
How to Submit Your Work
-----------------------
Do you have a great bug fix, new feature, or documentation expansion you’d like
to contribute? Follow these steps to create a GitHub pull request and your code
will ship in no time.
1. Fork the beets repository and clone it (see above) to create a workspace.
2. Install pre-commit, following the instructions `here
<https://pre-commit.com/>`_.
3. Make your changes.
4. Add tests. If you’ve fixed a bug, write a test to ensure that you’ve actually
fixed it. If there’s a new feature or plugin, please contribute tests that
show that your code does what it says.
5. Add documentation. If you’ve added a new command flag, for example, find the
appropriate page under ``docs/`` where it needs to be listed.
6. Add a changelog entry to ``docs/changelog.rst`` near the top of the document.
7. Run the tests and style checker, see :ref:`testing`.
8. Push to your fork and open a pull request! We’ll be in touch shortly.
9. If you add commits to a pull request, please add a comment or re-request a
review after you push them since GitHub doesn’t automatically notify us when
commits are added.
Remember, code contributions have four parts: the code, the tests, the
documentation, and the changelog entry. Thank you for contributing!
.. admonition:: Ownership
If you are the owner of a plugin, please consider reviewing pull requests
that affect your plugin. If you are not the owner of a plugin, please
consider becoming one! You can do so by adding an entry to
``.github/CODEOWNERS``. This way, you will automatically receive a review
request for pull requests that adjust the code that you own. If you have any
questions, please ask on our `discussion board`_.
The Code
--------
The documentation has a section on the `library API
<https://beets.readthedocs.io/en/stable/dev/library.html>`__ that serves as an
introduction to beets’ design.
Coding Conventions
------------------
General
~~~~~~~
There are a few coding conventions we use in beets:
- Whenever you access the library database, do so through the provided Library
methods or via a Transaction object. Never call ``lib.conn.*`` directly. For
example, do this:
.. code-block:: python
with g.lib.transaction() as tx:
rows = tx.query("SELECT DISTINCT {field} FROM {model._table} ORDER BY {sort_field}")
To fetch Item objects from the database, use lib.items(…) and supply a query
as an argument. Resist the urge to write raw SQL for your query. If you must
use lower-level queries into the database, do this, for example:
.. code-block:: python
with lib.transaction() as tx:
rows = tx.query("SELECT path FROM items WHERE album_id = ?", (album_id,))
Transaction objects help control concurrent access to the database and assist
in debugging conflicting accesses.
- f-strings should be used instead of the ``%`` operator and ``str.format()``
calls.
- Never ``print`` informational messages; use the `logging
<https://docs.python.org/3/library/logging.html>`__ module instead. In
particular, we have our own logging shim, so you’ll see ``from beets import
logging`` in most files.
- The loggers use `str.format
<https://docs.python.org/3/library/stdtypes.html>`__-style logging instead
of ``%``-style, so you can type ``log.debug("{}", obj)`` to do your
formatting.
- Exception handlers must use ``except A as B:`` instead of ``except A, B:``.
Style
~~~~~
We use `ruff <https://docs.astral.sh/ruff/>`__ to format and lint the codebase.
Run ``poe check-format`` and ``poe lint`` to check your code for style and
linting errors. Running ``poe format`` will automatically format your code
according to the specifications required by the project.
Similarly, run ``poe format-docs`` and ``poe lint-docs`` to ensure consistent
documentation formatting and check for any issues.
Editor Settings
~~~~~~~~~~~~~~~
Personally, I work on beets with vim_. Here are some ``.vimrc`` lines that might
help with PEP 8-compliant Python coding:
::
filetype indent on
autocmd FileType python setlocal shiftwidth=4 tabstop=4 softtabstop=4 expandtab shiftround autoindent
Consider installing `this alternative Python indentation plugin
<https://github.com/mitsuhiko/vim-python-combined>`__. I also like `neomake
<https://github.com/neomake/neomake>`__ with its flake8 checker.
.. _testing:
Testing
-------
Running the Tests
~~~~~~~~~~~~~~~~~
Use ``poe`` to run tests:
::
$ poe test [pytest options]
You can disable a hand-selected set of "slow" tests by setting the environment
variable ``SKIP_SLOW_TESTS``, for example:
::
$ SKIP_SLOW_TESTS=1 poe test
Coverage
++++++++
The ``test`` command does not include coverage as it slows down testing. In
order to measure it, use the ``test-with-coverage`` task
$ poe test-with-coverage [pytest options]
You are welcome to explore coverage by opening the HTML report in
``.reports/html/index.html``.
Note that for each covered line the report shows **which tests cover it**
(expand the list on the right-hand side of the affected line).
You can find project coverage status on Codecov_.
Red Flags
+++++++++
The pytest-random_ plugin makes it easy to randomize the order of tests. ``poe
test --random`` will occasionally turn up failing tests that reveal ordering
dependencies—which are bad news!
Test Dependencies
+++++++++++++++++
The tests have a few more dependencies than beets itself. (The additional
dependencies consist of testing utilities and dependencies of non-default
plugins exercised by the test suite.) The dependencies are listed under the
``tool.poetry.group.test.dependencies`` section in pyproject.toml_.
Writing Tests
~~~~~~~~~~~~~
Writing tests is done by adding or modifying files in folder test_. Take a look
at test-query_ to get a basic view on how tests are written. Since we are
currently migrating the tests from unittest_ to pytest_, new tests should be
written using pytest_. Contributions migrating existing tests are welcome!
External API requests under test should be mocked with requests-mock_, However,
we still want to know whether external APIs are up and that they return expected
responses, therefore we test them weekly with our `integration test`_ suite.
In order to add such a test, mark your test with the ``integration_test`` marker
.. code-block:: python
@pytest.mark.integration_test
def test_external_api_call(): ...
This way, the test will be run only in the integration test suite.
beets also defines custom pytest markers in ``test/conftest.py``:
- ``integration_test``: runs only when ``INTEGRATION_TEST=true`` is set.
- ``on_lyrics_update``: runs only when ``LYRICS_UPDATED=true`` is set.
- ``requires_import("module", force_ci=True)``: runs the test only when the
module is importable. With the default ``force_ci=True``, this import check is
bypassed on GitHub Actions for ``beetbox/beets`` so CI still runs the test.
Set ``force_ci=False`` to allow CI to skip when the module is missing.
.. code-block:: python
@pytest.mark.integration_test
def test_external_api_call(): ...
@pytest.mark.on_lyrics_update
def test_real_lyrics_backend(): ...
@pytest.mark.requires_import("langdetect")
def test_language_detection(): ...
@pytest.mark.requires_import("librosa", force_ci=False)
def test_autobpm_command(): ...
.. _codecov: https://app.codecov.io/github/beetbox/beets
.. _discussion board: https://github.com/beetbox/beets/discussions
.. _documentation: https://beets.readthedocs.io/en/stable/
.. _integration test: https://github.com/beetbox/beets/actions?query=workflow%3A%22integration+tests%22
.. _pipx: https://pipx.pypa.io/stable
.. _poethepoet: https://poethepoet.natn.io/index.html
.. _poetry: https://python-poetry.org/docs/
.. _pyproject.toml: https://github.com/beetbox/beets/blob/master/pyproject.toml
.. _pytest: https://docs.pytest.org/en/stable/
.. _pytest-random: https://github.com/klrmn/pytest-random
.. _requests-mock: https://requests-mock.readthedocs.io/en/latest/response.html
.. _test: https://github.com/beetbox/beets/tree/master/test
.. _test-query: https://github.com/beetbox/beets/blob/master/test/test_query.py
.. _unittest: https://docs.python.org/3/library/unittest.html
.. _vim: https://www.vim.org/
================================================
FILE: LICENSE
================================================
The MIT License
Copyright (c) 2010-2016 Adrian Sampson
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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.rst
================================================
.. image:: https://img.shields.io/pypi/v/beets.svg
:target: https://pypi.python.org/pypi/beets
.. image:: https://img.shields.io/codecov/c/github/beetbox/beets.svg
:target: https://app.codecov.io/github/beetbox/beets
.. image:: https://img.shields.io/github/actions/workflow/status/beetbox/beets/ci.yaml
:target: https://github.com/beetbox/beets/actions
.. image:: https://repology.org/badge/tiny-repos/beets.svg
:target: https://repology.org/project/beets/versions
beets
=====
Beets is the media library management system for obsessive music geeks.
The purpose of beets is to get your music collection right once and for all. It
catalogs your collection, automatically improving its metadata as it goes. It
then provides a suite of tools for manipulating and accessing your music.
Here's an example of beets' brainy tag corrector doing its thing:
::
$ beet import ~/music/ladytron
Tagging:
Ladytron - Witching Hour
(Similarity: 98.4%)
* Last One Standing -> The Last One Standing
* Beauty -> Beauty*2
* White Light Generation -> Whitelightgenerator
* All the Way -> All the Way...
Because beets is designed as a library, it can do almost anything you can
imagine for your music collection. Via plugins_, beets becomes a panacea:
- Fetch or calculate all the metadata you could possibly need: `album art`_,
lyrics_, genres_, tempos_, ReplayGain_ levels, or `acoustic fingerprints`_.
- Get metadata from MusicBrainz_, Discogs_, and Beatport_. Or guess metadata
using songs' filenames or their acoustic fingerprints.
- `Transcode audio`_ to any format you like.
- Check your library for `duplicate tracks and albums`_ or for `albums that are
missing tracks`_.
- Clean up crufty tags left behind by other, less-awesome tools.
- Embed and extract album art from files' metadata.
- Browse your music library graphically through a Web browser and play it in any
browser that supports `HTML5 Audio`_.
- Analyze music files' metadata from the command line.
- Listen to your library with a music player that speaks the MPD_ protocol and
works with a staggering variety of interfaces.
If beets doesn't do what you want yet, `writing your own plugin`_ is shockingly
simple if you know a little Python.
.. _acoustic fingerprints: https://beets.readthedocs.org/page/plugins/chroma.html
.. _album art: https://beets.readthedocs.org/page/plugins/fetchart.html
.. _albums that are missing tracks: https://beets.readthedocs.org/page/plugins/missing.html
.. _beatport: https://www.beatport.com
.. _discogs: https://www.discogs.com/
.. _duplicate tracks and albums: https://beets.readthedocs.org/page/plugins/duplicates.html
.. _genres: https://beets.readthedocs.org/page/plugins/lastgenre.html
.. _html5 audio: https://html.spec.whatwg.org/multipage/media.html#the-audio-element
.. _lyrics: https://beets.readthedocs.org/page/plugins/lyrics.html
.. _mpd: https://www.musicpd.org/
.. _musicbrainz: https://musicbrainz.org/
.. _musicbrainz music collection: https://musicbrainz.org/doc/Collections/
.. _plugins: https://beets.readthedocs.org/page/plugins/
.. _replaygain: https://beets.readthedocs.org/page/plugins/replaygain.html
.. _tempos: https://beets.readthedocs.org/page/plugins/acousticbrainz.html
.. _transcode audio: https://beets.readthedocs.org/page/plugins/convert.html
.. _writing your own plugin: https://beets.readthedocs.org/page/dev/plugins/index.html
Install
-------
You can install beets by typing ``pip install beets`` or directly from Github
(see details here_). Beets has also been packaged in the `software
repositories`_ of several distributions. Check out the `Getting Started`_ guide
for more information.
.. _getting started: https://beets.readthedocs.org/page/guides/main.html
.. _here: https://beets.readthedocs.io/en/latest/faq.html#run-the-latest-source-version-of-beets
.. _software repositories: https://repology.org/project/beets/versions
Contribute
----------
Thank you for considering contributing to ``beets``! Whether you're a programmer
or not, you should be able to find all the info you need at CONTRIBUTING.rst_.
.. _contributing.rst: https://github.com/beetbox/beets/blob/master/CONTRIBUTING.rst
Read More
---------
Learn more about beets at `its Web site`_. Follow `@b33ts`_ on Mastodon for news
and updates.
.. _@b33ts: https://fosstodon.org/@beets
.. _its web site: https://beets.io/
Contact
-------
- Encountered a bug you'd like to report? Check out our `issue tracker`_!
- If your issue hasn't already been reported, please `open a new ticket`_ and
we'll be in touch with you shortly.
- If you'd like to vote on a feature/bug, simply give a :+1: on issues you'd
like to see prioritized over others.
- Need help/support, would like to start a discussion, have an idea for a new
feature, or would just like to introduce yourself to the team? Check out
`GitHub Discussions`_!
.. _github discussions: https://github.com/beetbox/beets/discussions
.. _issue tracker: https://github.com/beetbox/beets/issues
.. _open a new ticket: https://github.com/beetbox/beets/issues/new/choose
Authors
-------
Beets is by `Adrian Sampson`_ with a supporting cast of thousands.
.. _adrian sampson: https://www.cs.cornell.edu/~asampson/
================================================
FILE: README_kr.rst
================================================
.. image:: https://img.shields.io/pypi/v/beets.svg
:target: https://pypi.python.org/pypi/beets
.. image:: https://img.shields.io/codecov/c/github/beetbox/beets.svg
:target: https://app.codecov.io/github/beetbox/beets
.. image:: https://travis-ci.org/beetbox/beets.svg?branch=master
:target: https://travis-ci.org/beetbox/beets
beets
=====
Beets는 강박적인 음악을 듣는 사람들을 위한 미디어 라이브러리 관리 시스템이다.
Beets의 목적은 음악들을 한번에 다 받는 것이다. 음악들을 카탈로그화 하고, 자동으로 메타 데이터를 개선한다. 그리고 음악에 접근하고 조작할
수 있는 도구들을 제공한다.
다음은 Beets의 brainy tag corrector가 한 일의 예시이다.
::
$ beet import ~/music/ladytron
Tagging:
Ladytron - Witching Hour
(Similarity: 98.4%)
* Last One Standing -> The Last One Standing
* Beauty -> Beauty*2
* White Light Generation -> Whitelightgenerator
* All the Way -> All the Way...
Beets는 라이브러리로 디자인 되었기 때문에, 당신이 음악들에 대해 상상하는 모든 것을 할 수 있다. plugins_ 을 통해서 모든 것을 할
수 있는 것이다!
- 필요하는 메타 데이터를 계산하거나 패치 할 때: `album art`_, lyrics_, genres_, tempos_,
ReplayGain_ levels, or `acoustic fingerprints`_.
- MusicBrainz_, Discogs_,`Beatport`_로부터 메타데이터를 가져오거나, 노래 제목이나 음향 특징으로 메타데이터를
추측한다
- `Transcode audio`_ 당신이 좋아하는 어떤 포맷으로든 변경한다.
- 당신의 라이브러리에서 `duplicate tracks and albums`_ 이나 `albums that are missing
tracks`_ 를 검사한다.
- 남이 남기거나, 좋지 않은 도구로 남긴 잡다한 태그들을 지운다.
- 파일의 메타데이터에서 앨범 아트를 삽입이나 추출한다.
- 당신의 음악들을 `HTML5 Audio`_ 를 지원하는 어떤 브라우저든 재생할 수 있고, 웹 브라우저에 표시 할 수 있다.
- 명령어로부터 음악 파일의 메타데이터를 분석할 수 있다.
- MPD_ 프로토콜을 사용하여 음악 플레이어로 음악을 들으면, 엄청나게 다양한 인터페이스로 작동한다.
만약 Beets에 당신이 원하는게 아직 없다면, 당신이 python을 안다면 `writing your own plugin`_ _은 놀라울정도로
간단하다.
.. _acoustic fingerprints: https://beets.readthedocs.org/page/plugins/chroma.html
.. _album art: https://beets.readthedocs.org/page/plugins/fetchart.html
.. _albums that are missing tracks: https://beets.readthedocs.org/page/plugins/missing.html
.. _beatport: https://www.beatport.com
.. _discogs: https://www.discogs.com/
.. _duplicate tracks and albums: https://beets.readthedocs.org/page/plugins/duplicates.html
.. _genres: https://beets.readthedocs.org/page/plugins/lastgenre.html
.. _html5 audio: https://html.spec.whatwg.org/multipage/media.html#the-audio-element
.. _lyrics: https://beets.readthedocs.org/page/plugins/lyrics.html
.. _mpd: https://www.musicpd.org/
.. _musicbrainz: https://musicbrainz.org/
.. _musicbrainz music collection: https://musicbrainz.org/doc/Collections/
.. _plugins: https://beets.readthedocs.org/page/plugins/
.. _replaygain: https://beets.readthedocs.org/page/plugins/replaygain.html
.. _tempos: https://beets.readthedocs.org/page/plugins/acousticbrainz.html
.. _transcode audio: https://beets.readthedocs.org/page/plugins/convert.html
.. _writing your own plugin: https://beets.readthedocs.org/page/dev/plugins/index.html
설치
-------
당신은 ``pip install beets`` 을 사용해서 Beets를 설치할 수 있다. 그리고 `Getting Started`_ 가이드를
확인할 수 있다.
.. _getting started: https://beets.readthedocs.org/page/guides/main.html
컨트리뷰션
----------
어떻게 도우려는지 알고싶다면 Hacking_ 위키페이지를 확인하라. 당신은 docs 안에 `For Developers`_ 에도 관심이 있을수
있다.
.. _for developers: https://beets.readthedocs.io/en/stable/dev/
.. _hacking: https://github.com/beetbox/beets/wiki/Hacking
Read More
---------
`its Web site`_ 에서 Beets에 대해 조금 더 알아볼 수 있다. 트위터에서 `@b33ts`_ 를 팔로우하면 새 소식을 볼 수
있다.
.. _@b33ts: https://twitter.com/b33ts/
.. _its web site: https://beets.io/
저자들
-------
`Adrian Sampson`_ 와 많은 사람들의 지지를 받아 Beets를 만들었다. 돕고 싶다면 forum_.를 방문하면 된다.
.. _adrian sampson: https://www.cs.cornell.edu/~asampson/
.. _forum: https://github.com/beetbox/beets/discussions/
================================================
FILE: SECURITY.md
================================================
# 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
================================================
FILE: beets/__init__.py
================================================
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# 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.
from sys import stderr
import confuse
from .util.deprecation import deprecate_imports
__version__ = "2.7.1"
__author__ = "Adrian Sampson <adrian@radbox.org>"
def __getattr__(name: str):
"""Handle deprecated imports."""
return deprecate_imports(
__name__,
{"art": "beetsplug._utils", "vfs": "beetsplug._utils"},
name,
)
class IncludeLazyConfig(confuse.LazyConfig):
"""A version of Confuse's LazyConfig that also merges in data from
YAML files specified in an `include` setting.
"""
def read(self, user: bool = True, defaults: bool = True) -> None:
super().read(user, defaults)
try:
for view in self["include"].sequence():
self.set_file(view.as_filename())
except confuse.NotFoundError:
pass
except confuse.ConfigReadError as err:
stderr.write(f"configuration `import` failed: {err.reason}")
config = IncludeLazyConfig("beets", __name__)
================================================
FILE: beets/__main__.py
================================================
# This file is part of beets.
# Copyright 2017, Adrian Sampson.
#
# 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.
"""The __main__ module lets you run the beets CLI interface by typing
`python -m beets`.
"""
import sys
from .ui import main
if __name__ == "__main__":
main(sys.argv[1:])
================================================
FILE: beets/autotag/__init__.py
================================================
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# 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.
"""Facilities for automatically determining files' correct metadata."""
from __future__ import annotations
from importlib import import_module
from typing import TYPE_CHECKING
from beets import config, logging
# Parts of external interface.
from beets.util import unique_list
from beets.util.deprecation import deprecate_for_maintainers, deprecate_imports
from .hooks import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch
from .match import Proposal, Recommendation, tag_album, tag_item
if TYPE_CHECKING:
from collections.abc import Sequence
from beets.library import Album, Item, LibModel
def __getattr__(name: str):
if name == "current_metadata":
deprecate_for_maintainers(
f"'beets.autotag.{name}'", "'beets.util.get_most_common_tags'"
)
return import_module("beets.util").get_most_common_tags
return deprecate_imports(
__name__, {"Distance": "beets.autotag.distance"}, name
)
__all__ = [
"AlbumInfo",
"AlbumMatch",
"Proposal",
"Recommendation",
"TrackInfo",
"TrackMatch",
"apply_album_metadata",
"apply_item_metadata",
"apply_metadata",
"tag_album",
"tag_item",
]
# Global logger.
log = logging.getLogger("beets")
# Metadata fields that are already hardcoded, or where the tag name changes.
SPECIAL_FIELDS = {
"album": (
"va",
"releasegroup_id",
"artist_id",
"artists_ids",
"album_id",
"mediums",
"tracks",
"year",
"month",
"day",
"artist",
"artists",
"artist_credit",
"artists_credit",
"artist_sort",
"artists_sort",
"data_url",
),
"track": (
"track_alt",
"artist_id",
"artists_ids",
"release_track_id",
"medium",
"index",
"medium_index",
"title",
"artist_credit",
"artists_credit",
"artist_sort",
"artists_sort",
"artist",
"artists",
"track_id",
"medium_total",
"data_url",
"length",
),
}
# Additional utilities for the main interface.
def _apply_metadata(
info: AlbumInfo | TrackInfo,
db_obj: Album | Item,
nullable_fields: Sequence[str] = [],
):
"""Set the db_obj's metadata to match the info."""
special_fields = SPECIAL_FIELDS[
"album" if isinstance(info, AlbumInfo) else "track"
]
for field, value in info.items():
# We only overwrite fields that are not already hardcoded.
if field in special_fields:
continue
# Don't overwrite fields with empty values unless the
# field is explicitly allowed to be overwritten.
if value is None and field not in nullable_fields:
continue
db_obj[field] = value
def correct_list_fields(m: LibModel) -> None:
"""Synchronise single and list values for the list fields that we use.
That is, ensure the same value in the single field and the first element
in the list.
For context, the value we set as, say, ``mb_artistid`` is simply ignored:
Under the current :class:`MediaFile` implementation, fields ``albumtype``,
``mb_artistid`` and ``mb_albumartistid`` are mapped to the first element of
``albumtypes``, ``mb_artistids`` and ``mb_albumartistids`` respectively.
This means setting ``mb_artistid`` has no effect. However, beets
functionality still assumes that ``mb_artistid`` is independent and stores
its value in the database. If ``mb_artistid`` != ``mb_artistids[0]``,
``beet write`` command thinks that ``mb_artistid`` is modified and tries to
update the field in the file. Of course nothing happens, so the same diff
is shown every time the command is run.
We can avoid this issue by ensuring that ``mb_artistid`` has the same value
as ``mb_artistids[0]``, and that's what this function does.
Note: :class:`Album` model does not have ``mb_artistids`` and
``mb_albumartistids`` fields therefore we need to check for their presence.
"""
def ensure_first_value(single_field: str, list_field: str) -> None:
"""Ensure the first ``list_field`` item is equal to ``single_field``."""
single_val, list_val = getattr(m, single_field), getattr(m, list_field)
if single_val:
setattr(m, list_field, unique_list([single_val, *list_val]))
elif list_val:
setattr(m, single_field, list_val[0])
ensure_first_value("albumtype", "albumtypes")
if hasattr(m, "mb_artistids"):
ensure_first_value("mb_artistid", "mb_artistids")
if hasattr(m, "mb_albumartistids"):
ensure_first_value("mb_albumartistid", "mb_albumartistids")
def apply_item_metadata(item: Item, track_info: TrackInfo):
"""Set an item's metadata from its matched TrackInfo object."""
item.artist = track_info.artist
item.artists = track_info.artists
item.artist_sort = track_info.artist_sort
item.artists_sort = track_info.artists_sort
item.artist_credit = track_info.artist_credit
item.artists_credit = track_info.artists_credit
item.title = track_info.title
item.mb_trackid = track_info.track_id
item.mb_releasetrackid = track_info.release_track_id
if track_info.artist_id:
item.mb_artistid = track_info.artist_id
if track_info.artists_ids:
item.mb_artistids = track_info.artists_ids
_apply_metadata(track_info, item)
correct_list_fields(item)
# At the moment, the other metadata is left intact (including album
# and track number). Perhaps these should be emptied?
def apply_album_metadata(album_info: AlbumInfo, album: Album):
"""Set the album's metadata to match the AlbumInfo object."""
_apply_metadata(album_info, album)
correct_list_fields(album)
def apply_metadata(
album_info: AlbumInfo, item_info_pairs: list[tuple[Item, TrackInfo]]
):
"""Set items metadata to match corresponding tagged info."""
for item, track_info in item_info_pairs:
# Artist or artist credit.
if config["artist_credit"]:
item.artist = (
track_info.artist_credit
or track_info.artist
or album_info.artist_credit
or album_info.artist
)
item.artists = (
track_info.artists_credit
or track_info.artists
or album_info.artists_credit
or album_info.artists
)
item.albumartist = album_info.artist_credit or album_info.artist
item.albumartists = album_info.artists_credit or album_info.artists
else:
item.artist = track_info.artist or album_info.artist
item.artists = track_info.artists or album_info.artists
item.albumartist = album_info.artist
item.albumartists = album_info.artists
# Album.
item.album = album_info.album
# Artist sort and credit names.
item.artist_sort = track_info.artist_sort or album_info.artist_sort
item.artists_sort = track_info.artists_sort or album_info.artists_sort
item.artist_credit = (
track_info.artist_credit or album_info.artist_credit
)
item.artists_credit = (
track_info.artists_credit or album_info.artists_credit
)
item.albumartist_sort = album_info.artist_sort
item.albumartists_sort = album_info.artists_sort
item.albumartist_credit = album_info.artist_credit
item.albumartists_credit = album_info.artists_credit
# Release date.
for prefix in "", "original_":
if config["original_date"] and not prefix:
# Ignore specific release date.
continue
for suffix in "year", "month", "day":
key = f"{prefix}{suffix}"
value = getattr(album_info, key) or 0
# If we don't even have a year, apply nothing.
if suffix == "year" and not value:
break
# Otherwise, set the fetched value (or 0 for the month
# and day if not available).
item[key] = value
# If we're using original release date for both fields,
# also set item.year = info.original_year, etc.
if config["original_date"]:
item[suffix] = value
# Title.
item.title = track_info.title
if config["per_disc_numbering"]:
# We want to let the track number be zero, but if the medium index
# is not provided we need to fall back to the overall index.
if track_info.medium_index is not None:
item.track = track_info.medium_index
else:
item.track = track_info.index
item.tracktotal = track_info.medium_total or len(album_info.tracks)
else:
item.track = track_info.index
item.tracktotal = len(album_info.tracks)
# Disc and disc count.
item.disc = track_info.medium
item.disctotal = album_info.mediums
# MusicBrainz IDs.
item.mb_trackid = track_info.track_id
item.mb_releasetrackid = track_info.release_track_id
item.mb_albumid = album_info.album_id
if track_info.artist_id:
item.mb_artistid = track_info.artist_id
else:
item.mb_artistid = album_info.artist_id
if track_info.artists_ids:
item.mb_artistids = track_info.artists_ids
else:
item.mb_artistids = album_info.artists_ids
item.mb_albumartistid = album_info.artist_id
item.mb_albumartistids = album_info.artists_ids
item.mb_releasegroupid = album_info.releasegroup_id
# Compilation flag.
item.comp = album_info.va
# Track alt.
item.track_alt = track_info.track_alt
_apply_metadata(
album_info,
item,
nullable_fields=config["overwrite_null"]["album"].as_str_seq(),
)
_apply_metadata(
track_info,
item,
nullable_fields=config["overwrite_null"]["track"].as_str_seq(),
)
correct_list_fields(item)
================================================
FILE: beets/autotag/distance.py
================================================
from __future__ import annotations
import datetime
import re
from functools import cache, total_ordering
from typing import TYPE_CHECKING, Any
from jellyfish import levenshtein_distance
from unidecode import unidecode
from beets import config, metadata_plugins
from beets.util import as_string, cached_classproperty, get_most_common_tags
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from beets.library import Item
from .hooks import AlbumInfo, TrackInfo
# Candidate distance scoring.
# Artist signals that indicate "various artists". These are used at the
# album level to determine whether a given release is likely a VA
# release and also on the track level to to remove the penalty for
# differing artists.
VA_ARTISTS = ("", "various artists", "various", "va", "unknown")
# Parameters for string distance function.
# Words that can be moved to the end of a string using a comma.
SD_END_WORDS = ["the", "a", "an"]
# Reduced weights for certain portions of the string.
SD_PATTERNS = [
(r"^the ", 0.1),
(r"[\[\(]?(ep|single)[\]\)]?", 0.0),
(r"[\[\(]?(featuring|feat|ft)[\. :].+", 0.1),
(r"\(.*?\)", 0.3),
(r"\[.*?\]", 0.3),
(r"(, )?(pt\.|part) .+", 0.2),
]
# Replacements to use before testing distance.
SD_REPLACE = [
(r"&", "and"),
]
def _string_dist_basic(str1: str, str2: str) -> float:
"""Basic edit distance between two strings, ignoring
non-alphanumeric characters and case. Comparisons are based on a
transliteration/lowering to ASCII characters. Normalized by string
length.
"""
assert isinstance(str1, str)
assert isinstance(str2, str)
str1 = as_string(unidecode(str1))
str2 = as_string(unidecode(str2))
str1 = re.sub(r"[^a-z0-9]", "", str1.lower())
str2 = re.sub(r"[^a-z0-9]", "", str2.lower())
if not str1 and not str2:
return 0.0
return levenshtein_distance(str1, str2) / float(max(len(str1), len(str2)))
def string_dist(str1: str | None, str2: str | None) -> float:
"""Gives an "intuitive" edit distance between two strings. This is
an edit distance, normalized by the string length, with a number of
tweaks that reflect intuition about text.
"""
if str1 is None and str2 is None:
return 0.0
if str1 is None or str2 is None:
return 1.0
str1 = str1.lower()
str2 = str2.lower()
# Don't penalize strings that move certain words to the end. For
# example, "the something" should be considered equal to
# "something, the".
for word in SD_END_WORDS:
if str1.endswith(f", {word}"):
str1 = f"{word} {str1[: -len(word) - 2]}"
if str2.endswith(f", {word}"):
str2 = f"{word} {str2[: -len(word) - 2]}"
# Perform a couple of basic normalizing substitutions.
for pat, repl in SD_REPLACE:
str1 = re.sub(pat, repl, str1)
str2 = re.sub(pat, repl, str2)
# Change the weight for certain string portions matched by a set
# of regular expressions. We gradually change the strings and build
# up penalties associated with parts of the string that were
# deleted.
base_dist = _string_dist_basic(str1, str2)
penalty = 0.0
for pat, weight in SD_PATTERNS:
# Get strings that drop the pattern.
case_str1 = re.sub(pat, "", str1)
case_str2 = re.sub(pat, "", str2)
if case_str1 != str1 or case_str2 != str2:
# If the pattern was present (i.e., it is deleted in the
# the current case), recalculate the distances for the
# modified strings.
case_dist = _string_dist_basic(case_str1, case_str2)
case_delta = max(0.0, base_dist - case_dist)
if case_delta == 0.0:
continue
# Shift our baseline strings down (to avoid rematching the
# same part of the string) and add a scaled distance
# amount to the penalties.
str1 = case_str1
str2 = case_str2
base_dist = case_dist
penalty += weight * case_delta
return base_dist + penalty
@total_ordering
class Distance:
"""Keeps track of multiple distance penalties. Provides a single
weighted distance for all penalties as well as a weighted distance
for each individual penalty.
"""
def __init__(self) -> None:
self._penalties: dict[str, list[float]] = {}
self.tracks: dict[TrackInfo, Distance] = {}
@cached_classproperty
def _weights(cls) -> dict[str, float]:
"""A dictionary from keys to floating-point weights."""
weights_view = config["match"]["distance_weights"]
weights = {}
for key in weights_view.keys():
weights[key] = weights_view[key].as_number()
return weights
# Access the components and their aggregates.
@property
def distance(self) -> float:
"""Return a weighted and normalized distance across all
penalties.
"""
dist_max = self.max_distance
if dist_max:
return self.raw_distance / self.max_distance
return 0.0
@property
def max_distance(self) -> float:
"""Return the maximum distance penalty (normalization factor)."""
dist_max = 0.0
for key, penalty in self._penalties.items():
dist_max += len(penalty) * self._weights[key]
return dist_max
@property
def raw_distance(self) -> float:
"""Return the raw (denormalized) distance."""
dist_raw = 0.0
for key, penalty in self._penalties.items():
dist_raw += sum(penalty) * self._weights[key]
return dist_raw
def items(self) -> list[tuple[str, float]]:
"""Return a list of (key, dist) pairs, with `dist` being the
weighted distance, sorted from highest to lowest. Does not
include penalties with a zero value.
"""
list_ = []
for key in self._penalties:
dist = self[key]
if dist:
list_.append((key, dist))
# Convert distance into a negative float we can sort items in
# ascending order (for keys, when the penalty is equal) and
# still get the items with the biggest distance first.
return sorted(
list_, key=lambda key_and_dist: (-key_and_dist[1], key_and_dist[0])
)
def __hash__(self) -> int:
return id(self)
def __eq__(self, other) -> bool:
return self.distance == other
# Behave like a float.
def __lt__(self, other) -> bool:
return self.distance < other
def __float__(self) -> float:
return self.distance
def __sub__(self, other) -> float:
return self.distance - other
def __rsub__(self, other) -> float:
return other - self.distance
def __str__(self) -> str:
return f"{self.distance:.2f}"
# Behave like a dict.
def __getitem__(self, key) -> float:
"""Returns the weighted distance for a named penalty."""
dist = sum(self._penalties[key]) * self._weights[key]
dist_max = self.max_distance
if dist_max:
return dist / dist_max
return 0.0
def __iter__(self) -> Iterator[tuple[str, float]]:
return iter(self.items())
def __len__(self) -> int:
return len(self.items())
def keys(self) -> list[str]:
return [key for key, _ in self.items()]
def update(self, dist: Distance):
"""Adds all the distance penalties from `dist`."""
if not isinstance(dist, Distance):
raise ValueError(
f"`dist` must be a Distance object, not {type(dist)}"
)
for key, penalties in dist._penalties.items():
self._penalties.setdefault(key, []).extend(penalties)
# Adding components.
def _eq(self, value1: re.Pattern[str] | Any, value2: Any) -> bool:
"""Returns True if `value1` is equal to `value2`. `value1` may
be a compiled regular expression, in which case it will be
matched against `value2`.
"""
if isinstance(value1, re.Pattern):
return bool(value1.match(value2))
return value1 == value2
def add(self, key: str, dist: float):
"""Adds a distance penalty. `key` must correspond with a
configured weight setting. `dist` must be a float between 0.0
and 1.0, and will be added to any existing distance penalties
for the same key.
"""
if not 0.0 <= dist <= 1.0:
raise ValueError(f"`dist` must be between 0.0 and 1.0, not {dist}")
self._penalties.setdefault(key, []).append(dist)
def add_equality(
self,
key: str,
value: Any,
options: list[Any] | tuple[Any, ...] | Any,
):
"""Adds a distance penalty of 1.0 if `value` doesn't match any
of the values in `options`. If an option is a compiled regular
expression, it will be considered equal if it matches against
`value`.
"""
if not isinstance(options, (list, tuple)):
options = [options]
for opt in options:
if self._eq(opt, value):
dist = 0.0
break
else:
dist = 1.0
self.add(key, dist)
def add_expr(self, key: str, expr: bool):
"""Adds a distance penalty of 1.0 if `expr` evaluates to True,
or 0.0.
"""
if expr:
self.add(key, 1.0)
else:
self.add(key, 0.0)
def add_number(self, key: str, number1: int, number2: int):
"""Adds a distance penalty of 1.0 for each number of difference
between `number1` and `number2`, or 0.0 when there is no
difference. Use this when there is no upper limit on the
difference between the two numbers.
"""
diff = abs(number1 - number2)
if diff:
for i in range(diff):
self.add(key, 1.0)
else:
self.add(key, 0.0)
def add_priority(
self,
key: str,
value: Any,
options: list[Any] | tuple[Any, ...] | Any,
):
"""Adds a distance penalty that corresponds to the position at
which `value` appears in `options`. A distance penalty of 0.0
for the first option, or 1.0 if there is no matching option. If
an option is a compiled regular expression, it will be
considered equal if it matches against `value`.
"""
if not isinstance(options, (list, tuple)):
options = [options]
unit = 1.0 / (len(options) or 1)
for i, opt in enumerate(options):
if self._eq(opt, value):
dist = i * unit
break
else:
dist = 1.0
self.add(key, dist)
def add_ratio(
self,
key: str,
number1: int | float,
number2: int | float,
):
"""Adds a distance penalty for `number1` as a ratio of `number2`.
`number1` is bound at 0 and `number2`.
"""
number = float(max(min(number1, number2), 0))
if number2:
dist = number / number2
else:
dist = 0.0
self.add(key, dist)
def add_string(self, key: str, str1: str | None, str2: str | None):
"""Adds a distance penalty based on the edit distance between
`str1` and `str2`.
"""
dist = string_dist(str1, str2)
self.add(key, dist)
def add_data_source(self, before: str | None, after: str | None) -> None:
if before != after and (
before or len(metadata_plugins.find_metadata_source_plugins()) > 1
):
self.add("data_source", metadata_plugins.get_penalty(after))
@cache
def get_track_length_grace() -> float:
"""Get cached grace period for track length matching."""
return config["match"]["track_length_grace"].as_number()
@cache
def get_track_length_max() -> float:
"""Get cached maximum track length for track length matching."""
return config["match"]["track_length_max"].as_number()
def track_index_changed(item: Item, track_info: TrackInfo) -> bool:
"""Returns True if the item and track info index is different. Tolerates
per disc and per release numbering.
"""
return item.track not in (track_info.medium_index, track_info.index)
def track_distance(
item: Item,
track_info: TrackInfo,
incl_artist: bool = False,
) -> Distance:
"""Determines the significance of a track metadata change. Returns a
Distance object. `incl_artist` indicates that a distance component should
be included for the track artist (i.e., for various-artist releases).
``track_length_grace`` and ``track_length_max`` configuration options are
cached because this function is called many times during the matching
process and their access comes with a performance overhead.
"""
dist = Distance()
# Length.
if info_length := track_info.length:
diff = abs(item.length - info_length) - get_track_length_grace()
dist.add_ratio("track_length", diff, get_track_length_max())
# Title.
dist.add_string("track_title", item.title, track_info.title)
# Artist. Only check if there is actually an artist in the track data.
if (
incl_artist
and track_info.artist
and item.artist.lower() not in VA_ARTISTS
):
dist.add_string("track_artist", item.artist, track_info.artist)
# Track index.
if track_info.index and item.track:
dist.add_expr("track_index", track_index_changed(item, track_info))
# Track ID.
if item.mb_trackid:
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)
dist.add_data_source(item.get("data_source"), track_info.data_source)
return dist
def distance(
items: Sequence[Item],
album_info: AlbumInfo,
item_info_pairs: list[tuple[Item, TrackInfo]],
) -> Distance:
"""Determines how "significant" an album metadata change would be.
Returns a Distance object. `album_info` is an AlbumInfo object
reflecting the album to be compared. `items` is a sequence of all
Item objects that will be matched (order is not important).
`mapping` is a dictionary mapping Items to TrackInfo objects; the
keys are a subset of `items` and the values are a subset of
`album_info.tracks`.
"""
likelies, _ = get_most_common_tags(items)
dist = Distance()
# Artist, if not various.
if not album_info.va:
dist.add_string("artist", likelies["artist"], album_info.artist)
# Album.
dist.add_string("album", likelies["album"], album_info.album)
preferred_config = config["match"]["preferred"]
# Current or preferred media.
if album_info.media:
# Preferred media options.
media_patterns: Sequence[str] = preferred_config["media"].as_str_seq()
options = [
re.compile(rf"(\d+x)?({pat})", re.I) for pat in media_patterns
]
if options:
dist.add_priority("media", album_info.media, options)
# Current media.
elif likelies["media"]:
dist.add_equality("media", album_info.media, likelies["media"])
# Mediums.
if likelies["disctotal"] and album_info.mediums:
dist.add_number("mediums", likelies["disctotal"], album_info.mediums)
# Prefer earliest release.
if album_info.year and preferred_config["original_year"]:
# Assume 1889 (earliest first gramophone discs) if we don't know the
# original year.
original = album_info.original_year or 1889
diff = abs(album_info.year - original)
diff_max = abs(datetime.date.today().year - original)
dist.add_ratio("year", diff, diff_max)
# Year.
elif likelies["year"] and album_info.year:
if likelies["year"] in (album_info.year, album_info.original_year):
# No penalty for matching release or original year.
dist.add("year", 0.0)
elif album_info.original_year:
# Prefer matchest closest to the release year.
diff = abs(likelies["year"] - album_info.year)
diff_max = abs(
datetime.date.today().year - album_info.original_year
)
dist.add_ratio("year", diff, diff_max)
else:
# Full penalty when there is no original year.
dist.add("year", 1.0)
# Preferred countries.
country_patterns: Sequence[str] = preferred_config["countries"].as_str_seq()
options = [re.compile(pat, re.I) for pat in country_patterns]
if album_info.country and options:
dist.add_priority("country", album_info.country, options)
# Country.
elif likelies["country"] and album_info.country:
dist.add_string("country", likelies["country"], album_info.country)
# Label.
if likelies["label"] and album_info.label:
dist.add_string("label", likelies["label"], album_info.label)
# Catalog number.
if likelies["catalognum"] and album_info.catalognum:
dist.add_string(
"catalognum", likelies["catalognum"], album_info.catalognum
)
# Disambiguation.
if likelies["albumdisambig"] and album_info.albumdisambig:
dist.add_string(
"albumdisambig", likelies["albumdisambig"], album_info.albumdisambig
)
# Album ID.
if likelies["mb_albumid"]:
dist.add_equality(
"album_id", likelies["mb_albumid"], album_info.album_id
)
# Tracks.
dist.tracks = {}
for item, track in item_info_pairs:
dist.tracks[track] = track_distance(item, track, album_info.va)
dist.add("tracks", dist.tracks[track].distance)
# Missing tracks.
for _ in range(len(album_info.tracks) - len(item_info_pairs)):
dist.add("missing_tracks", 1.0)
# Unmatched tracks.
for _ in range(len(items) - len(item_info_pairs)):
dist.add("unmatched_tracks", 1.0)
dist.add_data_source(likelies["data_source"], album_info.data_source)
return dist
================================================
FILE: beets/autotag/hooks.py
================================================
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# 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.
"""Glue between metadata sources and the matching logic."""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING, Any, TypeVar
from typing_extensions import Self
from beets import plugins
from beets.util import cached_classproperty
from beets.util.deprecation import deprecate_for_maintainers
if TYPE_CHECKING:
from beets.library import Item
from .distance import Distance
V = TypeVar("V")
# Classes used to represent candidate options.
class AttrDict(dict[str, V]):
"""Mapping enabling attribute-style access to stored metadata values."""
def copy(self) -> Self:
return deepcopy(self)
def __getattr__(self, attr: str) -> V:
if attr in self:
return self[attr]
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{attr}'"
)
def __setattr__(self, key: str, value: V) -> None:
self.__setitem__(key, value)
def __hash__(self) -> int: # type: ignore[override]
return id(self)
class Info(AttrDict[Any]):
"""Container for metadata about a musical entity."""
Identifier = tuple[str | None, str | None]
@property
def id(self) -> str | None:
"""Return the provider-specific identifier for this metadata object."""
raise NotImplementedError
@property
def identifier(self) -> Identifier:
"""Return a cross-provider key in ``(data_source, id)`` form."""
return (self.data_source, self.id)
@cached_property
def name(self) -> str:
raise NotImplementedError
def __init__(
self,
album: str | None = None,
artist_credit: str | None = None,
artist_id: str | None = None,
artist: str | None = None,
artists_credit: list[str] | None = None,
artists_ids: list[str] | None = None,
artists: list[str] | None = None,
artist_sort: str | None = None,
artists_sort: list[str] | None = None,
data_source: str | None = None,
data_url: str | None = None,
genre: str | None = None,
genres: list[str] | None = None,
media: str | None = None,
**kwargs,
) -> None:
if genre is not None:
deprecate_for_maintainers(
"The 'genre' parameter", "'genres' (list)", stacklevel=3
)
if not genres:
try:
sep = next(s for s in ["; ", ", ", " / "] if s in genre)
except StopIteration:
genres = [genre]
else:
genres = list(map(str.strip, genre.split(sep)))
self.album = album
self.artist = artist
self.artist_credit = artist_credit
self.artist_id = artist_id
self.artists = artists
self.artists_credit = artists_credit
self.artists_ids = artists_ids
self.artist_sort = artist_sort
self.artists_sort = artists_sort
self.data_source = data_source
self.data_url = data_url
self.genre = None
self.genres = genres
self.media = media
self.update(kwargs)
class AlbumInfo(Info):
"""Metadata snapshot representing a single album candidate.
Aggregates track entries and album-wide context gathered from an external
provider. Used during matching to evaluate similarity against a group of
user items, and later to drive tagging decisions once selected.
"""
@property
def id(self) -> str | None:
return self.album_id
@cached_property
def name(self) -> str:
return self.album or ""
def __init__(
self,
tracks: list[TrackInfo],
*,
album_id: str | None = None,
albumdisambig: str | None = None,
albumstatus: str | None = None,
albumtype: str | None = None,
albumtypes: list[str] | None = None,
asin: str | None = None,
barcode: str | None = None,
catalognum: str | None = None,
country: str | None = None,
day: int | None = None,
discogs_albumid: str | None = None,
discogs_artistid: str | None = None,
discogs_labelid: str | None = None,
label: str | None = None,
language: str | None = None,
mediums: int | None = None,
month: int | None = None,
original_day: int | None = None,
original_month: int | None = None,
original_year: int | None = None,
release_group_title: str | None = None,
releasegroup_id: str | None = None,
releasegroupdisambig: str | None = None,
script: str | None = None,
style: str | None = None,
va: bool = False,
year: int | None = None,
**kwargs,
) -> None:
self.tracks = tracks
self.album_id = album_id
self.albumdisambig = albumdisambig
self.albumstatus = albumstatus
self.albumtype = albumtype
self.albumtypes = albumtypes
self.asin = asin
self.barcode = barcode
self.catalognum = catalognum
self.country = country
self.day = day
self.discogs_albumid = discogs_albumid
self.discogs_artistid = discogs_artistid
self.discogs_labelid = discogs_labelid
self.label = label
self.language = language
self.mediums = mediums
self.month = month
self.original_day = original_day
self.original_month = original_month
self.original_year = original_year
self.release_group_title = release_group_title
self.releasegroup_id = releasegroup_id
self.releasegroupdisambig = releasegroupdisambig
self.script = script
self.style = style
self.va = va
self.year = year
super().__init__(**kwargs)
class TrackInfo(Info):
"""Metadata snapshot for a single track candidate.
Captures identifying details and creative credits used to compare against
a user's item. Instances often originate within an AlbumInfo but may also
stand alone for singleton matching.
"""
@property
def id(self) -> str | None:
return self.track_id
@cached_property
def name(self) -> str:
return self.title or ""
def __init__(
self,
*,
arranger: str | None = None,
bpm: str | None = None,
composer: str | None = None,
composer_sort: str | None = None,
disctitle: str | None = None,
index: int | None = None,
initial_key: str | None = None,
length: float | None = None,
lyricist: str | None = None,
mb_workid: str | None = None,
medium: int | None = None,
medium_index: int | None = None,
medium_total: int | None = None,
release_track_id: str | None = None,
title: str | None = None,
track_alt: str | None = None,
track_id: str | None = None,
work: str | None = None,
work_disambig: str | None = None,
**kwargs,
) -> None:
self.arranger = arranger
self.bpm = bpm
self.composer = composer
self.composer_sort = composer_sort
self.disctitle = disctitle
self.index = index
self.initial_key = initial_key
self.length = length
self.lyricist = lyricist
self.mb_workid = mb_workid
self.medium = medium
self.medium_index = medium_index
self.medium_total = medium_total
self.release_track_id = release_track_id
self.title = title
self.track_alt = track_alt
self.track_id = track_id
self.work = work
self.work_disambig = work_disambig
super().__init__(**kwargs)
# Structures that compose all the information for a candidate match.
@dataclass
class Match:
distance: Distance
info: Info
@cached_classproperty
def type(cls) -> str:
return cls.__name__.removesuffix("Match") # type: ignore[attr-defined]
@dataclass
class AlbumMatch(Match):
info: AlbumInfo
mapping: dict[Item, TrackInfo]
extra_items: list[Item]
extra_tracks: list[TrackInfo]
def __post_init__(self) -> None:
"""Notify listeners when an album candidate has been matched."""
plugins.send("album_matched", match=self)
@property
def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]:
return list(self.mapping.items())
@property
def items(self) -> list[Item]:
return [i for i, _ in self.item_info_pairs]
@dataclass
class TrackMatch(Match):
info: TrackInfo
================================================
FILE: beets/autotag/match.py
================================================
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# 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.
"""Matches existing metadata with canonical information to identify
releases and tracks.
"""
from __future__ import annotations
from enum import IntEnum
from typing import TYPE_CHECKING, NamedTuple, TypeVar
import lap
import numpy as np
from beets import config, logging, metadata_plugins
from beets.autotag import AlbumMatch, TrackMatch, hooks
from beets.util import get_most_common_tags
from .distance import VA_ARTISTS, distance, track_distance
from .hooks import Info
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from beets.autotag import AlbumInfo, TrackInfo
from beets.library import Item
AnyMatch = TypeVar("AnyMatch", TrackMatch, AlbumMatch)
Candidates = dict[Info.Identifier, AnyMatch]
# Global logger.
log = logging.getLogger("beets")
# Recommendation enumeration.
class Recommendation(IntEnum):
"""Indicates a qualitative suggestion to the user about what should
be done with a given match.
"""
none = 0
low = 1
medium = 2
strong = 3
# A structure for holding a set of possible matches to choose between. This
# consists of a list of possible candidates (i.e., AlbumInfo or TrackInfo
# objects) and a recommendation value.
class Proposal(NamedTuple):
candidates: Sequence[AlbumMatch | TrackMatch]
recommendation: Recommendation
# Primary matching functionality.
def assign_items(
items: Sequence[Item],
tracks: Sequence[TrackInfo],
) -> tuple[list[tuple[Item, TrackInfo]], list[Item], list[TrackInfo]]:
"""Given a list of Items and a list of TrackInfo objects, find the
best mapping between them. Returns a mapping from Items to TrackInfo
objects, a set of extra Items, and a set of extra TrackInfo
objects. These "extra" objects occur when there is an unequal number
of objects of the two types.
"""
log.debug("Computing track assignment...")
# Construct the cost matrix.
costs = [[float(track_distance(i, t)) for t in tracks] for i in items]
# Assign items to tracks
_, _, assigned_item_idxs = lap.lapjv(np.array(costs), extend_cost=True)
log.debug("...done.")
# Each item in `assigned_item_idxs` list corresponds to a track in the
# `tracks` list. Each value is either an index into the assigned item in
# `items` list, or -1 if that track has no match.
mapping = {
items[iidx]: t
for iidx, t in zip(assigned_item_idxs, tracks)
if iidx != -1
}
extra_items = list(set(items) - mapping.keys())
extra_items.sort(key=lambda i: (i.disc, i.track, i.title))
extra_tracks = list(set(tracks) - set(mapping.values()))
extra_tracks.sort(key=lambda t: (t.index, t.title))
return list(mapping.items()), extra_items, extra_tracks
def match_by_id(album_id: str | None, consensus: bool) -> Iterable[AlbumInfo]:
"""Return album candidates for the given album id.
Make sure that the ID is present and that there is consensus on it among
the items being tagged.
"""
if not album_id:
log.debug("No album ID found.")
elif not consensus:
log.debug("No album ID consensus.")
else:
log.debug("Searching for discovered album ID: {}", album_id)
return metadata_plugins.albums_for_ids([album_id])
return ()
def _recommendation(
results: Sequence[AlbumMatch | TrackMatch],
) -> Recommendation:
"""Given a sorted list of AlbumMatch or TrackMatch objects, return a
recommendation based on the results' distances.
If the recommendation is higher than the configured maximum for
an applied penalty, the recommendation will be downgraded to the
configured maximum for that penalty.
"""
if not results:
# No candidates: no recommendation.
return Recommendation.none
# Basic distance thresholding.
min_dist = results[0].distance
if min_dist < config["match"]["strong_rec_thresh"].as_number():
# Strong recommendation level.
rec = Recommendation.strong
elif min_dist <= config["match"]["medium_rec_thresh"].as_number():
# Medium recommendation level.
rec = Recommendation.medium
elif len(results) == 1:
# Only a single candidate.
rec = Recommendation.low
elif (
results[1].distance - min_dist
>= config["match"]["rec_gap_thresh"].as_number()
):
# Gap between first two candidates is large.
rec = Recommendation.low
else:
# No conclusion. Return immediately. Can't be downgraded any further.
return Recommendation.none
# Downgrade to the max rec if it is lower than the current rec for an
# applied penalty.
keys = set(min_dist.keys())
if isinstance(results[0], hooks.AlbumMatch):
for track_dist in min_dist.tracks.values():
keys.update(list(track_dist.keys()))
max_rec_view = config["match"]["max_rec"]
for key in keys:
if key in list(max_rec_view.keys()):
max_rec = max_rec_view[key].as_choice(
{
"strong": Recommendation.strong,
"medium": Recommendation.medium,
"low": Recommendation.low,
"none": Recommendation.none,
}
)
rec = min(rec, max_rec)
return rec
def _sort_candidates(candidates: Iterable[AnyMatch]) -> Sequence[AnyMatch]:
"""Sort candidates by distance."""
return sorted(candidates, key=lambda match: match.distance)
def _add_candidate(
items: Sequence[Item],
results: Candidates[AlbumMatch],
info: AlbumInfo,
):
"""Given a candidate AlbumInfo object, attempt to add the candidate
to the output dictionary of AlbumMatch objects. This involves
checking the track count, ordering the items, checking for
duplicates, and calculating the distance.
"""
log.debug(
"Candidate: {0.artist} - {0.album} ({0.album_id}) from {0.data_source}",
info,
)
# Discard albums with zero tracks.
if not info.tracks:
log.debug("No tracks.")
return
# Prevent duplicates.
if info.album_id and info.identifier in results:
log.debug("Duplicate.")
return
# Discard matches without required tags.
required_tags: Sequence[str] = config["match"]["required"].as_str_seq()
for req_tag in required_tags:
if getattr(info, req_tag) is None:
log.debug("Ignored. Missing required tag: {}", req_tag)
return
# Find mapping between the items and the track info.
item_info_pairs, extra_items, extra_tracks = assign_items(
items, info.tracks
)
# Get the change distance.
dist = distance(items, info, item_info_pairs)
# Skip matches with ignored penalties.
penalties = [key for key, _ in dist]
ignored_tags: Sequence[str] = config["match"]["ignored"].as_str_seq()
for penalty in ignored_tags:
if penalty in penalties:
log.debug("Ignored. Penalty: {}", penalty)
return
log.debug("Success. Distance: {}", dist)
results[info.identifier] = hooks.AlbumMatch(
dist, info, dict(item_info_pairs), extra_items, extra_tracks
)
def tag_album(
items,
search_artist: str | None = None,
search_name: str | None = None,
search_ids: list[str] = [],
) -> tuple[str, str, Proposal]:
"""Return a tuple of the current artist name, the current album
name, and a `Proposal` containing `AlbumMatch` candidates.
The artist and album are the most common values of these fields
among `items`.
The `AlbumMatch` objects are generated by searching the metadata
backends. By default, the metadata of the items is used for the
search. This can be customized by setting the parameters.
`search_ids` is a list of metadata backend IDs: if specified,
it will restrict the candidates to those IDs, ignoring
`search_artist` and `search album`. The `mapping` field of the
album has the matched `items` as keys.
The recommendation is calculated from the match quality of the
candidates.
"""
# Get current metadata.
likelies, consensus = get_most_common_tags(items)
cur_artist: str = likelies["artist"]
cur_album: str = likelies["album"]
log.debug("Tagging {} - {}", cur_artist, cur_album)
# The output result, keys are (data_source, album_id) pairs, values are
# AlbumMatch objects.
candidates: Candidates[AlbumMatch] = {}
# Search by explicit ID.
if search_ids:
log.debug("Searching for album IDs: {}", ", ".join(search_ids))
for _info in metadata_plugins.albums_for_ids(search_ids):
_add_candidate(items, candidates, _info)
# Use existing metadata or text search.
else:
# Try search based on current ID.
for info in match_by_id(
likelies["mb_albumid"], consensus["mb_albumid"]
):
_add_candidate(items, candidates, info)
rec = _recommendation(list(candidates.values()))
log.debug("Album ID match recommendation is {}", rec)
if candidates and not config["import"]["timid"]:
# If we have a very good MBID match, return immediately.
# Otherwise, this match will compete against metadata-based
# matches.
if rec == Recommendation.strong:
log.debug("ID match.")
return (
cur_artist,
cur_album,
Proposal(list(candidates.values()), rec),
)
# Search terms.
if not (search_artist and search_name):
# No explicit search terms -- use current metadata.
search_artist, search_name = cur_artist, cur_album
log.debug("Search terms: {} - {}", search_artist, search_name)
# Is this album likely to be a "various artist" release?
va_likely = (
(not consensus["artist"])
or (search_artist.lower() in VA_ARTISTS)
or any(item.comp for item in items)
)
log.debug("Album might be VA: {}", va_likely)
# Get the results from the data sources.
for matched_candidate in metadata_plugins.candidates(
items, search_artist, search_name, va_likely
):
_add_candidate(items, candidates, matched_candidate)
log.debug("Evaluating {} candidates.", len(candidates))
# Sort and get the recommendation.
candidates_sorted = _sort_candidates(candidates.values())
rec = _recommendation(candidates_sorted)
return cur_artist, cur_album, Proposal(candidates_sorted, rec)
def tag_item(
item,
search_artist: str | None = None,
search_name: str | None = None,
search_ids: list[str] | None = None,
) -> Proposal:
"""Find metadata for a single track. Return a `Proposal` consisting
of `TrackMatch` objects.
`search_artist` and `search_title` may be used to override the item
metadata in the search query. `search_ids` may be used for restricting the
search to a list of metadata backend IDs.
"""
# Holds candidates found so far: keys are (data_source, track_id) pairs,
# values TrackMatch objects
candidates: Candidates[TrackMatch] = {}
rec: Recommendation | None = None
# First, try matching by the external source ID.
trackids = search_ids or [t for t in [item.mb_trackid] if t]
if trackids:
log.debug("Searching for track IDs: {}", ", ".join(trackids))
for info in metadata_plugins.tracks_for_ids(trackids):
dist = track_distance(item, info, incl_artist=True)
candidates[info.identifier] = hooks.TrackMatch(dist, info)
# If this is a good match, then don't keep searching.
rec = _recommendation(_sort_candidates(candidates.values()))
if rec == Recommendation.strong and not config["import"]["timid"]:
log.debug("Track ID match.")
return Proposal(_sort_candidates(candidates.values()), rec)
# If we're searching by ID, don't proceed.
if search_ids:
if candidates:
assert rec is not None
return Proposal(_sort_candidates(candidates.values()), rec)
else:
return Proposal([], Recommendation.none)
# Search terms.
search_artist = search_artist or item.artist
search_name = search_name or item.title
log.debug("Item search terms: {} - {}", search_artist, search_name)
# Get and evaluate candidate metadata.
for track_info in metadata_plugins.item_candidates(
item, search_artist, search_name
):
dist = track_distance(item, track_info, incl_artist=True)
candidates[track_info.identifier] = hooks.TrackMatch(dist, track_info)
# Sort by distance and return with recommendation.
log.debug("Found {} candidates.", len(candidates))
candidates_sorted = _sort_candidates(candidates.values())
rec = _recommendation(candidates_sorted)
return Proposal(candidates_sorted, rec)
================================================
FILE: beets/config_default.yaml
================================================
# --------------- Main ---------------
library: library.db
directory: ~/Music
statefile: state.pickle
# --------------- Plugins ---------------
plugins: [musicbrainz]
pluginpath: []
raise_on_error: no
# --------------- Import ---------------
clutter: ["Thumbs.DB", ".DS_Store"]
ignore: [".*", "*~", "System Volume Information", "lost+found"]
ignore_hidden: yes
import:
# common options
write: yes
copy: yes
move: no
timid: no
quiet: no
log:
# other options
default_action: apply
languages: []
quiet_fallback: skip
none_rec_action: ask
# rare options
link: no
hardlink: no
reflink: no
delete: no
resume: ask
incremental: no
incremental_skip_later: no
from_scratch: no
autotag: yes
singletons: no
detail: no
flat: no
group_albums: no
pretend: false
search_ids: []
duplicate_keys:
album: albumartist album
item: artist title
duplicate_action: ask
duplicate_verbose_prompt: no
bell: no
set_fields: {}
ignored_alias_types: []
singleton_album_disambig: yes
# --------------- Paths ---------------
path_sep_replace: _
drive_sep_replace: _
asciify_paths: false
art_filename: cover
max_filename_length: 0
replace:
# Replace bad characters with _
# prohibited in many filesystem paths
'[<>:\?\*\|]': _
# double quotation mark "
'\"': _
# path separators: \ or /
'[\\/]': _
# starting and closing periods
'^\.': _
'\.$': _
# control characters
'[\x00-\x1f]': _
# dash at the start of a filename (causes command line ambiguity)
'^-': _
# Replace bad characters with nothing
# starting and closing whitespace
'\s+$': ''
'^\s+': ''
aunique:
keys: albumartist album
disambiguators: albumtype year label catalognum albumdisambig releasegroupdisambig
bracket: '[]'
sunique:
keys: artist title
disambiguators: year trackdisambig
bracket: '[]'
# --------------- Tagging ---------------
per_disc_numbering: no
original_date: no
artist_credit: no
id3v23: no
va_name: "Various Artists"
paths:
default: $albumartist/$album%aunique{}/$track $title
singleton: Non-Album/$artist/$title
comp: Compilations/$album%aunique{}/$track $title
# --------------- Performance ---------------
threaded: yes
timeout: 5.0
# --------------- UI ---------------
verbose: 0
terminal_encoding:
ui:
terminal_width: 80
length_diff_thresh: 10.0
color: yes
colors:
text_success: ['bold', 'green']
text_warning: ['bold', 'yellow']
text_error: ['bold', 'red']
text_highlight: ['bold', 'red']
text_highlight_minor: ['white']
action_default: ['bold', 'cyan']
action: ['bold', 'cyan']
# New Colors
text_faint: ['faint']
import_path: ['bold', 'blue']
import_path_items: ['bold', 'blue']
changed: ['yellow']
text_diff_added: ['bold', 'green']
text_diff_removed: ['bold', 'red']
action_description: ['white']
import:
indentation:
match_header: 2
match_details: 2
match_tracklist: 5
layout: column
# --------------- Search ---------------
format_item: $artist - $album - $title
format_album: $albumartist - $album
time_format: '%Y-%m-%d %H:%M:%S'
format_raw_length: no
sort_album: albumartist+ album+
sort_item: artist+ album+ disc+ track+
sort_case_insensitive: yes
# --------------- Autotagger ---------------
overwrite_null:
album: []
track: []
match:
strong_rec_thresh: 0.04
medium_rec_thresh: 0.25
rec_gap_thresh: 0.25
max_rec:
missing_tracks: medium
unmatched_tracks: medium
distance_weights:
data_source: 2.0
artist: 3.0
album: 3.0
media: 1.0
mediums: 1.0
year: 1.0
country: 0.5
label: 0.5
catalognum: 0.5
albumdisambig: 0.5
album_id: 5.0
tracks: 2.0
missing_tracks: 0.9
unmatched_tracks: 0.6
track_title: 3.0
track_artist: 2.0
track_index: 1.0
track_length: 2.0
track_id: 5.0
medium: 1.0
preferred:
countries: []
media: []
original_year: no
ignored: []
required: []
ignored_media: []
ignore_data_tracks: yes
ignore_video_tracks: yes
track_length_grace: 10
track_length_max: 30
album_disambig_fields: data_source media year country label catalognum albumdisambig
singleton_disambig_fields: data_source index track_alt album
================================================
FILE: beets/dbcore/__init__.py
================================================
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# 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.
"""DBCore is an abstract database package that forms the basis for beets'
Library.
"""
from .db import Database, Index, Model, Results
from .query import (
AndQuery,
FieldQuery,
InvalidQueryError,
MatchQuery,
OrQuery,
Query,
)
from .queryparse import (
parse_sorted_query,
query_from_strings,
sort_from_strings,
)
from .types import Type
__all__ = [
"AndQuery",
"Database",
"FieldQuery",
"Index",
"InvalidQueryError",
"MatchQuery",
"Model",
"OrQuery",
"Query",
"Results",
"Type",
"parse_sorted_query",
"query_from_strings",
"sort_from_strings",
]
================================================
FILE: beets/dbcore/db.py
================================================
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# 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.
"""The central Model and Database constructs for DBCore."""
from __future__ import annotations
import functools
import os
import re
import sqlite3
import sys
import threading
import time
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Mapping
from contextlib import contextmanager
from dataclasses import dataclass
from functools import cached_property
from sqlite3 import Connection, sqlite_version_info
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
ClassVar,
Generic,
Literal,
NamedTuple,
TypedDict,
)
from typing_extensions import (
Self,
TypeVar, # default value support
)
from unidecode import unidecode
import beets
from ..util import cached_classproperty, functemplate
from . import types
from .query import MatchQuery, NullSort, TrueQuery
if TYPE_CHECKING:
from collections.abc import (
Callable,
Generator,
Iterable,
Iterator,
Sequence,
)
from sqlite3 import Connection
from types import TracebackType
from .query import FieldQueryType, FieldSort, Query, Sort, SQLiteType
D = TypeVar("D", bound="Database", default=Any)
FlexAttrs = dict[str, str]
class DBAccessError(Exception):
"""The SQLite database became inaccessible.
This can happen when trying to read or write the database when, for
example, the database file is deleted or otherwise disappears. There
is probably no way to recover from this error.
"""
class DBCustomFunctionError(Exception):
"""A sqlite function registered by beets failed."""
def __init__(self):
super().__init__(
"beets defined SQLite function failed; "
"see the other errors above for details"
)
class NotFoundError(LookupError):
pass
class FormattedMapping(Mapping[str, str]):
"""A `dict`-like formatted view of a model.
The accessor `mapping[key]` returns the formatted version of
`model[key]` as a unicode string.
The `included_keys` parameter allows filtering the fields that are
returned. By default all fields are returned. Limiting to specific keys can
avoid expensive per-item database queries.
If `for_path` is true, all path separators in the formatted values
are replaced.
"""
model: Model
ALL_KEYS = "*"
def __init__(
self,
model: Model,
included_keys: str = ALL_KEYS,
for_path: bool = False,
):
self.for_path = for_path
self.model = model
if included_keys == self.ALL_KEYS:
# Performance note: this triggers a database query.
self.model_keys = self.model.keys(True)
else:
self.model_keys = included_keys
def __getitem__(self, key: str) -> str:
if key in self.model_keys:
return self._get_formatted(self.model, key)
else:
raise KeyError(key)
def __iter__(self) -> Iterator[str]:
return iter(self.model_keys)
def __len__(self) -> int:
return len(self.model_keys)
# The following signature is incompatible with `Mapping[str, str]`, since
# the return type doesn't include `None` (but `default` can be `None`).
def get( # type: ignore
self,
key: str,
default: str | None = None,
) -> str:
"""Similar to Mapping.get(key, default), but always formats to str."""
if default is None:
default = self.model._type(key).format(None)
return super().get(key, default)
def _get_formatted(self, model: Model, key: str) -> str:
value = model._type(key).format(model.get(key))
if isinstance(value, bytes):
value = value.decode("utf-8", "ignore")
if self.for_path:
sep_repl: str = beets.config["path_sep_replace"].as_str()
sep_drive: str = beets.config["drive_sep_replace"].as_str()
if re.match(r"^[a-zA-Z]:", value):
value = re.sub(r"(?<=[a-zA-Z]):", sep_drive, value)
for sep in (os.path.sep, os.path.altsep):
if sep:
value = value.replace(sep, sep_repl)
return value
# NOTE: This seems like it should be a `Mapping`, i.e.
# ```
# class LazyConvertDict(Mapping[str, Any])
# ```
# but there are some conflicts with the `Mapping` protocol such that we
# can't do this without changing behaviour: In particular, iterators returned
# by some methods build intermediate lists, such that modification of the
# `LazyConvertDict` becomes safe during iteration. Some code does in fact rely
# on this.
class LazyConvertDict:
"""Lazily convert types for attributes fetched from the database"""
def __init__(self, model_cls: Model):
"""Initialize the object empty"""
# FIXME: Dict[str, SQLiteType]
self._data: dict[str, Any] = {}
self.model_cls = model_cls
self._converted: dict[str, Any] = {}
def init(self, data: dict[str, Any]):
"""Set the base data that should be lazily converted"""
self._data = data
def _convert(self, key: str, value: Any):
"""Convert the attribute type according to the SQL type"""
return self.model_cls._type(key).from_sql(value)
def __setitem__(self, key: str, value: Any):
"""Set an attribute value, assume it's already converted"""
self._converted[key] = value
def __getitem__(self, key: str) -> Any:
"""Get an attribute value, converting the type on demand
if needed
"""
if key in self._converted:
return self._converted[key]
elif key in self._data:
value = self._convert(key, self._data[key])
self._converted[key] = value
return value
def __delitem__(self, key: str):
"""Delete both converted and base data"""
if key in self._converted:
del self._converted[key]
if key in self._data:
del self._data[key]
def keys(self) -> list[str]:
"""Get a list of available field names for this object."""
return list(self._converted.keys()) + list(self._data.keys())
def copy(self) -> LazyConvertDict:
"""Create a copy of the object."""
new = self.__class__(self.model_cls)
new._data = self._data.copy()
new._converted = self._converted.copy()
return new
# Act like a dictionary.
def update(self, values: Mapping[str, Any]):
"""Assign all values in the given dict."""
for key, value in values.items():
self[key] = value
def items(self) -> Iterable[tuple[str, Any]]:
"""Iterate over (key, value) pairs that this object contains.
Computed fields are not included.
"""
for key in self:
yield key, self[key]
def get(self, key: str, default: Any | None = None):
"""Get the value for a given key or `default` if it does not
exist.
"""
if key in self:
return self[key]
else:
return default
def __contains__(self, key: Any) -> bool:
"""Determine whether `key` is an attribute on this object."""
return key in self._converted or key in self._data
def __iter__(self) -> Iterator[str]:
"""Iterate over the available field names (excluding computed
fields).
"""
# NOTE: It would be nice to use the following:
# yield from self._converted
# yield from self._data
# but that won't work since some code relies on modifying `self`
# during iteration.
return iter(self.keys())
def __len__(self) -> int:
# FIXME: This is incorrect due to duplication of keys
return len(self._converted) + len(self._data)
# Abstract base for model classes.
class Model(ABC, Generic[D]):
"""An abstract object representing an object in the database. Model
objects act like dictionaries (i.e., they allow subscript access like
``obj['field']``). The same field set is available via attribute
access as a shortcut (i.e., ``obj.field``). Three kinds of attributes are
available:
* **Fixed attributes** come from a predetermined list of field
names. These fields correspond to SQLite table columns and are
thus fast to read, write, and query.
* **Flexible attributes** are free-form and do not need to be listed
ahead of time.
* **Computed attributes** are read-only fields computed by a getter
function provided by a plugin.
Access to all three field types is uniform: ``obj.field`` works the
same regardless of whether ``field`` is fixed, flexible, or
computed.
Model objects can optionally be associated with a `Library` object,
in which case they can be loaded and stored from the database. Dirty
flags are used to track which fields need to be stored.
"""
# Abstract components (to be provided by subclasses).
_table: str
"""The main SQLite table name.
"""
_flex_table: str
"""The flex field SQLite table name.
"""
_fields: ClassVar[dict[str, types.Type]] = {}
"""A mapping indicating available "fixed" fields on this type. The
keys are field names and the values are `Type` objects.
"""
_search_fields: Sequence[str] = ()
"""The fields that should be queried by default by unqualified query
terms.
"""
_indices: Sequence[Index] = ()
"""A sequence of `Index` objects that describe the indices to be
created for this table.
"""
@cached_classproperty
def _types(cls) -> dict[str, types.Type]:
"""Optional types for non-fixed (flexible and computed) fields."""
return {}
_sorts: ClassVar[dict[str, type[FieldSort]]] = {}
"""Optional named sort criteria. The keys are strings and the values
are subclasses of `Sort`.
"""
@cached_classproperty
def _queries(cls) -> dict[str, FieldQueryType]:
"""Named queries that use a field-like `name:value` syntax but which
do not relate to any specific field.
"""
return {}
_always_dirty = False
"""By default, fields only become "dirty" when their value actually
changes. Enabling this flag marks fields as dirty even when the new
value is the same as the old value (e.g., `o.f = o.f`).
"""
_revision = -1
"""A revision number from when the model was loaded from or written
to the database.
"""
@cached_classproperty
def _relation(cls):
"""The model that this model is closely related to."""
return cls
@cached_classproperty
def relation_join(cls) -> str:
"""Return the join required to include the related table in the query.
This is intended to be used as a FROM clause in the SQL query.
"""
return ""
@cached_classproperty
def all_db_fields(cls) -> set[str]:
return cls._fields.keys() | cls._relation._fields.keys()
@cached_classproperty
def shared_db_fields(cls) -> set[str]:
return cls._fields.keys() & cls._relation._fields.keys()
@cached_classproperty
def other_db_fields(cls) -> set[str]:
"""Fields in the related table."""
return cls._relation._fields.keys() - cls.shared_db_fields
@cached_property
def db(self) -> D:
"""Get the database associated with this object.
This validates that the database is attached and the object has an id.
"""
return self._check_db()
def get_fresh_from_db(self) -> Self:
"""Load this object from the database."""
model_cls = self.__class__
if obj := self.db._get(model_cls, self.id):
return obj
raise NotFoundError(f"No matching {model_cls.__name__} found") from None
@classmethod
def _getters(cls: type[Model]):
"""Return a mapping from field names to getter functions."""
# We could cache this if it becomes a performance problem to
# gather the getter mapping every time.
raise NotImplementedError()
def _template_funcs(self) -> Mapping[str, Callable[[str], str]]:
"""Return a mapping from function names to text-transformer
functions.
"""
# As above: we could consider caching this result.
raise NotImplementedError()
# Basic operation.
def __init__(self, db: D | None = None, **values):
"""Create a new object with an optional Database association and
initial field values.
"""
self._db = db
self._dirty: set[str] = set()
self._values_fixed = LazyConvertDict(self)
self._values_flex = LazyConvertDict(self)
# Initial contents.
self.update(values)
self.clear_dirty()
@classmethod
def _awaken(
cls: type[AnyModel],
db: D | None = None,
fixed_values: dict[str, Any] = {},
flex_values: dict[str, Any] = {},
) -> AnyModel:
"""Create an object with values drawn from the database.
This is a performance optimization: the checks involved with
ordinary construction are bypassed.
"""
obj = cls(db)
obj._values_fixed.init(fixed_values)
obj._values_flex.init(flex_values)
return obj
def __repr__(self) -> str:
return (
f"{type(self).__name__}"
f"({', '.join(f'{k}={v!r}' for k, v in dict(self).items())})"
)
def clear_dirty(self):
"""Mark all fields as *clean* (i.e., not needing to be stored to
the database). Also update the revision.
"""
self._dirty = set()
if self._db:
self._revision = self._db.revision
def _check_db(self, need_id: bool = True) -> D:
"""Ensure that this object is associated with a database row: it
has a reference to a database (`_db`) and an id. A ValueError
exception is raised otherwise.
"""
if not self._db:
raise ValueError(f"{type(self).__name__} has no database")
if need_id and not self.id:
raise ValueError(f"{type(self).__name__} has no id")
return self._db
def copy(self) -> Model:
"""Create a copy of the model object.
The field values and other state is duplicated, but the new copy
remains associated with the same database as the old object.
(A simple `copy.deepcopy` will not work because it would try to
duplicate the SQLite connection.)
"""
new = self.__class__()
new._db = self._db
new._values_fixed = self._values_fixed.copy()
new._values_flex = self._values_flex.copy()
new._dirty = self._dirty.copy()
return new
# Essential field accessors.
@classmethod
def _type(cls, key) -> types.Type:
"""Get the type of a field, a `Type` instance.
If the field has no explicit type, it is given the base `Type`,
which does no conversion.
"""
return cls._fields.get(key) or cls._types.get(key) or types.DEFAULT
def _get(self, key, default: Any = None, raise_: bool = False):
"""Get the value for a field, or `default`. Alternatively,
raise a KeyError if the field is not available.
"""
getters = self._getters()
if key in getters: # Computed.
return getters[key](self)
elif key in self._fields: # Fixed.
if key in self._values_fixed:
return self._values_fixed[key]
else:
return self._type(key).null
elif key in self._values_flex: # Flexible.
return self._values_flex[key]
elif raise_:
raise KeyError(key)
else:
return default
get = _get
def __getitem__(self, key):
"""Get the value for a field. Raise a KeyError if the field is
not available.
"""
return self._get(key, raise_=True)
def _setitem(self, key, value):
"""Assign the value for a field, return whether new and old value
differ.
"""
# Choose where to place the value.
if key in self._fields:
source = self._values_fixed
else:
source = self._values_flex
# If the field has a type, filter the value.
value = self._type(key).normalize(value)
# Assign value and possibly mark as dirty.
old_value = source.get(key)
source[key] = value
changed = old_value != value
if self._always_dirty or changed:
self._dirty.add(key)
return changed
def __setitem__(self, key, value):
"""Assign the value for a field."""
self._setitem(key, value)
def __delitem__(self, key):
"""Remove a flexible attribute from the model."""
if key in self._values_flex: # Flexible.
del self._values_flex[key]
self._dirty.add(key) # Mark for dropping on store.
elif key in self._fields: # Fixed
setattr(self, key, self._type(key).null)
elif key in self._getters(): # Computed.
raise KeyError(f"computed field {key} cannot be deleted")
else:
raise KeyError(f"no such field {key}")
def keys(self, computed: bool = False):
"""Get a list of available field names for this object. The
`computed` parameter controls whether computed (plugin-provided)
fields are included in the key list.
"""
base_keys = list(self._fields) + list(self._values_flex.keys())
if computed:
return base_keys + list(self._getters().keys())
else:
return base_keys
@classmethod
def all_keys(cls):
"""Get a list of available keys for objects of this type.
Includes fixed and computed fields.
"""
return list(cls._fields) + list(cls._getters().keys())
# Act like a dictionary.
def update(self, values):
"""Assign all values in the given dict."""
for key, value in values.items():
self[key] = value
def items(self) -> Iterator[tuple[str, Any]]:
"""Iterate over (key, value) pairs that this object contains.
Computed fields are not included.
"""
for key in self:
yield key, self[key]
def __contains__(self, key) -> bool:
"""Determine whether `key` is an attribute on this object."""
return key in self.keys(computed=True)
def __iter__(self) -> Iterator[str]:
"""Iterate over the available field names (excluding computed
fields).
"""
return iter(self.keys())
# Convenient attribute access.
def __getattr__(self, key):
if key.startswith("_"):
raise AttributeError(f"model has no attribute {key!r}")
else:
try:
return self[key]
except KeyError:
raise AttributeError(f"no such field {key!r}")
def __setattr__(self, key, value):
if key.startswith("_"):
super().__setattr__(key, value)
else:
self[key] = value
def __delattr__(self, key):
if key.startswith("_"):
super().__delattr__(key)
else:
del self[key]
# Database interaction (CRUD methods).
def store(self, fields: Iterable[str] | None = None):
"""Save the object's metadata into the library database.
:param fields: the fields to be stored. If not specified, all fields
will be.
"""
if fields is None:
fields = self._fields
# Build assignments for query.
assignments = []
subvars: list[SQLiteType] = []
for key in fields:
if key != "id" and key in self._dirty:
self._dirty.remove(key)
assignments.append(f"{key}=?")
value = self._type(key).to_sql(self[key])
subvars.append(value)
with self.db.transaction() as tx:
# Main table update.
if assignments:
query = f"UPDATE {self._table} SET {','.join(assignments)} WHERE id=?"
subvars.append(self.id)
tx.mutate(query, subvars)
# Modified/added flexible attributes.
for key, value in self._values_flex.items():
if key in self._dirty:
self._dirty.remove(key)
value = self._type(key).to_sql(value)
tx.mutate(
f"INSERT INTO {self._flex_table} "
"(entity_id, key, value) "
"VALUES (?, ?, ?);",
(self.id, key, value),
)
# Deleted flexible attributes.
for key in self._dirty:
tx.mutate(
f"DELETE FROM {self._flex_table} WHERE entity_id=? AND key=?",
(self.id, key),
)
self.clear_dirty()
def load(self):
"""Refresh the object's metadata from the library database.
If check_revision is true, the database is only queried loaded when a
transaction has been committed since the item was last loaded.
"""
if not self._dirty and self.db.revision == self._revision:
# Exit early
return
self.__dict__.update(self.get_fresh_from_db().__dict__)
self.clear_dirty()
def remove(self):
"""Remove the object's associated rows from the database."""
with self.db.transaction() as tx:
tx.mutate(f"DELETE FROM {self._table} WHERE id=?", (self.id,))
tx.mutate(
f"DELETE FROM {self._flex_table} WHERE entity_id=?", (self.id,)
)
def add(self, db: D | None = None):
"""Add the object to the library database. This object must be
associated with a database; you can provide one via the `db`
parameter or use the currently associated database.
The object's `id` and `added` fields are set along with any
current field values.
"""
if db:
self._db = db
db = self._check_db(need_id=False)
with db.transaction() as tx:
new_id = tx.mutate(f"INSERT INTO {self._table} DEFAULT VALUES")
self.id = new_id
self.added = time.time()
# Mark every non-null field as dirty and store.
for key in self:
if self[key] is not None:
self._dirty.add(key)
self.store()
# Formatting and templating.
_formatter = FormattedMapping
def formatted(
self,
included_keys: str = _formatter.ALL_KEYS,
for_path: bool = False,
) -> FormattedMapping:
"""Get a mapping containing all values on this object formatted
as human-readable unicode strings.
"""
return self._formatter(self, included_keys, for_path)
def evaluate_template(
self,
template: str | functemplate.Template,
for_path: bool = False,
) -> str:
"""Evaluate a template (a string or a `Template` object) using
the object's fields. If `for_path` is true, then no new path
separators will be added to the template.
"""
# Perform substitution.
if isinstance(template, str):
t = functemplate.template(template)
else:
# Help out mypy
t = template
return t.substitute(
self.formatted(for_path=for_path), self._template_funcs()
)
# Parsing.
@classmethod
def _parse(cls, key, string: str) -> Any:
"""Parse a string as a value for the given key."""
if not isinstance(string, str):
raise TypeError("_parse() argument must be a string")
return cls._type(key).parse(string)
def set_parse(self, key, string: str):
"""Set the object's key to a value represented by a string."""
self[key] = self._parse(key, string)
def __getstate__(self):
"""Return the state of the object for pickling.
Remove the database connection as sqlite connections are not
picklable.
"""
return {
k: v for k, v in self.__dict__.items() if k not in {"_db", "db"}
}
# Database controller and supporting interfaces.
AnyModel = TypeVar("AnyModel", bound=Model)
class Results(Generic[AnyModel]):
"""An item query result set. Iterating over the collection lazily
constructs Model objects that reflect database rows.
"""
def __init__(
self,
model_class: type[AnyModel],
rows: list[sqlite3.Row],
db: D,
flex_rows,
query: Query | None = None,
sort=None,
):
"""Create a result set that will construct objects of type
`model_class`.
`model_class` is a subclass of `Model` that will be
constructed. `rows` is a query result: a list of mappings. The
new objects will be associated with the database `db`.
If `query` is provided, it is used as a predicate to filter the
results for a "slow query" that cannot be evaluated by the
database directly. If `sort` is provided, it is used to sort the
full list of results before returning. This means it is a "slow
sort" and all objects must be built before returning the first
one.
"""
self.model_class = model_class
self.rows = rows
self.db = db
self.query = query
self.sort = sort
self.flex_rows = flex_rows
# We keep a queue of rows we haven't yet consumed for
# materialization. We preserve the original total number of
# rows.
self._rows = rows
self._row_count = len(rows)
# The materialized objects corresponding to rows that have been
# consumed.
self._objects: list[AnyModel] = []
def _get_objects(self) -> Iterator[AnyModel]:
"""Construct and generate Model objects for they query. The
objects are returned in the order emitted from the database; no
slow sort is applied.
For performance, this generator caches materialized objects to
avoid constructing them more than once. This way, iterating over
a `Results` object a second time should be much faster than the
first.
"""
# Index flexible attributes by the item ID, so we have easier access
flex_attrs = self._get_indexed_flex_attrs()
index = 0 # Position in the materialized objects.
while index < len(self._objects) or self._rows:
# Are there previously-materialized objects to produce?
if index < len(self._objects):
yield self._objects[index]
index += 1
# Otherwise, we consume another row, materialize its object
# and produce it.
else:
while self._rows:
row = self._rows.pop(0)
obj = self._make_model(row, flex_attrs.get(row["id"], {}))
# If there is a slow-query predicate, ensurer that the
# object passes it.
if not self.query or self.query.match(obj):
self._objects.append(obj)
index += 1
yield obj
break
def __iter__(self) -> Iterator[AnyModel]:
"""Construct and generate Model objects for all matching
objects, in sorted order.
"""
if self.sort:
# Slow sort. Must build the full list first.
objects = self.sort.sort(list(self._get_objects()))
return iter(objects)
else:
# Objects are pre-sorted (i.e., by the database).
return self._get_objects()
def _get_indexed_flex_attrs(self) -> dict[int, FlexAttrs]:
"""Index flexible attributes by the entity id they belong to"""
flex_values: dict[int, FlexAttrs] = {}
for row in self.flex_rows:
if row["entity_id"] not in flex_values:
flex_values[row["entity_id"]] = {}
flex_values[row["entity_id"]][row["key"]] = row["value"]
return flex_values
def _make_model(
self, row: sqlite3.Row, flex_values: FlexAttrs = {}
) -> AnyModel:
"""Create a Model object for the given row"""
cols = dict(row)
values = {k: v for (k, v) in cols.items() if not k[:4] == "flex"}
# Construct the Python object
obj = self.model_class._awaken(self.db, values, flex_values)
return obj
def __len__(self) -> int:
"""Get the number of matching objects."""
if not self._rows:
# Fully materialized. Just count the objects.
return len(self._objects)
elif self.query:
# A slow query. Fall back to testing every object.
count = 0
for obj in self:
count += 1
return count
else:
# A fast query. Just count the rows.
return self._row_count
def __nonzero__(self) -> bool:
"""Does this result contain any objects?"""
return self.__bool__()
def __bool__(self) -> bool:
"""Does this result contain any objects?"""
return bool(len(self))
def __getitem__(self, n):
"""Get the nth item in this result set. This is inefficient: all
items up to n are materialized and thrown away.
"""
if not self._rows and not self.sort:
# Fully materialized and already in order. Just look up the
# object.
return self._objects[n]
it = iter(self)
try:
for i in range(n):
next(it)
return next(it)
except StopIteration:
raise IndexError(f"result index {n} out of range")
def get(self) -> AnyModel | None:
"""Return the first matching object, or None if no objects
match.
"""
it = iter(self)
try:
return next(it)
except StopIteration:
return None
class Transaction:
"""A context manager for safe, concurrent access to the database.
All SQL commands should be executed through a transaction.
"""
_mutated = False
"""A flag storing whether a mutation has been executed in the
current transaction.
"""
def __init__(self, db: Database):
self.db = db
def __enter__(self) -> Transaction:
"""Begin a transaction. This transaction may be created while
another is active in a different thread.
"""
with self.db._tx_stack() as stack:
first = not stack
stack.append(self)
if first:
# Beginning a "root" transaction, which corresponds to an
# SQLite transaction.
self.db._db_lock.acquire()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> bool | None:
"""Complete a transaction. This must be the most recently
entered but not yet exited transaction. If it is the last active
transaction, the database updates are committed.
"""
# Beware of races; currently secured by db._db_lock
self.db.revision += self._mutated
with self.db._tx_stack() as stack:
assert stack.pop() is self
empty = not stack
if empty:
# Ending a "root" transaction. End the SQLite transaction.
self.db._connection().commit()
self._mutated = False
self.db._db_lock.release()
if (
isinstance(exc_value, sqlite3.OperationalError)
and exc_value.args[0] == "user-defined function raised exception"
):
raise DBCustomFunctionError()
return None
def query(
self, statement: str, subvals: Sequence[SQLiteType] = ()
) -> list[sqlite3.Row]:
"""Execute an SQL statement with substitution values and return
a list of rows from the database.
"""
cursor = self.db._connection().execute(statement, subvals)
return cursor.fetchall()
@contextmanager
def _handle_mutate(self) -> Iterator[None]:
"""Handle mutation bookkeeping and database access errors.
Yield control to mutation execution code. If execution succeeds,
mark this transaction as mutated.
"""
try:
yield
except sqlite3.OperationalError as e:
# In two specific cases, SQLite reports an error while accessing
# the underlying database file. We surface these exceptions as
# DBAccessError so the application can abort.
if e.args[0] in (
"attempt to write a readonly database",
"unable to open database file",
):
raise DBAccessError(e.args[0])
raise
else:
self._mutated = True
def mutate(self, statement: str, subvals: Sequence[SQLiteType] = ()) -> Any:
"""Run one write statement with shared mutation/error handling."""
with self._handle_mutate():
return self.db._connection().execute(statement, subvals).lastrowid
def mutate_many(
self, statement: str, subvals: Sequence[tuple[SQLiteType, ...]] = ()
) -> Any:
"""Run batched writes with shared mutation/error handling."""
with self._handle_mutate():
return (
self.db._connection().executemany(statement, subvals).lastrowid
)
def script(self, statements: str):
"""Execute a string containing multiple SQL statements."""
# We don't know whether this mutates, but quite likely it does.
self._mutated = True
self.db._connection().executescript(statements)
@dataclass
class Migration(ABC):
"""Define a one-time data migration that runs during database startup."""
db: Database
@cached_classproperty
def name(cls) -> str:
"""Class name (except Migration) converted to snake case."""
name = cls.__name__.removesuffix("Migration") # type: ignore[attr-defined]
return re.sub(r"(?<=[a-z])(?=[A-Z])", "_", name).lower()
@contextmanager
def with_row_factory(self, factory: type[NamedTuple]) -> Iterator[None]:
"""Temporarily decode query rows into a typed tuple shape."""
original_factory = self.db._connection().row_factory
self.db._connection().row_factory = lambda _, row: factory(*row)
try:
yield
finally:
self.db._connection().row_factory = original_factory
def migrate_model(self, model_cls: type[Model], *args, **kwargs) -> None:
"""Run this migration once for a model's backing table."""
table = model_cls._table
if not self.db.migration_exists(self.name, table):
self._migrate_data(model_cls, *args, **kwargs)
self.db.record_migration(self.name, table)
@abstractmethod
def _migrate_data(
self, model_cls: type[Model], current_fields: set[str]
) -> None:
"""Migrate data for a specific model."""
class TableInfo(TypedDict):
columns: set[str]
migrations: set[str]
class Database:
"""A container for Model objects that wraps an SQLite database as
the backend.
"""
_models: Sequence[type[Model]] = ()
"""The Model subclasses representing tables in this database.
"""
_migrations: Sequence[tuple[type[Migration], Sequence[type[Model]]]] = ()
"""Migrations that are to be performed for the configured models."""
supports_extensions = hasattr(sqlite3.Connection, "enable_load_extension")
"""Whether or not the current version of SQLite supports extensions"""
revision = 0
"""The current revision of the database. To be increased whenever
data is written in a transaction.
"""
def __init__(self, path, timeout: float = 5.0):
if sqlite3.threadsafety == 0:
raise RuntimeError(
"sqlite3 must be compiled with multi-threading support"
)
# Print tracebacks for exceptions in user defined functions
# See also `self.add_functions` and `DBCustomFunctionError`.
#
# `if`: use feature detection because PyPy doesn't support this.
if hasattr(sqlite3, "enable_callback_tracebacks"):
sqlite3.enable_callback_tracebacks(True)
self.path = path
self.timeout = timeout
self._connections: dict[int, sqlite3.Connection] = {}
self._tx_stacks: defaultdict[int, list[Transaction]] = defaultdict(list)
self._extensions: list[str] = []
# A lock to protect the _connections and _tx_stacks maps, which
# both map thread IDs to private resources.
self._shared_map_lock = threading.Lock()
# A lock to protect access to the database itself. SQLite does
# allow multiple threads to access the database at the same
# time, but many users were experiencing crashes related to this
# capability: where SQLite was compiled without HAVE_USLEEP, its
# backoff algorithm in the case of contention was causing
# whole-second sleeps (!) that would trigger its internal
# timeout. Using this lock ensures only one SQLite transaction
# is active at a time.
self._db_lock = threading.Lock()
# Set up database schema.
self._ensure_migration_state_table()
for model_cls in self._models:
self._make_table(model_cls._table, model_cls._fields)
self._make_attribute_table(model_cls._flex_table)
self._create_indices(model_cls._table, model_cls._indices)
self._migrate()
@cached_property
def db_tables(self) -> dict[str, TableInfo]:
column_queries = [
f"""
SELECT '{m._table}' AS table_name, 'columns' AS source, name
FROM pragma_table_info('{m._table}')
"""
for m in self._models
]
with self.transaction() as tx:
rows = tx.query(f"""
{" UNION ALL ".join(column_queries)}
UNION ALL
SELECT table_name, 'migrations' AS source, name FROM migrations
""")
tables_data: dict[str, TableInfo] = defaultdict(
lambda: TableInfo(columns=set(), migrations=set())
)
source: Literal["columns", "migrations"]
for table_name, source, name in rows:
tables_data[table_name][source].add(name)
return tables_data
# Primitive access control: connections and transactions.
def _connection(self) -> Connection:
"""Get a SQLite connection object to the underlying database.
One connection object is created per thread.
"""
thread_id = threading.current_thread().ident
# Help the type checker: ident can only be None if the thread has not
# been started yet; but since this results from current_thread(), that
# can't happen
assert thread_id is not None
with self._shared_map_lock:
if thread_id in self._connections:
return self._connections[thread_id]
else:
conn = self._create_connection()
self._connections[thread_id] = conn
return conn
def _create_connection(self) -> Connection:
"""Create a SQLite connection to the underlying database.
Makes a new connection every time. If you need to configure the
connection settings (e.g., add custom functions), override this
method.
"""
# Make a new connection. The `sqlite3` module can't use
# bytestring paths here on Python 3, so we need to
# provide a `str` using `os.fsdecode`.
conn = sqlite3.connect(
os.fsdecode(self.path),
timeout=self.timeout,
# We have our own same-thread checks in _connection(), but need to
# call conn.close() in _close()
check_same_thread=False,
)
if sys.version_info >= (3, 12) and sqlite3.sqlite_version_info >= (
3,
29,
0,
):
# If possible, disable double-quoted strings
conn.setconfig(sqlite3.SQLITE_DBCONFIG_DQS_DDL, 0)
conn.setconfig(sqlite3.SQLITE_DBCONFIG_DQS_DML, 0)
self.add_functions(conn)
if self.supports_extensions:
conn.enable_load_extension(True)
# Load any extension that are already loaded for other connections.
for path in self._extensions:
conn.load_extension(path)
# Access SELECT results like dictionaries.
conn.row_factory = sqlite3.Row
return conn
def add_functions(self, conn):
def regexp(value, pattern):
if isinstance(value, bytes):
value = value.decode()
return re.search(pattern, str(value)) is not None
def bytelower(bytestring: AnyStr | None) -> AnyStr | None:
"""A custom ``bytelower`` sqlite function so we can compare
bytestrings in a semi case insensitive fashion.
This is to work around sqlite builds are that compiled with
``-DSQLITE_LIKE_DOESNT_MATCH_BLOBS``. See
``https://github.com/beetbox/beets/issues/2172`` for details.
"""
if bytestring is not None:
return bytestring.lower()
return bytestring
create_function = conn.create_function
if sys.version_info >= (3, 8) and sqlite_version_info >= (3, 8, 3):
# Let sqlite make extra optimizations
create_function = functools.partial(
conn.create_function, deterministic=True
)
create_function("regexp", 2, regexp)
create_function("unidecode", 1, unidecode)
create_function("bytelower", 1, bytelower)
def _close(self):
"""Close the all connections to the underlying SQLite database
from all threads. This does not render the database object
unusable; new connections can still be opened on demand.
"""
with self._shared_map_lock:
while self._connections:
_thread_id, conn = self._connections.popitem()
conn.close()
@contextmanager
def _tx_stack(self) -> Generator[list[Transaction]]:
"""A context manager providing access to the current thread's
transaction stack. The context manager synchronizes access to
the stack map. Transactions should never migrate across threads.
"""
thread_id = threading.current_thread().ident
# Help the type checker: ident can only be None if the thread has not
# been started yet; but since this results from current_thread(), that
# can't happen
assert thread_id is not None
with self._shared_map_lock:
yield self._tx_stacks[thread_id]
def transaction(self) -> Transaction:
"""Get a :class:`Transaction` object for interacting directly
with the underlying SQLite database.
"""
return Transaction(self)
def load_extension(self, path: str):
"""Load an SQLite extension into all open connections."""
if not self.supports_extensions:
raise ValueError(
"this sqlite3 installation does not support extensions"
)
self._extensions.append(path)
# Load the extension into every open connection.
for conn in self._connections.values():
conn.load_extension(path)
# Schema setup and migration.
def _make_table(self, table: str, fields: Mapping[str, types.Type]):
"""Set up the schema of the database. `fields` is a mapping
from field names to `Type`s. Columns are added if necessary.
"""
if table not in self.db_tables:
# No table exists.
columns = []
for name, typ in fields.items():
columns.append(f"{name} {typ.sql}")
setup_sql = f"CREATE TABLE {table} ({', '.join(columns)});\n"
self.db_tables[table]["columns"] = set(fields)
else:
# Table exists does not match the field set.
setup_sql = ""
current_fields = self.db_tables[table]["columns"]
for name, typ in fields.items():
if name not in current_fields:
setup_sql += (
f"ALTER TABLE {table} ADD COLUMN {name} {typ.sql};\n"
)
with self.transaction() as tx:
tx.script(setup_sql)
def _make_attribute_table(self, flex_table: str):
"""Create a table and associated index for flexible attributes
for the given entity (if they don't exist).
"""
with self.transaction() as tx:
tx.script(f"""
CREATE TABLE IF NOT EXISTS {flex_table} (
id INTEGER PRIMARY KEY,
entity_id INTEGER,
key TEXT,
value TEXT,
UNIQUE(entity_id, key) ON CONFLICT REPLACE);
CREATE INDEX IF NOT EXISTS {flex_table}_by_entity
ON {flex_table} (entity_id);
""")
def _create_indices(
self,
table: str,
indices: Sequence[Index],
):
"""Create indices for the given table if they don't exist."""
with self.transaction() as tx:
for index in indices:
tx.script(
f"CREATE INDEX IF NOT EXISTS {index.name} "
f"ON {table} ({', '.join(index.columns)});"
)
# Generic migration state handling.
def _ensure_migration_state_table(self) -> None:
with self.transaction() as tx:
tx.script("""
CREATE TABLE IF NOT EXISTS migrations (
name TEXT NOT NULL,
table_name TEXT NOT NULL,
PRIMARY KEY(name, table_name)
);
""")
def _migrate(self) -> None:
"""Perform any necessary migration for the database."""
for migration_cls, model_classes in self._migrations:
migration = migration_cls(self)
for model_cls in model_classes:
migration.migrate_model(
model_cls, self.db_tables[model_cls._table]["columns"]
)
def migration_exists(self, name: str, table: str) -> bool:
"""Return whether a named migration has been marked complete."""
return name in self.db_tables[table]["migrations"]
def record_migration(self, name: str, table: str) -> None:
"""Set completion state for a named migration."""
with self.transaction() as tx:
tx.mutate(
"INSERT INTO migrations(name, table_name) VALUES (?, ?)",
(name, table),
)
# Querying.
def _fetch(
self,
model_cls: type[AnyModel],
query: Query | None = None,
sort: Sort | None = None,
) -> Results[AnyModel]:
"""Fetch the objects of type `model_cls` matching the given
query. The query may be given as a string, string sequence, a
Query object, or None (to fetch everything). `sort` is an
`Sort` object.
"""
query = query or TrueQuery() # A null query.
sort = sort or NullSort() # Unsorted.
where, subvals = query.clause()
order_by = sort.order_clause()
table = model_cls._table
_from = table
if query.field_names & model_cls.other_db_fields:
_from += f" {model_cls.relation_join}"
# group by id to avoid duplicates when joining with the relation
sql = (
f"SELECT {table}.* "
f"FROM ({_from}) "
f"WHERE {where or 1} "
f"GROUP BY {table}.id"
)
# Fetch flexible attributes for items matching the main query.
# Doing the per-item filtering in python is faster than issuing
# one query per item to sqlite.
flex_sql = (
"SELECT * "
f"FROM {model_cls._flex_table} "
f"WHERE entity_id IN (SELECT id FROM ({sql}))"
)
if order_by:
# the sort field may exist in both 'items' and 'albums' tables
# (when they are joined), causing ambiguous column OperationalError
# if we try to order directly.
# Since the join is required only for filtering, we can filter in
# a subquery and order the result, which returns unique fields.
sql = f"SELECT * FROM ({sql}) ORDER BY {order_by}"
with self.transaction() as tx:
rows = tx.query(sql, subvals)
flex_rows = tx.query(flex_sql, subvals)
return Results(
model_cls,
rows,
self,
flex_rows,
None if where else query, # Slow query component.
sort if sort.is_slow() else None, # Slow sort component.
)
def _get(self, model_cls: type[AnyModel], id_: int) -> AnyModel | None:
"""Get a Model object by its id or None if the id does not exist."""
return self._fetch(model_cls, MatchQuery("id", id_)).get()
class Index(NamedTuple):
"""A helper class to represent the index
information in the database schema.
"""
name: str
columns: tuple[str, ...]
================================================
FILE: beets/dbcore/query.py
================================================
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# 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.
"""The Query type hierarchy for DBCore."""
from __future__ import annotations
import os
import re
import unicodedata
from abc import ABC, abstractmethod
from collections.abc import Sequence
from datetime import datetime, timedelta
from functools import cached_property, reduce
from operator import mul, or_
from re import Pattern
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
from beets import util
from beets.util.units import raw_seconds_short
if TYPE_CHECKING:
from collections.abc import Iterator, MutableSequence
from beets.dbcore.db import AnyModel, Model
P = TypeVar("P", default=Any)
else:
P = TypeVar("P")
# To use the SQLite "blob" type, it doesn't suffice to provide a byte
# string; SQLite treats that as encoded text. Wrapping it in a
# `memoryview` tells it that we actually mean non-text data.
# needs to be defined in here due to circular import.
# TODO: remove it from this module and define it in dbcore/types.py instead
BLOB_TYPE = memoryview
class ParsingError(ValueError):
"""Abstract class for any unparsable user-requested album/query
specification.
"""
class InvalidQueryError(ParsingError):
"""Represent any kind of invalid query.
The query should be a unicode string or a list, which will be space-joined.
"""
def __init__(self, query, explanation):
if isinstance(query, list):
query = " ".join(query)
message = f"'{query}': {explanation}"
super().__init__(message)
class InvalidQueryArgumentValueError(ParsingError):
"""Represent a query argument that could not be converted as expected.
It exists to be caught in upper stack levels so a meaningful (i.e. with the
query) InvalidQueryError can be raised.
"""
def __init__(self, what, expected, detail=None):
message = f"'{what}' is not {expected}"
if detail:
message = f"{message}: {detail}"
super().__init__(message)
class Query(ABC):
"""An abstract class representing a query into the database."""
@property
def field_names(self) -> set[str]:
"""Return a set with field names that this query operates on."""
return set()
@abstractmethod
def clause(self) -> tuple[str | None, Sequence[Any]]:
"""Generate an SQLite expression implementing the query.
Return (clause, subvals) where clause is a valid sqlite
WHERE clause implementing the query and subvals is a list of
items to be substituted for ?s in the clause.
The default implementation returns None, falling back to a slow query
using `match()`.
"""
@abstractmethod
def match(self, obj: Model):
"""Check whether this query matches a given Model. Can be used to
perform queries on arbitrary sets of Model.
"""
def __and__(self, other: Query) -> AndQuery:
return AndQuery([self, other])
def __repr__(self) -> str:
return f"{self.__class__.__name__}()"
def __eq__(self, other) -> bool:
return type(self) is type(other)
def __hash__(self) -> int:
"""Minimalistic default implementation of a hash.
Given the implementation if __eq__ above, this is
certainly correct.
"""
return hash(type(self))
SQLiteType = str | bytes | float | int | memoryview | None
AnySQLiteType = TypeVar("AnySQLiteType", bound=SQLiteType)
FieldQueryType = type["FieldQuery"]
class FieldQuery(Query, Generic[P]):
"""An abstract query that searches in a specific field for a
pattern. Subclasses must provide a `value_match` class method, which
determines whether a certain pattern string matches a certain value
string. Subclasses may also provide `col_clause` to implement the
same matching functionality in SQLite.
"""
@property
def field(self) -> str:
return (
f"{self.table}.{self.field_name}" if self.table else self.field_name
)
@property
def field_names(self) -> set[str]:
"""Return a set with field names that this query operates on."""
return {self.field_name}
def __init__(self, field_name: str, pattern: P, fast: bool = True):
self.table, _, self.field_name = field_name.rpartition(".")
self.pattern = pattern
self.fast = fast
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
raise NotImplementedError
def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
if self.fast:
return self.col_clause()
else:
# Matching a flexattr. This is a slow query.
return None, ()
@classmethod
def value_match(cls, pattern: P, value: Any):
"""Determine whether the value matches the pattern."""
raise NotImplementedError
def match(self, obj: Model) -> bool:
return self.value_match(self.pattern, obj.get(self.field_name))
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.field_name!r}, {self.pattern!r}, "
f"fast={self.fast})"
)
def __eq__(self, other) -> bool:
return (
super().__eq__(other)
and self.field_name == other.field_name
and self.pattern == other.pattern
)
def __hash__(self) -> int:
return hash((self.field_name, hash(self.pattern)))
class MatchQuery(FieldQuery[AnySQLiteType]):
"""A query that looks for exact matches in an Model field."""
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
return f"{self.field} = ?", [self.pattern]
@classmethod
def value_match(cls, pattern: AnySQLiteType, value: Any) -> bool:
return pattern == value
class NoneQuery(FieldQuery[None]):
"""A query that checks whether a field is null."""
def __init__(self, field, fast: bool = True):
super().__init__(field, None, fast)
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
return f"{self.field} IS NULL", ()
def match(self, obj: Model) -> bool:
return obj.get(self.field_name) is None
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.field_name!r}, {self.fast})"
class StringFieldQuery(FieldQuery[P]):
"""A FieldQuery that converts values to strings before matching
them.
"""
@classmethod
def value_match(cls, pattern: P, value: Any):
"""Determine whether the value matches the pattern. The value
may have any type.
"""
return cls.string_match(pattern, util.as_string(value))
@classmethod
def string_match(
cls,
pattern: P,
value: str,
) -> bool:
"""Determine whether the value matches the pattern. Both
arguments are strings. Subclasses implement this method.
"""
raise NotImplementedError
class StringQuery(StringFieldQuery[str]):
"""A query that matches a whole string in a specific Model field."""
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
search = (
self.pattern.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
clause = f"{self.field} like ? escape '\\'"
subvals = [search]
return clause, subvals
@classmethod
def string_match(cls, pattern: str, value: str) -> bool:
return pattern.lower() == value.lower()
class SubstringQuery(StringFieldQuery[str]):
"""A query that matches a substring in a specific Model field."""
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
pattern = (
self.pattern.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
search = f"%{pattern}%"
clause = f"{self.field} like ? escape '\\'"
subvals = [search]
return clause, subvals
@classmethod
def string_match(cls, pattern: str, value: str) -> bool:
return pattern.lower() in value.lower()
class PathQuery(FieldQuery[bytes]):
"""A query that matches all items under a given path.
Matching can either be case-insensitive or case-sensitive. By
default, the behavior depends on the OS: case-insensitive on Windows
and case-sensitive otherwise.
"""
def __init__(self, field: str, pattern: bytes, fast: bool = True) -> None:
"""Create a path query.
`pattern` must be a path, either to a file or a directory.
"""
path = util.normpath(pattern)
# Case sensitivity depends on the filesystem that the query path is located on.
self.case_sensitive = util.case_sensitive(path)
# Use a normalized-case pattern for case-insensitive matches.
if not self.case_sensitive:
# We need to lowercase the entire path, not just the pattern.
# In particular, on Windows, the drive letter is otherwise not
# lowercased.
# This also ensures that the `match()` method below and the SQL
# from `col_clause()` do the same thing.
path = path.lower()
super().__init__(field, path, fast)
@cached_property
def dir_path(self) -> bytes:
return os.path.join(self.pattern, b"")
@staticmethod
def is_path_query(query_part: str) -> bool:
"""Try to guess whether a unicode query part is a path query.
The path query must
1. precede the colon in the query, if a colon is present
2. contain either ``os.sep`` or ``os.altsep`` (Windows)
3. this path must exist on the filesystem.
"""
query_part = query_part.split(":")[0]
return (
# make sure the query part contains a path separator
bool(set(query_part) & {os.sep, os.altsep})
and os.path.exists(util.normpath(query_part))
)
def match(self, obj: Model) -> bool:
"""Check whether a model object's path matches this query.
Performs either an exact match against the pattern or checks if the path
starts with the given directory path. Case sensitivity depends on the object's
filesystem as determined during initialization.
"""
path = obj.path if self.case_sensitive else obj.path.lower()
return (path == self.pattern) or path.startswith(self.dir_path)
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
"""Generate an SQL clause that implements path matching in the database.
Returns a tuple of SQL clause string and parameter values list that matches
paths either exactly or by directory prefix. Handles case sensitivity
appropriately using BYTELOWER for case-insensitive matches.
"""
if self.case_sensitive:
left, right = self.field, "?"
else:
left, right = f"BYTELOWER({self.field})", "BYTELOWER(?)"
return f"({left} = {right}) || (substr({left}, 1, ?) = {right})", [
BLOB_TYPE(self.pattern),
len(dir_blob := BLOB_TYPE(self.dir_path)),
dir_blob,
]
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, "
f"fast={self.fast}, case_sensitive={self.case_sensitive})"
)
class RegexpQuery(StringFieldQuery[Pattern[str]]):
"""A query that matches a regular expression in a specific Model field.
Raises InvalidQueryError when the pattern is not a valid regular
expression.
"""
def __init__(self, field_name: str, pattern: str, fast: bool = True):
pattern = self._normalize(pattern)
try:
pattern_re = re.compile(pattern)
except re.error as exc:
# Invalid regular expression.
raise InvalidQueryArgumentValueError(
pattern, "a regular expression", format(exc)
)
super().__init__(field_name, pattern_re, fast)
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
return f" regexp({self.field}, ?)", [self.pattern.pattern]
@staticmethod
def _normalize(s: str) -> str:
"""Normalize a Unicode string's representation (used on both
patterns and matched values).
"""
return unicodedata.normalize("NFC", s)
@classmethod
def string_match(cls, pattern: Pattern[str], value: str) -> bool:
return pattern.search(cls._normalize(value)) is not None
class BooleanQuery(MatchQuery[int]):
"""Matches a boolean field. Pattern should either be a boolean or a
string reflecting a boolean.
"""
def __init__(
self,
field_name: str,
pattern: bool,
fast: bool = True,
):
if isinstance(pattern, str):
pattern = util.str2bool(pattern)
pattern_int = int(pattern)
super().__init__(field_name, pattern_int, fast)
class NumericQuery(FieldQuery[str]):
"""Matches numeric fields. A syntax using Ruby-style range ellipses
(``..``) lets users specify one- or two-sided ranges. For example,
``year:2001..`` finds music released since the turn of the century.
Raises InvalidQueryError when the pattern does not represent an int or
a float.
"""
def _convert(self, s: str) -> float | int | None:
"""Convert a string to a numeric type (float or int).
Return None if `s` is empty.
Raise an InvalidQueryError if the string cannot be converted.
"""
# This is really just a bit of fun premature optimization.
if not s:
return None
try:
return int(s)
except ValueError:
try:
return float(s)
except ValueError:
raise InvalidQueryArgumentValueError(s, "an int or a float")
def __init__(self, field_name: str, pattern: str, fast: bool = True):
super().__init__(field_name, pattern, fast)
parts = pattern.split("..", 1)
if len(parts) == 1:
# No range.
self.point = self._convert(parts[0])
self.rangemin = None
self.rangemax = None
else:
# One- or two-sided range.
self.point = None
self.rangemin = self._convert(parts[0])
self.rangemax = self._convert(parts[1])
def match(self, obj: Model) -> bool:
if self.field_name not in obj:
return False
value = obj[self.field_name]
if isinstance(value, str):
value = self._convert(value)
if self.point is not None:
return value == self.point
else:
if self.rangemin is not None and value < self.rangemin:
return False
if self.rangemax is not None and value > self.rangemax:
return False
return True
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
if self.point is not None:
return f"{self.field}=?", (self.point,)
else:
if self.rangemin is not None and self.rangemax is not None:
return (
f"{self.field} >= ? AND {self.field} <= ?",
(self.rangemin, self.rangemax),
)
elif self.rangemin is not None:
return f"{self.field} >= ?", (self.rangemin,)
elif self.rangemax is not None:
return f"{self.field} <= ?", (self.rangemax,)
else:
return "1", ()
class InQuery(Generic[AnySQLiteType], FieldQuery[Sequence[AnySQLiteType]]):
"""Query which matches values in the given set."""
field_name: str
pattern: Sequence[AnySQLiteType]
fast: bool = True
@property
def subvals(self) -> Sequence[SQLiteType]:
return self.pattern
def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
placeholders = ", ".join(["?"] * len(self.subvals))
return f"{self.field_name} IN ({placeholders})", self.subvals
@classmethod
def value_match(
cls, pattern: Sequence[AnySQLiteType], value: AnySQLiteType
) -> bool:
return value in pattern
class CollectionQuery(Query):
"""An abstract query class that aggregates other queries. Can be
indexed like a list to access the sub-queries.
"""
@property
def field_names(self) -> set[str]:
"""Return a set with field names that this query operates on."""
return reduce(or_, (sq.field_names for sq in self.subqueries))
def __init__(self, subqueries: Sequence[Query] = ()):
self.subqueries = subqueries
# Act like a sequence.
def __len__(self) -> int:
return len(self.subqueries)
def __getitem__(self, key):
return self.subqueries[key]
def __iter__(self) -> Iterator[Query]:
return iter(self.subqueries)
def __contains__(self, subq) -> bool:
return subq in self.subqueries
def clause_with_joiner(
self,
joiner: str,
) -> tuple[str | None, Sequence[SQLiteType]]:
"""Return a clause created by joining together the clauses of
all subqueries with the string joiner (padded by spaces).
"""
clause_parts = []
subvals: list[SQLiteType] = []
for subq in self.subqueries:
subq_clause, subq_subvals = subq.clause()
if not subq_clause:
# Fall back to slow query.
return None, ()
clause_parts.append(f"({subq_clause})")
subvals += subq_subvals
clause = f" {joiner} ".join(clause_parts)
return clause, subvals
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.subqueries!r})"
def __eq__(self, other) -> bool:
return super().__eq__(other) and self.subqueries == other.subqueries
def __hash__(self) -> int:
"""Since subqueries are mutable, this object should not be hashable.
However and for conveniences purposes, it can be hashed.
"""
return reduce(mul, map(hash, self.subqueries), 1)
class MutableCollectionQuery(CollectionQuery):
"""A collection query whose subqueries may be modified after the
query is initialized.
"""
subqueries: MutableSequence[Query]
def __setitem__(self, key, value):
self.subqueries[key] = value
def __delitem__(self, key):
del self.subqueries[key]
class AndQuery(MutableCollectionQuery):
"""A conjunction of a list of other queries."""
def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
return self.clause_with_joiner("and")
def match(self, obj: Model) -> bool:
return all(q.match(obj) for q in self.subqueries)
class OrQuery(MutableCollectionQuery):
"""A conjunction of a list of other queries."""
def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
return self.clause_with_joiner("or")
def match(self, obj: Model) -> bool:
return any(q.match(obj) for q in self.subqueries)
class NotQuery(Query):
"""A query that matches the negation of its `subquery`, as a shortcut for
performing `not(subquery)` without using regular expressions.
"""
@property
def field_names(self) -> set[str]:
"""Return a set with field names that this query operates on."""
return self.subquery.field_names
def __init__(self, subquery):
self.subquery = subquery
def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
clause, subvals = self.subquery.clause()
if clause:
return f"not ({clause})", subvals
else:
# If there is no clause, there is nothing to negate. All the logic
# is handled by match() for slow queries.
return clause, subvals
def match(self, obj: Model) -> bool:
return not self.subquery.match(obj)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.subquery!r})"
def __eq__(self, other) -> bool:
return super().__eq__(other) and self.subquery == other.subquery
def __hash__(self) -> int:
return hash(("not", hash(self.subquery)))
class TrueQuery(Query):
"""A query that always matches."""
def clause(self) -> tuple[str, Sequence[SQLiteType]]:
return "1", ()
def match(self, obj: Model) -> bool:
return True
class FalseQuery(Query):
"""A query that never matches."""
def clause(self) -> tuple[str, Sequence[SQLiteType]]:
return "0", ()
def match(self, obj: Model) -> bool:
return False
# Time/date queries.
def _parse_periods(pattern: str) -> tuple[Period | None, Period | None]:
"""Parse a string containing two dates separated by two dots (..).
Return a pair of `Period` objects.
"""
parts = pattern.split("..", 1)
if len(parts) == 1:
instant = Period.parse(parts[0])
return (instant, instant)
else:
start = Period.parse(parts[0])
end = Period.parse(parts[1])
return (start, end)
class Period:
"""A period of time given by a date, time and precision.
Example: 2014-01-01 10:50:30 with precision 'month' represents all
instants of time during January 2014.
"""
precisions = ("year", "month", "day", "hour", "minute", "second")
date_formats = (
("%Y",), # year
("%Y-%m",), # month
("%Y-%m-%d",), # day
("%Y-%m-%dT%H", "%Y-%m-%d %H"), # hour
("%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M"), # minute
("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"), # second
)
relative_units: ClassVar[dict[str, int]] = {
"y": 365,
"m": 30,
"w": 7,
"d": 1,
}
relative_re = "(?P<sign>[+|-]?)(?P<quantity>[0-9]+)(?P<timespan>[y|m|w|d])"
def __init__(self, date: datetime, precision: str):
"""Create a period with the given date (a `datetime` object) and
precision (a string, one of "year", "month", "day", "hour", "minute",
or "second").
"""
if precision not in Period.precisions:
raise ValueError(f"Invalid precision {precision}")
self.date = date
self.precision = precision
@classmethod
def parse(cls: type[Period], string: str) -> Period | None:
"""Parse a date and return a `Period` object or `None` if the
string is empty, or raise an InvalidQueryArgumentValueError if
the string cannot be parsed to a date.
The date may be absolute or relative. Absolute dates look like
`YYYY`, or `YYYY-MM-DD`, or `YYYY-MM-DD HH:MM:SS`, etc. Relative
dates have three parts:
- Optionally, a ``+`` or ``-`` sign indicating
gitextract_bueceyr9/
├── .git-blame-ignore-revs
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ ├── config.yml
│ │ └── feature-request.md
│ ├── copilot-instructions.md
│ ├── problem-matchers/
│ │ ├── sphinx-build.json
│ │ └── sphinx-lint.json
│ ├── pull_request_template.md
│ ├── stale.yml
│ └── workflows/
│ ├── changelog_reminder.yaml
│ ├── ci.yaml
│ ├── integration_test.yaml
│ ├── lint.yaml
│ └── make_release.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CODE_OF_CONDUCT.rst
├── CONTRIBUTING.rst
├── LICENSE
├── README.rst
├── README_kr.rst
├── SECURITY.md
├── beets/
│ ├── __init__.py
│ ├── __main__.py
│ ├── autotag/
│ │ ├── __init__.py
│ │ ├── distance.py
│ │ ├── hooks.py
│ │ └── match.py
│ ├── config_default.yaml
│ ├── dbcore/
│ │ ├── __init__.py
│ │ ├── db.py
│ │ ├── query.py
│ │ ├── queryparse.py
│ │ └── types.py
│ ├── importer/
│ │ ├── __init__.py
│ │ ├── session.py
│ │ ├── stages.py
│ │ ├── state.py
│ │ └── tasks.py
│ ├── library/
│ │ ├── __init__.py
│ │ ├── exceptions.py
│ │ ├── library.py
│ │ ├── migrations.py
│ │ ├── models.py
│ │ └── queries.py
│ ├── logging.py
│ ├── mediafile.py
│ ├── metadata_plugins.py
│ ├── plugins.py
│ ├── py.typed
│ ├── test/
│ │ ├── __init__.py
│ │ ├── _common.py
│ │ └── helper.py
│ ├── ui/
│ │ ├── __init__.py
│ │ └── commands/
│ │ ├── __init__.py
│ │ ├── completion.py
│ │ ├── completion_base.sh
│ │ ├── config.py
│ │ ├── fields.py
│ │ ├── help.py
│ │ ├── import_/
│ │ │ ├── __init__.py
│ │ │ ├── display.py
│ │ │ └── session.py
│ │ ├── list.py
│ │ ├── modify.py
│ │ ├── move.py
│ │ ├── remove.py
│ │ ├── stats.py
│ │ ├── update.py
│ │ ├── utils.py
│ │ ├── version.py
│ │ └── write.py
│ └── util/
│ ├── __init__.py
│ ├── artresizer.py
│ ├── bluelet.py
│ ├── color.py
│ ├── config.py
│ ├── deprecation.py
│ ├── diff.py
│ ├── functemplate.py
│ ├── hidden.py
│ ├── id_extractors.py
│ ├── layout.py
│ ├── lyrics.py
│ ├── m3u.py
│ ├── pipeline.py
│ └── units.py
├── beetsplug/
│ ├── _typing.py
│ ├── _utils/
│ │ ├── __init__.py
│ │ ├── art.py
│ │ ├── musicbrainz.py
│ │ ├── requests.py
│ │ └── vfs.py
│ ├── absubmit.py
│ ├── acousticbrainz.py
│ ├── advancedrewrite.py
│ ├── albumtypes.py
│ ├── aura.py
│ ├── autobpm.py
│ ├── badfiles.py
│ ├── bareasc.py
│ ├── beatport.py
│ ├── bench.py
│ ├── bpd/
│ │ ├── __init__.py
│ │ └── gstplayer.py
│ ├── bpm.py
│ ├── bpsync.py
│ ├── bucket.py
│ ├── chroma.py
│ ├── convert.py
│ ├── deezer.py
│ ├── discogs/
│ │ ├── __init__.py
│ │ ├── states.py
│ │ └── types.py
│ ├── duplicates.py
│ ├── edit.py
│ ├── embedart.py
│ ├── embyupdate.py
│ ├── export.py
│ ├── fetchart.py
│ ├── filefilter.py
│ ├── fish.py
│ ├── freedesktop.py
│ ├── fromfilename.py
│ ├── ftintitle.py
│ ├── fuzzy.py
│ ├── hook.py
│ ├── ihate.py
│ ├── importadded.py
│ ├── importfeeds.py
│ ├── importsource.py
│ ├── info.py
│ ├── inline.py
│ ├── ipfs.py
│ ├── keyfinder.py
│ ├── kodiupdate.py
│ ├── lastgenre/
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── genres-tree.yaml
│ │ └── genres.txt
│ ├── lastimport.py
│ ├── limit.py
│ ├── listenbrainz.py
│ ├── loadext.py
│ ├── lyrics.py
│ ├── mbcollection.py
│ ├── mbpseudo.py
│ ├── mbsubmit.py
│ ├── mbsync.py
│ ├── metasync/
│ │ ├── __init__.py
│ │ ├── amarok.py
│ │ └── itunes.py
│ ├── missing.py
│ ├── mpdstats.py
│ ├── mpdupdate.py
│ ├── musicbrainz.py
│ ├── parentwork.py
│ ├── permissions.py
│ ├── play.py
│ ├── playlist.py
│ ├── plexupdate.py
│ ├── random.py
│ ├── replace.py
│ ├── replaygain.py
│ ├── rewrite.py
│ ├── scrub.py
│ ├── smartplaylist.py
│ ├── sonosupdate.py
│ ├── spotify.py
│ ├── subsonicplaylist.py
│ ├── subsonicupdate.py
│ ├── substitute.py
│ ├── the.py
│ ├── thumbnails.py
│ ├── titlecase.py
│ ├── types.py
│ ├── unimported.py
│ ├── web/
│ │ ├── __init__.py
│ │ ├── static/
│ │ │ ├── backbone.js
│ │ │ ├── beets.css
│ │ │ ├── beets.js
│ │ │ ├── jquery.js
│ │ │ └── underscore.js
│ │ └── templates/
│ │ └── index.html
│ └── zero.py
├── codecov.yml
├── docs/
│ ├── .gitignore
│ ├── Makefile
│ ├── _static/
│ │ └── beets.css
│ ├── _templates/
│ │ └── autosummary/
│ │ ├── base.rst
│ │ ├── class.rst
│ │ ├── module.rst
│ │ └── namedtuple.rst
│ ├── api/
│ │ ├── database.rst
│ │ ├── index.rst
│ │ ├── plugin_utilities.rst
│ │ └── plugins.rst
│ ├── changelog.rst
│ ├── code_of_conduct.rst
│ ├── conf.py
│ ├── contributing.rst
│ ├── dev/
│ │ ├── cli.rst
│ │ ├── importer.rst
│ │ ├── index.rst
│ │ ├── library.rst
│ │ ├── paths.rst
│ │ └── plugins/
│ │ ├── autotagger.rst
│ │ ├── commands.rst
│ │ ├── events.rst
│ │ ├── index.rst
│ │ └── other/
│ │ ├── config.rst
│ │ ├── fields.rst
│ │ ├── import.rst
│ │ ├── index.rst
│ │ ├── logging.rst
│ │ ├── mediafile.rst
│ │ ├── prompts.rst
│ │ └── templates.rst
│ ├── extensions/
│ │ └── conf.py
│ ├── faq.rst
│ ├── guides/
│ │ ├── advanced.rst
│ │ ├── index.rst
│ │ ├── installation.rst
│ │ ├── main.rst
│ │ └── tagger.rst
│ ├── index.rst
│ ├── modd.conf
│ ├── plugins/
│ │ ├── absubmit.rst
│ │ ├── acousticbrainz.rst
│ │ ├── advancedrewrite.rst
│ │ ├── albumtypes.rst
│ │ ├── aura.rst
│ │ ├── autobpm.rst
│ │ ├── badfiles.rst
│ │ ├── bareasc.rst
│ │ ├── beatport.rst
│ │ ├── bpd.rst
│ │ ├── bpm.rst
│ │ ├── bpsync.rst
│ │ ├── bucket.rst
│ │ ├── chroma.rst
│ │ ├── convert.rst
│ │ ├── deezer.rst
│ │ ├── discogs.rst
│ │ ├── duplicates.rst
│ │ ├── edit.rst
│ │ ├── embedart.rst
│ │ ├── embyupdate.rst
│ │ ├── export.rst
│ │ ├── fetchart.rst
│ │ ├── filefilter.rst
│ │ ├── fish.rst
│ │ ├── freedesktop.rst
│ │ ├── fromfilename.rst
│ │ ├── ftintitle.rst
│ │ ├── fuzzy.rst
│ │ ├── hook.rst
│ │ ├── ihate.rst
│ │ ├── importadded.rst
│ │ ├── importfeeds.rst
│ │ ├── importsource.rst
│ │ ├── index.rst
│ │ ├── info.rst
│ │ ├── inline.rst
│ │ ├── ipfs.rst
│ │ ├── keyfinder.rst
│ │ ├── kodiupdate.rst
│ │ ├── lastgenre.rst
│ │ ├── lastimport.rst
│ │ ├── limit.rst
│ │ ├── listenbrainz.rst
│ │ ├── loadext.rst
│ │ ├── lyrics.rst
│ │ ├── mbcollection.rst
│ │ ├── mbpseudo.rst
│ │ ├── mbsubmit.rst
│ │ ├── mbsync.rst
│ │ ├── metasync.rst
│ │ ├── missing.rst
│ │ ├── mpdstats.rst
│ │ ├── mpdupdate.rst
│ │ ├── musicbrainz.rst
│ │ ├── parentwork.rst
│ │ ├── permissions.rst
│ │ ├── play.rst
│ │ ├── playlist.rst
│ │ ├── plexupdate.rst
│ │ ├── random.rst
│ │ ├── replace.rst
│ │ ├── replaygain.rst
│ │ ├── rewrite.rst
│ │ ├── scrub.rst
│ │ ├── shared_metadata_source_config.rst
│ │ ├── smartplaylist.rst
│ │ ├── sonosupdate.rst
│ │ ├── spotify.rst
│ │ ├── subsonicplaylist.rst
│ │ ├── subsonicupdate.rst
│ │ ├── substitute.rst
│ │ ├── the.rst
│ │ ├── thumbnails.rst
│ │ ├── titlecase.rst
│ │ ├── types.rst
│ │ ├── unimported.rst
│ │ ├── web.rst
│ │ └── zero.rst
│ ├── reference/
│ │ ├── cli.rst
│ │ ├── config.rst
│ │ ├── index.rst
│ │ ├── pathformat.rst
│ │ └── query.rst
│ └── team.rst
├── extra/
│ ├── _beet
│ ├── ascii_logo.txt
│ ├── beets.reg
│ └── release.py
├── pyproject.toml
├── setup.cfg
└── test/
├── __init__.py
├── autotag/
│ ├── __init__.py
│ ├── test_autotag.py
│ ├── test_distance.py
│ ├── test_hooks.py
│ └── test_match.py
├── conftest.py
├── library/
│ ├── __init__.py
│ └── test_migrations.py
├── plugins/
│ ├── __init__.py
│ ├── conftest.py
│ ├── lyrics_pages.py
│ ├── test_acousticbrainz.py
│ ├── test_advancedrewrite.py
│ ├── test_albumtypes.py
│ ├── test_art.py
│ ├── test_aura.py
│ ├── test_autobpm.py
│ ├── test_bareasc.py
│ ├── test_beatport.py
│ ├── test_bpd.py
│ ├── test_bucket.py
│ ├── test_convert.py
│ ├── test_discogs.py
│ ├── test_edit.py
│ ├── test_embedart.py
│ ├── test_embyupdate.py
│ ├── test_export.py
│ ├── test_fetchart.py
│ ├── test_filefilter.py
│ ├── test_fromfilename.py
│ ├── test_ftintitle.py
│ ├── test_fuzzy.py
│ ├── test_hook.py
│ ├── test_ihate.py
│ ├── test_importadded.py
│ ├── test_importfeeds.py
│ ├── test_importsource.py
│ ├── test_info.py
│ ├── test_inline.py
│ ├── test_ipfs.py
│ ├── test_keyfinder.py
│ ├── test_lastgenre.py
│ ├── test_limit.py
│ ├── test_listenbrainz.py
│ ├── test_lyrics.py
│ ├── test_mbcollection.py
│ ├── test_mbpseudo.py
│ ├── test_mbsubmit.py
│ ├── test_mbsync.py
│ ├── test_missing.py
│ ├── test_mpdstats.py
│ ├── test_musicbrainz.py
│ ├── test_parentwork.py
│ ├── test_permissions.py
│ ├── test_play.py
│ ├── test_playlist.py
│ ├── test_plexupdate.py
│ ├── test_plugin_mediafield.py
│ ├── test_random.py
│ ├── test_replace.py
│ ├── test_replaygain.py
│ ├── test_scrub.py
│ ├── test_smartplaylist.py
│ ├── test_spotify.py
│ ├── test_subsonicupdate.py
│ ├── test_substitute.py
│ ├── test_the.py
│ ├── test_thumbnails.py
│ ├── test_titlecase.py
│ ├── test_types_plugin.py
│ ├── test_web.py
│ ├── test_zero.py
│ └── utils/
│ ├── __init__.py
│ ├── test_musicbrainz.py
│ └── test_vfs.py
├── rsrc/
│ ├── acousticbrainz/
│ │ └── data.json
│ ├── beetsplug/
│ │ └── test.py
│ ├── convert_stub.py
│ ├── coverart.ogg
│ ├── date_with_slashes.ogg
│ ├── discc.ogg
│ ├── empty.aiff
│ ├── empty.alac.m4a
│ ├── empty.ape
│ ├── empty.dsf
│ ├── empty.flac
│ ├── empty.m4a
│ ├── empty.mpc
│ ├── empty.ogg
│ ├── empty.opus
│ ├── empty.wma
│ ├── empty.wv
│ ├── full.aiff
│ ├── full.alac.m4a
│ ├── full.ape
│ ├── full.dsf
│ ├── full.flac
│ ├── full.m4a
│ ├── full.mpc
│ ├── full.ogg
│ ├── full.opus
│ ├── full.wma
│ ├── full.wv
│ ├── image-2x3.tiff
│ ├── image.ape
│ ├── image.flac
│ ├── image.m4a
│ ├── image.ogg
│ ├── image.wma
│ ├── itunes_library_unix.xml
│ ├── itunes_library_windows.xml
│ ├── lyrics/
│ │ ├── examplecom/
│ │ │ └── beetssong.txt
│ │ ├── geniuscom/
│ │ │ ├── 2pacalleyezonmelyrics.txt
│ │ │ ├── Ttngchinchillalyrics.txt
│ │ │ └── sample.txt
│ │ └── tekstowopl/
│ │ ├── piosenka24kgoldncityofangels1.txt
│ │ ├── piosenkabaileybiggerblackeyedsusan.txt
│ │ └── piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement.txt
│ ├── mbpseudo/
│ │ ├── official_release.json
│ │ └── pseudo_release.json
│ ├── min.flac
│ ├── min.m4a
│ ├── oldape.ape
│ ├── partial.flac
│ ├── partial.m4a
│ ├── playlist.m3u
│ ├── playlist.m3u8
│ ├── playlist_non_ext.m3u
│ ├── playlist_windows.m3u8
│ ├── pure.wma
│ ├── soundcheck-nonascii.m4a
│ ├── spotify/
│ │ ├── album_info.json
│ │ ├── japanese_track_request.json
│ │ ├── missing_request.json
│ │ ├── multiartist_album.json
│ │ ├── multiartist_track.json
│ │ ├── track_info.json
│ │ └── track_request.json
│ ├── t_time.m4a
│ ├── test_completion.sh
│ ├── unparseable.aiff
│ ├── unparseable.alac.m4a
│ ├── unparseable.ape
│ ├── unparseable.dsf
│ ├── unparseable.flac
│ ├── unparseable.m4a
│ ├── unparseable.mpc
│ ├── unparseable.ogg
│ ├── unparseable.opus
│ ├── unparseable.wma
│ ├── unparseable.wv
│ ├── whitenoise.flac
│ ├── whitenoise.opus
│ └── year.ogg
├── test_art_resize.py
├── test_datequery.py
├── test_dbcore.py
├── test_files.py
├── test_hidden.py
├── test_importer.py
├── test_library.py
├── test_logging.py
├── test_m3ufile.py
├── test_metadata_plugins.py
├── test_metasync.py
├── test_pipeline.py
├── test_plugins.py
├── test_query.py
├── test_release.py
├── test_sort.py
├── test_template.py
├── test_types.py
├── test_util.py
├── testall.py
├── ui/
│ ├── __init__.py
│ ├── commands/
│ │ ├── __init__.py
│ │ ├── test_completion.py
│ │ ├── test_config.py
│ │ ├── test_fields.py
│ │ ├── test_import.py
│ │ ├── test_list.py
│ │ ├── test_modify.py
│ │ ├── test_move.py
│ │ ├── test_remove.py
│ │ ├── test_update.py
│ │ ├── test_utils.py
│ │ └── test_write.py
│ ├── test_ui.py
│ ├── test_ui_importer.py
│ └── test_ui_init.py
└── util/
├── test_color.py
├── test_config.py
├── test_diff.py
├── test_id_extractors.py
├── test_layout.py
├── test_lyrics.py
└── test_units.py
Showing preview only (404K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (5087 symbols across 264 files)
FILE: beets/__init__.py
function __getattr__ (line 26) | def __getattr__(name: str):
class IncludeLazyConfig (line 35) | class IncludeLazyConfig(confuse.LazyConfig):
method read (line 40) | def read(self, user: bool = True, defaults: bool = True) -> None:
FILE: beets/autotag/__init__.py
function __getattr__ (line 37) | def __getattr__(name: str):
function _apply_metadata (line 113) | def _apply_metadata(
function correct_list_fields (line 136) | def correct_list_fields(m: LibModel) -> None:
function apply_item_metadata (line 178) | def apply_item_metadata(item: Item, track_info: TrackInfo):
function apply_album_metadata (line 201) | def apply_album_metadata(album_info: AlbumInfo, album: Album):
function apply_metadata (line 207) | def apply_metadata(
FILE: beets/autotag/distance.py
function _string_dist_basic (line 47) | def _string_dist_basic(str1: str, str2: str) -> float:
function string_dist (line 64) | def string_dist(str1: str | None, str2: str | None) -> float:
class Distance (line 123) | class Distance:
method __init__ (line 129) | def __init__(self) -> None:
method _weights (line 134) | def _weights(cls) -> dict[str, float]:
method distance (line 145) | def distance(self) -> float:
method max_distance (line 155) | def max_distance(self) -> float:
method raw_distance (line 163) | def raw_distance(self) -> float:
method items (line 170) | def items(self) -> list[tuple[str, float]]:
method __hash__ (line 187) | def __hash__(self) -> int:
method __eq__ (line 190) | def __eq__(self, other) -> bool:
method __lt__ (line 195) | def __lt__(self, other) -> bool:
method __float__ (line 198) | def __float__(self) -> float:
method __sub__ (line 201) | def __sub__(self, other) -> float:
method __rsub__ (line 204) | def __rsub__(self, other) -> float:
method __str__ (line 207) | def __str__(self) -> str:
method __getitem__ (line 212) | def __getitem__(self, key) -> float:
method __iter__ (line 220) | def __iter__(self) -> Iterator[tuple[str, float]]:
method __len__ (line 223) | def __len__(self) -> int:
method keys (line 226) | def keys(self) -> list[str]:
method update (line 229) | def update(self, dist: Distance):
method _eq (line 240) | def _eq(self, value1: re.Pattern[str] | Any, value2: Any) -> bool:
method add (line 249) | def add(self, key: str, dist: float):
method add_equality (line 259) | def add_equality(
method add_expr (line 280) | def add_expr(self, key: str, expr: bool):
method add_number (line 289) | def add_number(self, key: str, number1: int, number2: int):
method add_priority (line 302) | def add_priority(
method add_ratio (line 325) | def add_ratio(
method add_string (line 341) | def add_string(self, key: str, str1: str | None, str2: str | None):
method add_data_source (line 348) | def add_data_source(self, before: str | None, after: str | None) -> None:
function get_track_length_grace (line 356) | def get_track_length_grace() -> float:
function get_track_length_max (line 362) | def get_track_length_max() -> float:
function track_index_changed (line 367) | def track_index_changed(item: Item, track_info: TrackInfo) -> bool:
function track_distance (line 374) | def track_distance(
function distance (line 422) | def distance(
FILE: beets/autotag/hooks.py
class AttrDict (line 39) | class AttrDict(dict[str, V]):
method copy (line 42) | def copy(self) -> Self:
method __getattr__ (line 45) | def __getattr__(self, attr: str) -> V:
method __setattr__ (line 53) | def __setattr__(self, key: str, value: V) -> None:
method __hash__ (line 56) | def __hash__(self) -> int: # type: ignore[override]
class Info (line 60) | class Info(AttrDict[Any]):
method id (line 66) | def id(self) -> str | None:
method identifier (line 71) | def identifier(self) -> Identifier:
method name (line 76) | def name(self) -> str:
method __init__ (line 79) | def __init__(
class AlbumInfo (line 126) | class AlbumInfo(Info):
method id (line 135) | def id(self) -> str | None:
method name (line 139) | def name(self) -> str:
method __init__ (line 142) | def __init__(
class TrackInfo (line 206) | class TrackInfo(Info):
method id (line 215) | def id(self) -> str | None:
method name (line 219) | def name(self) -> str:
method __init__ (line 222) | def __init__(
class Match (line 270) | class Match:
method type (line 275) | def type(cls) -> str:
class AlbumMatch (line 280) | class AlbumMatch(Match):
method __post_init__ (line 286) | def __post_init__(self) -> None:
method item_info_pairs (line 291) | def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]:
method items (line 295) | def items(self) -> list[Item]:
class TrackMatch (line 300) | class TrackMatch(Match):
FILE: beets/autotag/match.py
class Recommendation (line 51) | class Recommendation(IntEnum):
class Proposal (line 67) | class Proposal(NamedTuple):
function assign_items (line 75) | def assign_items(
function match_by_id (line 107) | def match_by_id(album_id: str | None, consensus: bool) -> Iterable[Album...
function _recommendation (line 124) | def _recommendation(
function _sort_candidates (line 181) | def _sort_candidates(candidates: Iterable[AnyMatch]) -> Sequence[AnyMatch]:
function _add_candidate (line 186) | def _add_candidate(
function tag_album (line 240) | def tag_album(
function tag_item (line 328) | def tag_item(
FILE: beets/dbcore/db.py
class DBAccessError (line 74) | class DBAccessError(Exception):
class DBCustomFunctionError (line 83) | class DBCustomFunctionError(Exception):
method __init__ (line 86) | def __init__(self):
class NotFoundError (line 93) | class NotFoundError(LookupError):
class FormattedMapping (line 97) | class FormattedMapping(Mapping[str, str]):
method __init__ (line 115) | def __init__(
method __getitem__ (line 129) | def __getitem__(self, key: str) -> str:
method __iter__ (line 135) | def __iter__(self) -> Iterator[str]:
method __len__ (line 138) | def __len__(self) -> int:
method get (line 143) | def get( # type: ignore
method _get_formatted (line 153) | def _get_formatted(self, model: Model, key: str) -> str:
class LazyConvertDict (line 181) | class LazyConvertDict:
method __init__ (line 184) | def __init__(self, model_cls: Model):
method init (line 191) | def init(self, data: dict[str, Any]):
method _convert (line 195) | def _convert(self, key: str, value: Any):
method __setitem__ (line 199) | def __setitem__(self, key: str, value: Any):
method __getitem__ (line 203) | def __getitem__(self, key: str) -> Any:
method __delitem__ (line 214) | def __delitem__(self, key: str):
method keys (line 221) | def keys(self) -> list[str]:
method copy (line 225) | def copy(self) -> LazyConvertDict:
method update (line 234) | def update(self, values: Mapping[str, Any]):
method items (line 239) | def items(self) -> Iterable[tuple[str, Any]]:
method get (line 246) | def get(self, key: str, default: Any | None = None):
method __contains__ (line 255) | def __contains__(self, key: Any) -> bool:
method __iter__ (line 259) | def __iter__(self) -> Iterator[str]:
method __len__ (line 270) | def __len__(self) -> int:
class Model (line 278) | class Model(ABC, Generic[D]):
method _types (line 328) | def _types(cls) -> dict[str, types.Type]:
method _queries (line 338) | def _queries(cls) -> dict[str, FieldQueryType]:
method _relation (line 356) | def _relation(cls):
method relation_join (line 361) | def relation_join(cls) -> str:
method all_db_fields (line 369) | def all_db_fields(cls) -> set[str]:
method shared_db_fields (line 373) | def shared_db_fields(cls) -> set[str]:
method other_db_fields (line 377) | def other_db_fields(cls) -> set[str]:
method db (line 382) | def db(self) -> D:
method get_fresh_from_db (line 389) | def get_fresh_from_db(self) -> Self:
method _getters (line 398) | def _getters(cls: type[Model]):
method _template_funcs (line 404) | def _template_funcs(self) -> Mapping[str, Callable[[str], str]]:
method __init__ (line 413) | def __init__(self, db: D | None = None, **values):
method _awaken (line 427) | def _awaken(
method __repr__ (line 445) | def __repr__(self) -> str:
method clear_dirty (line 451) | def clear_dirty(self):
method _check_db (line 459) | def _check_db(self, need_id: bool = True) -> D:
method copy (line 471) | def copy(self) -> Model:
method _type (line 489) | def _type(cls, key) -> types.Type:
method _get (line 497) | def _get(self, key, default: Any = None, raise_: bool = False):
method __getitem__ (line 518) | def __getitem__(self, key):
method _setitem (line 524) | def _setitem(self, key, value):
method __setitem__ (line 546) | def __setitem__(self, key, value):
method __delitem__ (line 550) | def __delitem__(self, key):
method keys (line 562) | def keys(self, computed: bool = False):
method all_keys (line 574) | def all_keys(cls):
method update (line 582) | def update(self, values):
method items (line 587) | def items(self) -> Iterator[tuple[str, Any]]:
method __contains__ (line 594) | def __contains__(self, key) -> bool:
method __iter__ (line 598) | def __iter__(self) -> Iterator[str]:
method __getattr__ (line 606) | def __getattr__(self, key):
method __setattr__ (line 615) | def __setattr__(self, key, value):
method __delattr__ (line 621) | def __delattr__(self, key):
method store (line 629) | def store(self, fields: Iterable[str] | None = None):
method load (line 675) | def load(self):
method remove (line 688) | def remove(self):
method add (line 696) | def add(self, db: D | None = None):
method formatted (line 723) | def formatted(
method evaluate_template (line 733) | def evaluate_template(
method _parse (line 755) | def _parse(cls, key, string: str) -> Any:
method set_parse (line 762) | def set_parse(self, key, string: str):
method __getstate__ (line 766) | def __getstate__(self):
class Results (line 782) | class Results(Generic[AnyModel]):
method __init__ (line 787) | def __init__(
method _get_objects (line 827) | def _get_objects(self) -> Iterator[AnyModel]:
method __iter__ (line 862) | def __iter__(self) -> Iterator[AnyModel]:
method _get_indexed_flex_attrs (line 875) | def _get_indexed_flex_attrs(self) -> dict[int, FlexAttrs]:
method _make_model (line 886) | def _make_model(
method __len__ (line 897) | def __len__(self) -> int:
method __nonzero__ (line 914) | def __nonzero__(self) -> bool:
method __bool__ (line 918) | def __bool__(self) -> bool:
method __getitem__ (line 922) | def __getitem__(self, n):
method get (line 939) | def get(self) -> AnyModel | None:
class Transaction (line 950) | class Transaction:
method __init__ (line 960) | def __init__(self, db: Database):
method __enter__ (line 963) | def __enter__(self) -> Transaction:
method __exit__ (line 976) | def __exit__(
method query (line 1005) | def query(
method _handle_mutate (line 1015) | def _handle_mutate(self) -> Iterator[None]:
method mutate (line 1036) | def mutate(self, statement: str, subvals: Sequence[SQLiteType] = ()) -...
method mutate_many (line 1041) | def mutate_many(
method script (line 1050) | def script(self, statements: str):
class Migration (line 1058) | class Migration(ABC):
method name (line 1064) | def name(cls) -> str:
method with_row_factory (line 1070) | def with_row_factory(self, factory: type[NamedTuple]) -> Iterator[None]:
method migrate_model (line 1079) | def migrate_model(self, model_cls: type[Model], *args, **kwargs) -> None:
method _migrate_data (line 1087) | def _migrate_data(
class TableInfo (line 1093) | class TableInfo(TypedDict):
class Database (line 1098) | class Database:
method __init__ (line 1118) | def __init__(self, path, timeout: float = 5.0):
method db_tables (line 1162) | def db_tables(self) -> dict[str, TableInfo]:
method _connection (line 1189) | def _connection(self) -> Connection:
method _create_connection (line 1207) | def _create_connection(self) -> Connection:
method add_functions (line 1247) | def add_functions(self, conn):
method _close (line 1277) | def _close(self):
method _tx_stack (line 1288) | def _tx_stack(self) -> Generator[list[Transaction]]:
method transaction (line 1302) | def transaction(self) -> Transaction:
method load_extension (line 1308) | def load_extension(self, path: str):
method _make_table (line 1323) | def _make_table(self, table: str, fields: Mapping[str, types.Type]):
method _make_attribute_table (line 1347) | def _make_attribute_table(self, flex_table: str):
method _create_indices (line 1363) | def _create_indices(
method _ensure_migration_state_table (line 1378) | def _ensure_migration_state_table(self) -> None:
method _migrate (line 1388) | def _migrate(self) -> None:
method migration_exists (line 1397) | def migration_exists(self, name: str, table: str) -> bool:
method record_migration (line 1401) | def record_migration(self, name: str, table: str) -> None:
method _fetch (line 1411) | def _fetch(
method _get (line 1469) | def _get(self, model_cls: type[AnyModel], id_: int) -> AnyModel | None:
class Index (line 1474) | class Index(NamedTuple):
FILE: beets/dbcore/query.py
class ParsingError (line 50) | class ParsingError(ValueError):
class InvalidQueryError (line 56) | class InvalidQueryError(ParsingError):
method __init__ (line 62) | def __init__(self, query, explanation):
class InvalidQueryArgumentValueError (line 69) | class InvalidQueryArgumentValueError(ParsingError):
method __init__ (line 76) | def __init__(self, what, expected, detail=None):
class Query (line 83) | class Query(ABC):
method field_names (line 87) | def field_names(self) -> set[str]:
method clause (line 92) | def clause(self) -> tuple[str | None, Sequence[Any]]:
method match (line 104) | def match(self, obj: Model):
method __and__ (line 109) | def __and__(self, other: Query) -> AndQuery:
method __repr__ (line 112) | def __repr__(self) -> str:
method __eq__ (line 115) | def __eq__(self, other) -> bool:
method __hash__ (line 118) | def __hash__(self) -> int:
class FieldQuery (line 132) | class FieldQuery(Query, Generic[P]):
method field (line 141) | def field(self) -> str:
method field_names (line 147) | def field_names(self) -> set[str]:
method __init__ (line 151) | def __init__(self, field_name: str, pattern: P, fast: bool = True):
method col_clause (line 156) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method clause (line 159) | def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
method value_match (line 167) | def value_match(cls, pattern: P, value: Any):
method match (line 171) | def match(self, obj: Model) -> bool:
method __repr__ (line 174) | def __repr__(self) -> str:
method __eq__ (line 180) | def __eq__(self, other) -> bool:
method __hash__ (line 187) | def __hash__(self) -> int:
class MatchQuery (line 191) | class MatchQuery(FieldQuery[AnySQLiteType]):
method col_clause (line 194) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method value_match (line 198) | def value_match(cls, pattern: AnySQLiteType, value: Any) -> bool:
class NoneQuery (line 202) | class NoneQuery(FieldQuery[None]):
method __init__ (line 205) | def __init__(self, field, fast: bool = True):
method col_clause (line 208) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method match (line 211) | def match(self, obj: Model) -> bool:
method __repr__ (line 214) | def __repr__(self) -> str:
class StringFieldQuery (line 218) | class StringFieldQuery(FieldQuery[P]):
method value_match (line 224) | def value_match(cls, pattern: P, value: Any):
method string_match (line 231) | def string_match(
class StringQuery (line 242) | class StringQuery(StringFieldQuery[str]):
method col_clause (line 245) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method string_match (line 256) | def string_match(cls, pattern: str, value: str) -> bool:
class SubstringQuery (line 260) | class SubstringQuery(StringFieldQuery[str]):
method col_clause (line 263) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method string_match (line 275) | def string_match(cls, pattern: str, value: str) -> bool:
class PathQuery (line 279) | class PathQuery(FieldQuery[bytes]):
method __init__ (line 287) | def __init__(self, field: str, pattern: bytes, fast: bool = True) -> N...
method dir_path (line 309) | def dir_path(self) -> bytes:
method is_path_query (line 313) | def is_path_query(query_part: str) -> bool:
method match (line 329) | def match(self, obj: Model) -> bool:
method col_clause (line 339) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method __repr__ (line 357) | def __repr__(self) -> str:
class RegexpQuery (line 364) | class RegexpQuery(StringFieldQuery[Pattern[str]]):
method __init__ (line 371) | def __init__(self, field_name: str, pattern: str, fast: bool = True):
method col_clause (line 383) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method _normalize (line 387) | def _normalize(s: str) -> str:
method string_match (line 394) | def string_match(cls, pattern: Pattern[str], value: str) -> bool:
class BooleanQuery (line 398) | class BooleanQuery(MatchQuery[int]):
method __init__ (line 403) | def __init__(
class NumericQuery (line 417) | class NumericQuery(FieldQuery[str]):
method _convert (line 426) | def _convert(self, s: str) -> float | int | None:
method __init__ (line 443) | def __init__(self, field_name: str, pattern: str, fast: bool = True):
method match (line 458) | def match(self, obj: Model) -> bool:
method col_clause (line 474) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
class InQuery (line 491) | class InQuery(Generic[AnySQLiteType], FieldQuery[Sequence[AnySQLiteType]]):
method subvals (line 499) | def subvals(self) -> Sequence[SQLiteType]:
method col_clause (line 502) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
method value_match (line 507) | def value_match(
class CollectionQuery (line 513) | class CollectionQuery(Query):
method field_names (line 519) | def field_names(self) -> set[str]:
method __init__ (line 523) | def __init__(self, subqueries: Sequence[Query] = ()):
method __len__ (line 528) | def __len__(self) -> int:
method __getitem__ (line 531) | def __getitem__(self, key):
method __iter__ (line 534) | def __iter__(self) -> Iterator[Query]:
method __contains__ (line 537) | def __contains__(self, subq) -> bool:
method clause_with_joiner (line 540) | def clause_with_joiner(
method __repr__ (line 559) | def __repr__(self) -> str:
method __eq__ (line 562) | def __eq__(self, other) -> bool:
method __hash__ (line 565) | def __hash__(self) -> int:
class MutableCollectionQuery (line 572) | class MutableCollectionQuery(CollectionQuery):
method __setitem__ (line 579) | def __setitem__(self, key, value):
method __delitem__ (line 582) | def __delitem__(self, key):
class AndQuery (line 586) | class AndQuery(MutableCollectionQuery):
method clause (line 589) | def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
method match (line 592) | def match(self, obj: Model) -> bool:
class OrQuery (line 596) | class OrQuery(MutableCollectionQuery):
method clause (line 599) | def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
method match (line 602) | def match(self, obj: Model) -> bool:
class NotQuery (line 606) | class NotQuery(Query):
method field_names (line 612) | def field_names(self) -> set[str]:
method __init__ (line 616) | def __init__(self, subquery):
method clause (line 619) | def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:
method match (line 628) | def match(self, obj: Model) -> bool:
method __repr__ (line 631) | def __repr__(self) -> str:
method __eq__ (line 634) | def __eq__(self, other) -> bool:
method __hash__ (line 637) | def __hash__(self) -> int:
class TrueQuery (line 641) | class TrueQuery(Query):
method clause (line 644) | def clause(self) -> tuple[str, Sequence[SQLiteType]]:
method match (line 647) | def match(self, obj: Model) -> bool:
class FalseQuery (line 651) | class FalseQuery(Query):
method clause (line 654) | def clause(self) -> tuple[str, Sequence[SQLiteType]]:
method match (line 657) | def match(self, obj: Model) -> bool:
function _parse_periods (line 664) | def _parse_periods(pattern: str) -> tuple[Period | None, Period | None]:
class Period (line 678) | class Period:
method __init__ (line 702) | def __init__(self, date: datetime, precision: str):
method parse (line 713) | def parse(cls: type[Period], string: str) -> Period | None:
method open_right_endpoint (line 774) | def open_right_endpoint(self) -> datetime:
class DateInterval (line 799) | class DateInterval:
method __init__ (line 806) | def __init__(self, start: datetime | None, end: datetime | None):
method from_periods (line 813) | def from_periods(
method contains (line 823) | def contains(self, date: datetime) -> bool:
method __str__ (line 830) | def __str__(self) -> str:
class DateQuery (line 834) | class DateQuery(FieldQuery[str]):
method __init__ (line 844) | def __init__(self, field_name: str, pattern: str, fast: bool = True):
method match (line 849) | def match(self, obj: Model) -> bool:
method col_clause (line 856) | def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:
class DurationQuery (line 879) | class DurationQuery(NumericQuery):
method _convert (line 888) | def _convert(self, s: str) -> float | None:
class SingletonQuery (line 907) | class SingletonQuery(FieldQuery[str]):
method __new__ (line 918) | def __new__(cls, field: str, value: str, *args, **kwargs):
class Sort (line 928) | class Sort:
method order_clause (line 933) | def order_clause(self) -> str | None:
method sort (line 939) | def sort(self, items: list[AnyModel]) -> list[AnyModel]:
method is_slow (line 943) | def is_slow(self) -> bool:
method __hash__ (line 949) | def __hash__(self) -> int:
method __eq__ (line 952) | def __eq__(self, other) -> bool:
method __repr__ (line 955) | def __repr__(self):
class MultipleSort (line 959) | class MultipleSort(Sort):
method __init__ (line 962) | def __init__(self, sorts: list[Sort] | None = None):
method add_sort (line 965) | def add_sort(self, sort: Sort):
method order_clause (line 968) | def order_clause(self) -> str:
method is_slow (line 986) | def is_slow(self) -> bool:
method sort (line 992) | def sort(self, items):
method __repr__ (line 1008) | def __repr__(self):
method __hash__ (line 1011) | def __hash__(self):
method __eq__ (line 1014) | def __eq__(self, other):
class FieldSort (line 1018) | class FieldSort(Sort):
method __init__ (line 1023) | def __init__(
method sort (line 1033) | def sort(self, objs: list[AnyModel]) -> list[AnyModel]:
method __repr__ (line 1053) | def __repr__(self) -> str:
method __hash__ (line 1059) | def __hash__(self) -> int:
method __eq__ (line 1062) | def __eq__(self, other) -> bool:
class FixedFieldSort (line 1070) | class FixedFieldSort(FieldSort):
method order_clause (line 1073) | def order_clause(self) -> str:
class SlowFieldSort (line 1087) | class SlowFieldSort(FieldSort):
method is_slow (line 1092) | def is_slow(self) -> bool:
class NullSort (line 1096) | class NullSort(Sort):
method sort (line 1099) | def sort(self, items: list[AnyModel]) -> list[AnyModel]:
method __nonzero__ (line 1102) | def __nonzero__(self) -> bool:
method __bool__ (line 1105) | def __bool__(self) -> bool:
method __eq__ (line 1108) | def __eq__(self, other) -> bool:
method __hash__ (line 1111) | def __hash__(self) -> int:
class SmartArtistSort (line 1115) | class SmartArtistSort(FieldSort):
method order_clause (line 1120) | def order_clause(self):
method sort (line 1127) | def sort(self, objs: list[AnyModel]) -> list[AnyModel]:
FILE: beets/dbcore/queryparse.py
function parse_query_part (line 46) | def parse_query_part(
function construct_query_part (line 116) | def construct_query_part(
function query_from_strings (line 169) | def query_from_strings(
function construct_sort_part (line 187) | def construct_sort_part(
function sort_from_strings (line 218) | def sort_from_strings(
function parse_sorted_query (line 237) | def parse_sorted_query(
FILE: beets/dbcore/types.py
class ModelType (line 36) | class ModelType(typing.Protocol):
method __init__ (line 42) | def __init__(self, value: Any = None): ...
class Type (line 56) | class Type(ABC, Generic[T, N]):
method null (line 81) | def null(self) -> N:
method format (line 88) | def format(self, value: N | T) -> str:
method parse (line 102) | def parse(self, string: str) -> T | N:
method normalize (line 111) | def normalize(self, value: Any) -> T | N:
method from_sql (line 124) | def from_sql(self, sql_value: SQLiteType) -> T | N:
method to_sql (line 145) | def to_sql(self, model_value: Any) -> SQLiteType:
class Default (line 155) | class Default(Type[str, None]):
method null (line 159) | def null(self):
class BaseInteger (line 163) | class BaseInteger(Type[int, N]):
method normalize (line 170) | def normalize(self, value: Any) -> int | N:
class Integer (line 179) | class Integer(BaseInteger[int]):
method null (line 181) | def null(self) -> int:
class NullInteger (line 185) | class NullInteger(BaseInteger[None]):
method null (line 187) | def null(self) -> None:
class BasePaddedInt (line 191) | class BasePaddedInt(BaseInteger[N]):
method __init__ (line 196) | def __init__(self, digits: int):
method format (line 199) | def format(self, value: int | N) -> str:
class PaddedInt (line 203) | class PaddedInt(BasePaddedInt[int]):
class NullPaddedInt (line 207) | class NullPaddedInt(BasePaddedInt[None]):
method null (line 211) | def null(self) -> None:
class ScaledInt (line 215) | class ScaledInt(Integer):
method __init__ (line 220) | def __init__(self, unit: int, suffix: str = ""):
method format (line 224) | def format(self, value: int) -> str:
class Id (line 228) | class Id(NullInteger):
method null (line 234) | def null(self) -> None:
method __init__ (line 237) | def __init__(self, primary: bool = True):
class BaseFloat (line 242) | class BaseFloat(Type[float, N]):
method __init__ (line 251) | def __init__(self, digits: int = 1):
method format (line 254) | def format(self, value: float | N) -> str:
class Float (line 258) | class Float(BaseFloat[float]):
method null (line 262) | def null(self) -> float:
class NullFloat (line 266) | class NullFloat(BaseFloat[None]):
method null (line 270) | def null(self) -> None:
class BaseString (line 274) | class BaseString(Type[T, N]):
method normalize (line 280) | def normalize(self, value: Any) -> T | N:
class String (line 287) | class String(BaseString[str, Any]):
class DelimitedString (line 293) | class DelimitedString(BaseString[list, list]): # type: ignore[type-arg]
method __init__ (line 306) | def __init__(self, db_delimiter: str):
method format (line 309) | def format(self, value: list[str]):
method parse (line 312) | def parse(self, string: str):
method to_sql (line 323) | def to_sql(self, model_value: list[str]):
class Boolean (line 327) | class Boolean(Type):
method format (line 334) | def format(self, value: bool) -> str:
method parse (line 337) | def parse(self, string: str) -> bool:
class DateType (line 341) | class DateType(Float):
method format (line 346) | def format(self, value):
method parse (line 351) | def parse(self, string):
class BasePathType (line 365) | class BasePathType(Type[bytes, N]):
method parse (line 376) | def parse(self, string: str) -> bytes:
method normalize (line 379) | def normalize(self, value: Any) -> bytes | N:
method from_sql (line 391) | def from_sql(self, sql_value):
method to_sql (line 394) | def to_sql(self, value: bytes) -> BLOB_TYPE:
class NullPathType (line 400) | class NullPathType(BasePathType[None]):
method null (line 402) | def null(self) -> None:
method format (line 405) | def format(self, value: bytes | None) -> str:
class PathType (line 409) | class PathType(BasePathType[bytes]):
method null (line 411) | def null(self) -> bytes:
method format (line 414) | def format(self, value: bytes) -> str:
class MusicalKey (line 418) | class MusicalKey(String):
method parse (line 434) | def parse(self, key):
method normalize (line 442) | def normalize(self, key):
class DurationType (line 449) | class DurationType(Float):
method format (line 454) | def format(self, value):
method parse (line 460) | def parse(self, string):
FILE: beets/importer/session.py
class ImportAbortError (line 42) | class ImportAbortError(Exception):
class ImportSession (line 48) | class ImportSession:
method __init__ (line 61) | def __init__(
method _setup_logging (line 92) | def _setup_logging(self, loghandler: logging.Handler | None):
method set_config (line 100) | def set_config(self, config):
method tag_log (line 152) | def tag_log(self, status, paths: Sequence[PathBytes]):
method log_choice (line 158) | def log_choice(self, task: ImportTask, duplicate=False):
method should_resume (line 179) | def should_resume(self, path: PathBytes):
method choose_match (line 182) | def choose_match(self, task: ImportTask):
method resolve_duplicate (line 185) | def resolve_duplicate(self, task: ImportTask, found_duplicates):
method choose_item (line 188) | def choose_item(self, task: ImportTask):
method run (line 191) | def run(self):
method already_imported (line 246) | def already_imported(self, toppath: PathBytes, paths: Sequence[PathByt...
method history_dirs (line 262) | def history_dirs(self) -> set[tuple[PathBytes, ...]]:
method already_merged (line 268) | def already_merged(self, paths: Sequence[PathBytes]):
method mark_merged (line 277) | def mark_merged(self, paths: Sequence[PathBytes]):
method is_resuming (line 286) | def is_resuming(self, toppath: PathBytes):
method ask_resume (line 293) | def ask_resume(self, toppath: PathBytes):
FILE: beets/importer/stages.py
function read_tasks (line 46) | def read_tasks(session: ImportSession):
function query_tasks (line 70) | def query_tasks(session: ImportSession):
function group_albums (line 102) | def group_albums(session: ImportSession):
function lookup_candidates (line 130) | def lookup_candidates(session: ImportSession, task: ImportTask):
function user_query (line 150) | def user_query(session: ImportSession, task: ImportTask):
function import_asis (line 222) | def import_asis(session: ImportSession, task: ImportTask):
function plugin_stage (line 238) | def plugin_stage(
function log_files (line 259) | def log_files(session: ImportSession, task: ImportTask):
function manipulate_files (line 276) | def manipulate_files(session: ImportSession, task: ImportTask):
function _apply_choice (line 314) | def _apply_choice(session: ImportSession, task: ImportTask):
function _resolve_duplicates (line 337) | def _resolve_duplicates(session: ImportSession, task: ImportTask):
function _freshen_items (line 377) | def _freshen_items(items):
function _extend_pipeline (line 385) | def _extend_pipeline(tasks, *stages):
FILE: beets/importer/state.py
class ImportState (line 35) | class ImportState:
method __init__ (line 64) | def __init__(self, readonly=False, path: PathBytes | None = None):
method __enter__ (line 70) | def __enter__(self):
method __exit__ (line 73) | def __exit__(self, exc_type, exc_val, exc_tb):
method _open (line 76) | def _open(
method _save (line 92) | def _save(self):
method progress_add (line 107) | def progress_add(self, toppath: PathBytes, *paths: PathBytes):
method progress_has_element (line 119) | def progress_has_element(self, toppath: PathBytes, path: PathBytes) ->...
method progress_has (line 125) | def progress_has(self, toppath: PathBytes) -> bool:
method progress_reset (line 131) | def progress_reset(self, toppath: PathBytes | None):
method history_add (line 139) | def history_add(self, paths: list[PathBytes]):
FILE: beets/importer/tasks.py
class ImportAbortError (line 72) | class ImportAbortError(Exception):
class Action (line 78) | class Action(Enum):
class BaseImportTask (line 92) | class BaseImportTask:
method __init__ (line 102) | def __init__(
class ImportTask (line 129) | class ImportTask(BaseImportTask):
method __init__ (line 170) | def __init__(
method set_choice (line 181) | def set_choice(
method save_progress (line 207) | def save_progress(self):
method save_history (line 214) | def save_history(self):
method apply (line 221) | def apply(self):
method skip (line 225) | def skip(self):
method chosen_info (line 230) | def chosen_info(self):
method imported_items (line 243) | def imported_items(self):
method apply_metadata (line 258) | def apply_metadata(self):
method duplicate_items (line 266) | def duplicate_items(self, lib: library.Library):
method remove_duplicates (line 272) | def remove_duplicates(self, lib: library.Library):
method set_fields (line 282) | def set_fields(self, lib: library.Library):
method finalize (line 303) | def finalize(self, session: ImportSession):
method cleanup (line 323) | def cleanup(self, copy=False, delete=False, move=False):
method _emit_imported (line 345) | def _emit_imported(self, lib: library.Library):
method handle_created (line 348) | def handle_created(self, session: ImportSession):
method lookup_candidates (line 362) | def lookup_candidates(self, search_ids: list[str]) -> None:
method find_duplicates (line 372) | def find_duplicates(self, lib: library.Library) -> list[library.Album]:
method align_album_level_fields (line 405) | def align_album_level_fields(self):
method manipulate_files (line 446) | def manipulate_files(
method add (line 494) | def add(self, lib: library.Library):
method record_replaced (line 514) | def record_replaced(self, lib: library.Library):
method reimport_metadata (line 537) | def reimport_metadata(self, lib: library.Library):
method remove_replaced (line 615) | def remove_replaced(self, lib):
method choose_match (line 629) | def choose_match(self, session):
method reload (line 635) | def reload(self):
method prune (line 643) | def prune(self, filename):
class SingletonImportTask (line 658) | class SingletonImportTask(ImportTask):
method __init__ (line 661) | def __init__(self, toppath: util.PathBytes | None, item: library.Item):
method chosen_info (line 667) | def chosen_info(self):
method imported_items (line 679) | def imported_items(self):
method apply_metadata (line 682) | def apply_metadata(self):
method _emit_imported (line 687) | def _emit_imported(self, lib):
method lookup_candidates (line 691) | def lookup_candidates(self, search_ids: list[str]) -> None:
method find_duplicates (line 696) | def find_duplicates(self, lib: library.Library) -> list[library.Item]:...
method add (line 719) | def add(self, lib):
method infer_album_fields (line 726) | def infer_album_fields(self):
method choose_match (line 729) | def choose_match(self, session: ImportSession):
method reload (line 735) | def reload(self):
method set_fields (line 738) | def set_fields(self, lib):
class SentinelImportTask (line 757) | class SentinelImportTask(ImportTask):
method __init__ (line 766) | def __init__(self, toppath, paths):
method save_history (line 773) | def save_history(self):
method save_progress (line 776) | def save_progress(self):
method skip (line 785) | def skip(self) -> bool:
method set_choice (line 788) | def set_choice(self, choice):
method cleanup (line 791) | def cleanup(self, copy=False, delete=False, move=False):
method _emit_imported (line 794) | def _emit_imported(self, lib):
class ArchiveImportTask (line 803) | class ArchiveImportTask(SentinelImportTask):
method __init__ (line 816) | def __init__(self, toppath):
method is_archive (line 821) | def is_archive(cls, path):
method handlers (line 834) | def handlers(cls) -> list[ArchiveHandler]:
method cleanup (line 864) | def cleanup(self, copy=False, delete=False, move=False):
method extract (line 873) | def extract(self):
class ImportTaskFactory (line 907) | class ImportTaskFactory:
method __init__ (line 912) | def __init__(self, toppath: util.PathBytes, session: ImportSession):
method tasks (line 925) | def tasks(self) -> Iterable[ImportTask]:
method _create (line 963) | def _create(self, task: ImportTask | None):
method paths (line 976) | def paths(self):
method singleton (line 996) | def singleton(self, path: util.PathBytes):
method album (line 1012) | def album(self, paths: Iterable[util.PathBytes], dirs=None):
method sentinel (line 1039) | def sentinel(self, paths: Iterable[util.PathBytes] | None = None):
method unarchive (line 1045) | def unarchive(self):
method read_item (line 1074) | def read_item(self, path: util.PathBytes):
function is_subdir_of_any_in_list (line 1098) | def is_subdir_of_any_in_list(path, dirs):
function albums_in_dir (line 1106) | def albums_in_dir(path: util.PathBytes):
FILE: beets/library/__init__.py
function __getattr__ (line 15) | def __getattr__(name: str):
FILE: beets/library/exceptions.py
class FileOperationError (line 4) | class FileOperationError(Exception):
method __init__ (line 11) | def __init__(self, path, reason):
method __str__ (line 19) | def __str__(self):
class ReadError (line 27) | class ReadError(FileOperationError):
method __str__ (line 30) | def __str__(self):
class WriteError (line 34) | class WriteError(FileOperationError):
method __str__ (line 37) | def __str__(self):
FILE: beets/library/library.py
class Library (line 19) | class Library(dbcore.Database):
method __init__ (line 28) | def __init__(
method add (line 48) | def add(self, obj):
method add_album (line 58) | def add_album(self, items):
method _fetch (line 87) | def _fetch(self, model_cls, query, sort=None):
method get_default_album_sort (line 111) | def get_default_album_sort():
method get_default_item_sort (line 118) | def get_default_item_sort():
method albums (line 124) | def albums(self, query=None, sort=None) -> Results[Album]:
method items (line 128) | def items(self, query=None, sort=None) -> Results[Item]:
method get_item (line 133) | def get_item(self, id_: int) -> Item | None:
method get_album (line 140) | def get_album(self, item_or_id: Item | int) -> Album | None:
FILE: beets/library/migrations.py
class GenreRow (line 24) | class GenreRow(NamedTuple):
function chunks (line 30) | def chunks(lst: list[T], n: int) -> Iterator[list[T]]:
class MultiGenreFieldMigration (line 36) | class MultiGenreFieldMigration(Migration):
method separators (line 40) | def separators(self) -> list[str]:
method get_genres (line 49) | def get_genres(self, genre: str) -> str:
method _migrate_data (line 57) | def _migrate_data(
class LyricsRow (line 101) | class LyricsRow(NamedTuple):
class LyricsMetadataInFlexFieldsMigration (line 106) | class LyricsMetadataInFlexFieldsMigration(Migration):
method _migrate_data (line 109) | def _migrate_data(self, model_cls: type[Model], _: set[str]) -> None:
FILE: beets/library/models.py
class LibModel (line 37) | class LibModel(dbcore.Model["Library"]):
method _types (line 46) | def _types(cls) -> dict[str, types.Type]:
method _queries (line 54) | def _queries(cls) -> dict[str, FieldQueryType]:
method writable_media_fields (line 58) | def writable_media_fields(cls) -> set[str]:
method filepath (line 62) | def filepath(self) -> Path:
method _template_funcs (line 66) | def _template_funcs(self):
method store (line 71) | def store(self, fields=None):
method remove (line 75) | def remove(self):
method add (line 79) | def add(self, lib=None):
method __format__ (line 84) | def __format__(self, spec):
method __str__ (line 90) | def __str__(self):
method __bytes__ (line 93) | def __bytes__(self):
method field_query (line 99) | def field_query(
method any_field_query (line 113) | def any_field_query(cls, *args, **kwargs) -> dbcore.OrQuery:
method any_writable_media_field_query (line 119) | def any_writable_media_field_query(cls, *args, **kwargs) -> dbcore.OrQ...
method duplicates_query (line 125) | def duplicates_query(self, fields: list[str]) -> dbcore.AndQuery:
class FormattedItemMapping (line 135) | class FormattedItemMapping(dbcore.db.FormattedMapping):
method __init__ (line 143) | def __init__(self, item, included_keys=ALL_KEYS, for_path=False):
method all_keys (line 156) | def all_keys(self):
method album_keys (line 160) | def album_keys(self):
method album (line 176) | def album(self):
method _get (line 179) | def _get(self, key):
method __getitem__ (line 193) | def __getitem__(self, key):
method __iter__ (line 214) | def __iter__(self):
method __len__ (line 217) | def __len__(self):
class Album (line 221) | class Album(LibModel):
method _types (line 282) | def _types(cls) -> dict[str, types.Type]:
method _relation (line 338) | def _relation(cls) -> type[Item]:
method relation_join (line 342) | def relation_join(cls) -> str:
method art_filepath (line 354) | def art_filepath(self) -> Path | None:
method _getters (line 359) | def _getters(cls):
method items (line 367) | def items(self):
method remove (line 378) | def remove(self, delete=False, with_items=True):
method move_art (line 404) | def move_art(self, operation=MoveOperation.MOVE):
method move (line 450) | def move(self, operation=MoveOperation.MOVE, basedir=None, store=True):
method item_dir (line 480) | def item_dir(self):
method _albumtotal (line 489) | def _albumtotal(self):
method art_destination (line 509) | def art_destination(self, image, item_dir=None):
method set_art (line 539) | def set_art(self, path, copy=True):
method store (line 571) | def store(self, fields=None, inherit=True):
method try_sync (line 607) | def try_sync(self, write, move, inherit=True):
method length (line 621) | def length(self) -> float: # type: ignore[override] # still writable ...
class Item (line 626) | class Item(LibModel):
method _queries (line 759) | def _queries(cls) -> dict[str, FieldQueryType]:
method _relation (line 768) | def _relation(cls) -> type[Album]:
method relation_join (line 772) | def relation_join(cls) -> str:
method _cached_album (line 784) | def _cached_album(self):
method _cached_album (line 800) | def _cached_album(self, album):
method _getters (line 804) | def _getters(cls):
method duplicates_query (line 810) | def duplicates_query(self, fields: list[str]) -> dbcore.AndQuery:
method from_path (line 817) | def from_path(cls, path):
method __setitem__ (line 825) | def __setitem__(self, key, value):
method __getitem__ (line 841) | def __getitem__(self, key):
method __repr__ (line 854) | def __repr__(self):
method keys (line 863) | def keys(self, computed=False, with_album=True):
method get (line 875) | def get(self, key, default=None, with_album=True):
method update (line 888) | def update(self, values):
method clear (line 897) | def clear(self):
method get_album (line 902) | def get_album(self):
method read (line 913) | def read(self, read_path=None):
method write (line 944) | def write(self, path=None, tags=None, id3v23=None):
method try_write (line 996) | def try_write(self, *args, **kwargs):
method try_sync (line 1009) | def try_sync(self, write, move, with_album=True):
method move_file (line 1032) | def move_file(self, dest, operation=MoveOperation.MOVE):
method current_mtime (line 1084) | def current_mtime(self):
method try_filesize (line 1090) | def try_filesize(self):
method remove (line 1103) | def remove(self, delete=False, with_album=True):
method move (line 1129) | def move(
method destination (line 1182) | def destination(
function _int_arg (line 1255) | def _int_arg(s):
class DefaultTemplateFunctions (line 1264) | class DefaultTemplateFunctions:
method _func_names (line 1276) | def _func_names(cls) -> list[str]:
method __init__ (line 1280) | def __init__(self, item=None, lib=None):
method functions (line 1289) | def functions(self):
method tmpl_lower (line 1302) | def tmpl_lower(s):
method tmpl_upper (line 1307) | def tmpl_upper(s):
method tmpl_capitalize (line 1312) | def tmpl_capitalize(s):
method tmpl_title (line 1317) | def tmpl_title(s):
method tmpl_left (line 1322) | def tmpl_left(s, chars):
method tmpl_right (line 1327) | def tmpl_right(s, chars):
method tmpl_if (line 1332) | def tmpl_if(condition, trueval, falseval=""):
method tmpl_asciify (line 1350) | def tmpl_asciify(s):
method tmpl_time (line 1355) | def tmpl_time(s, fmt):
method tmpl_aunique (line 1360) | def tmpl_aunique(self, keys=None, disam=None, bracket=None):
method tmpl_sunique (line 1402) | def tmpl_sunique(self, keys=None, disam=None, bracket=None):
method _tmpl_unique_memokey (line 1437) | def _tmpl_unique_memokey(self, name, keys, disam, item_id):
method _tmpl_unique (line 1443) | def _tmpl_unique(
method tmpl_first (line 1541) | def tmpl_first(s, count=1, skip=0, sep="; ", join_str="; "):
method tmpl_ifdef (line 1556) | def tmpl_ifdef(self, field, trueval="", falseval=""):
FILE: beets/library/queries.py
function parse_query_parts (line 17) | def parse_query_parts(parts, model_cls):
function parse_query_string (line 49) | def parse_query_string(s, model_cls):
FILE: beets/logging.py
function _logsafe (line 81) | def _logsafe(val: T) -> str | T:
class StrFormatLogger (line 105) | class StrFormatLogger(Logger):
class _LogMessage (line 117) | class _LogMessage:
method __init__ (line 118) | def __init__(
method __str__ (line 128) | def __str__(self):
method _log (line 133) | def _log(
class ThreadLocalLevelLogger (line 160) | class ThreadLocalLevelLogger(Logger):
method __init__ (line 163) | def __init__(self, name, level=NOTSET):
method level (line 169) | def level(self):
method level (line 177) | def level(self, value):
method set_global_level (line 180) | def set_global_level(self, level):
class BeetsLogger (line 188) | class BeetsLogger(ThreadLocalLevelLogger, StrFormatLogger):
method extra_debug (line 191) | def extra_debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
function getLogger (line 209) | def getLogger(name: str) -> BeetsLogger: ...
function getLogger (line 211) | def getLogger(name: None = ...) -> RootLogger: ...
function getLogger (line 212) | def getLogger(name=None) -> BeetsLogger | RootLogger: # noqa: N802
FILE: beets/metadata_plugins.py
function find_metadata_source_plugins (line 45) | def find_metadata_source_plugins() -> list[MetadataSourcePlugin]:
function get_metadata_source (line 53) | def get_metadata_source(name: str) -> MetadataSourcePlugin | None:
function maybe_handle_plugin_error (line 61) | def maybe_handle_plugin_error(plugin: MetadataSourcePlugin, method_name:...
function _yield_from_plugins (line 75) | def _yield_from_plugins(
function candidates (line 92) | def candidates(*args, **kwargs) -> Iterator[AlbumInfo]:
function item_candidates (line 98) | def item_candidates(*args, **kwargs) -> Iterator[TrackInfo]:
function albums_for_ids (line 104) | def albums_for_ids(*args, **kwargs) -> Iterator[AlbumInfo]:
function tracks_for_ids (line 110) | def tracks_for_ids(*args, **kwargs) -> Iterator[TrackInfo]:
function album_for_id (line 114) | def album_for_id(_id: str, data_source: str) -> AlbumInfo | None:
function track_for_id (line 125) | def track_for_id(_id: str, data_source: str) -> TrackInfo | None:
function get_penalty (line 137) | def get_penalty(data_source: str | None) -> float:
class MetadataSourcePlugin (line 149) | class MetadataSourcePlugin(BeetsPlugin, metaclass=abc.ABCMeta):
method data_source (line 160) | def data_source(cls) -> str:
method data_source_mismatch_penalty (line 168) | def data_source_mismatch_penalty(self) -> float:
method __init__ (line 174) | def __init__(self, *args, **kwargs) -> None:
method album_for_id (line 184) | def album_for_id(self, album_id: str) -> AlbumInfo | None:
method track_for_id (line 190) | def track_for_id(self, track_id: str) -> TrackInfo | None:
method candidates (line 199) | def candidates(
method item_candidates (line 218) | def item_candidates(
method albums_for_ids (line 231) | def albums_for_ids(self, ids: Iterable[str]) -> Iterable[AlbumInfo | N...
method tracks_for_ids (line 242) | def tracks_for_ids(self, ids: Iterable[str]) -> Iterable[TrackInfo | N...
method _extract_id (line 253) | def _extract_id(self, url: str) -> str | None:
method get_artist (line 262) | def get_artist(
class IDResponse (line 310) | class IDResponse(TypedDict):
class SearchParams (line 316) | class SearchParams(NamedTuple):
class SearchApiMetadataSourcePlugin (line 332) | class SearchApiMetadataSourcePlugin(
method __init__ (line 344) | def __init__(self, *args, **kwargs) -> None:
method get_search_query_with_filters (line 353) | def get_search_query_with_filters(
method get_search_response (line 376) | def get_search_response(self, params: SearchParams) -> Sequence[R]:
method _search_api (line 388) | def _search_api(
method _get_candidates (line 417) | def _get_candidates(
method candidates (line 427) | def candidates(
method item_candidates (line 437) | def item_candidates(
FILE: beets/plugins.py
class PluginConflictError (line 104) | class PluginConflictError(Exception):
class PluginImportError (line 112) | class PluginImportError(ImportError):
method __init__ (line 119) | def __init__(self, name: str):
class PluginLogFilter (line 123) | class PluginLogFilter(logging.Filter):
method __init__ (line 128) | def __init__(self, plugin):
method filter (line 131) | def filter(self, record):
class BeetsPluginMeta (line 143) | class BeetsPluginMeta(abc.ABCMeta):
class BeetsPlugin (line 149) | class BeetsPlugin(metaclass=BeetsPluginMeta):
method __init_subclass__ (line 169) | def __init_subclass__(cls) -> None:
method __init__ (line 223) | def __init__(self, name: str | None = None):
method _verify_config (line 247) | def _verify_config(self, *_, **__) -> None:
method commands (line 276) | def commands(self) -> Sequence[Subcommand]:
method _set_stage_log_level (line 282) | def _set_stage_log_level(
method get_early_import_stages (line 292) | def get_early_import_stages(self) -> list[ImportStageFunc]:
method get_import_stages (line 302) | def get_import_stages(self) -> list[ImportStageFunc]:
method _set_log_level_and_params (line 312) | def _set_log_level_and_params(
method queries (line 341) | def queries(self) -> dict[str, type[Query]]:
method add_media_field (line 345) | def add_media_field(
method register_listener (line 361) | def register_listener(self, event: EventType, func: Listener) -> None:
method template_func (line 370) | def template_func(cls, name: str) -> Callable[[TFunc[str]], TFunc[str]]:
method template_field (line 383) | def template_field(cls, name: str) -> Callable[[TFunc[Item]], TFunc[It...
function get_plugin_names (line 397) | def get_plugin_names() -> list[str]:
function _get_plugin (line 438) | def _get_plugin(name: str) -> BeetsPlugin | None:
function load_plugins (line 481) | def load_plugins() -> None:
function find_plugins (line 496) | def find_plugins() -> Iterable[BeetsPlugin]:
function commands (line 503) | def commands() -> list[Subcommand]:
function queries (line 511) | def queries() -> dict[str, type[Query]]:
function types (line 521) | def types(model_cls: type[AnyModel]) -> dict[str, Type]:
function named_queries (line 538) | def named_queries(model_cls: type[AnyModel]) -> dict[str, FieldQueryType]:
function notify_info_yielded (line 548) | def notify_info_yielded(
function template_funcs (line 572) | def template_funcs() -> TFuncMap[str]:
function early_import_stages (line 582) | def early_import_stages() -> list[ImportStageFunc]:
function import_stages (line 590) | def import_stages() -> list[ImportStageFunc]:
function _check_conflicts_and_merge (line 603) | def _check_conflicts_and_merge(
function item_field_getters (line 620) | def item_field_getters() -> TFuncMap[Item]:
function album_field_getters (line 630) | def album_field_getters() -> TFuncMap[Album]:
function send (line 641) | def send(event: EventType, **arguments: Any) -> list[Any]:
function feat_tokens (line 657) | def feat_tokens(
function apply_item_changes (line 675) | def apply_item_changes(
FILE: beets/test/_common.py
function item (line 72) | def item(lib=None, **kwargs):
function import_session (line 115) | def import_session(lib=None, loghandler=None, paths=[], query=[], cli=Fa...
class InputError (line 127) | class InputError(IOError):
method __str__ (line 128) | def __str__(self) -> str:
class DummyIn (line 132) | class DummyIn:
method __init__ (line 135) | def __init__(self) -> None:
method add (line 138) | def add(self, s: str) -> None:
method close (line 141) | def close(self) -> None:
method readline (line 144) | def readline(self) -> str:
class DummyIO (line 151) | class DummyIO:
method __init__ (line 154) | def __init__(
method addinput (line 164) | def addinput(self, text: str) -> None:
method getoutput (line 168) | def getoutput(self) -> str:
function touch (line 181) | def touch(path):
class Bag (line 185) | class Bag:
method __init__ (line 191) | def __init__(self, **fields):
method __getattr__ (line 194) | def __getattr__(self, key):
function platform_windows (line 202) | def platform_windows():
function platform_posix (line 214) | def platform_posix():
function system_mock (line 226) | def system_mock(name):
function slow_test (line 237) | def slow_test(unused=None):
FILE: beets/test/helper.py
class LogCapture (line 63) | class LogCapture(logging.Handler):
method __init__ (line 64) | def __init__(self):
method emit (line 68) | def emit(self, record):
function capture_log (line 73) | def capture_log(logger="beets"):
function has_program (line 83) | def has_program(cmd, args=["--version"]):
function check_reflink_support (line 99) | def check_reflink_support(path: str) -> bool:
class ConfigMixin (line 108) | class ConfigMixin:
method config (line 110) | def config(self) -> beets.IncludeLazyConfig:
class RunMixin (line 128) | class RunMixin:
method run_command (line 129) | def run_command(self, *args, **kwargs):
class IOMixin (line 143) | class IOMixin(RunMixin):
method run_with_output (line 146) | def run_with_output(self, *args):
class TestHelper (line 152) | class TestHelper(RunMixin, ConfigMixin):
method temp_dir_path (line 166) | def temp_dir_path(self) -> Path:
method temp_dir (line 170) | def temp_dir(self) -> bytes:
method lib_path (line 174) | def lib_path(self) -> Path:
method libdir (line 180) | def libdir(self) -> bytes:
method setup_beets (line 185) | def setup_beets(self):
method teardown_beets (line 223) | def teardown_beets(self):
method create_item (line 230) | def create_item(self, **values):
method add_item (line 259) | def add_item(self, **values):
method add_item_fixture (line 281) | def add_item_fixture(self, **values):
method add_album (line 293) | def add_album(self, **values):
method add_item_fixtures (line 297) | def add_item_fixtures(self, ext="mp3", count=1):
method add_album_fixture (line 314) | def add_album_fixture(
method create_mediafile_fixture (line 341) | def create_mediafile_fixture(self, ext="mp3", images=[], target_dir=No...
method create_temp_dir (line 371) | def create_temp_dir(self, **kwargs) -> str:
method remove_temp_dir (line 374) | def remove_temp_dir(self):
method touch (line 378) | def touch(self, path, dir=None, content=""):
class BeetsTestCase (line 402) | class BeetsTestCase(unittest.TestCase, TestHelper):
method setUp (line 410) | def setUp(self):
method tearDown (line 413) | def tearDown(self):
class ItemInDBTestCase (line 417) | class ItemInDBTestCase(BeetsTestCase):
method setUp (line 422) | def setUp(self):
class PluginMixin (line 427) | class PluginMixin(ConfigMixin):
method setup_beets (line 431) | def setup_beets(self):
method teardown_beets (line 436) | def teardown_beets(self):
method register_plugin (line 440) | def register_plugin(
method load_plugins (line 445) | def load_plugins(self, *plugins: str) -> None:
method unload_plugins (line 456) | def unload_plugins(self) -> None:
method configure_plugin (line 465) | def configure_plugin(self, config: Any):
class PluginTestCase (line 474) | class PluginTestCase(PluginMixin, BeetsTestCase):
class ImportHelper (line 478) | class ImportHelper(TestHelper):
method import_path (line 499) | def import_path(self) -> Path:
method import_dir (line 505) | def import_dir(self) -> bytes:
method setUp (line 508) | def setUp(self):
method prepare_track_for_import (line 517) | def prepare_track_for_import(
method prepare_album_for_import (line 542) | def prepare_album_for_import(
method prepare_albums_for_import (line 567) | def prepare_albums_for_import(self, count: int = 1) -> None:
method _get_import_session (line 574) | def _get_import_session(self, import_dir: bytes) -> ImportSession:
method setup_importer (line 582) | def setup_importer(
method setup_singleton_importer (line 589) | def setup_singleton_importer(self, **kwargs) -> ImportSession:
class AsIsImporterMixin (line 593) | class AsIsImporterMixin:
method setUp (line 594) | def setUp(self):
method run_asis_importer (line 598) | def run_asis_importer(self, **kwargs):
class ImportTestCase (line 604) | class ImportTestCase(ImportHelper, BeetsTestCase):
class ImportSessionFixture (line 608) | class ImportSessionFixture(ImportSession):
method __init__ (line 623) | def __init__(self, *args, **kwargs):
method add_choice (line 630) | def add_choice(self, choice):
method clear_choices (line 633) | def clear_choices(self):
method choose_match (line 636) | def choose_match(self, task):
method resolve_duplicate (line 655) | def resolve_duplicate(self, task, found_duplicates):
class TerminalImportSessionFixture (line 669) | class TerminalImportSessionFixture(TerminalImportSession):
method __init__ (line 670) | def __init__(self, *args, **kwargs):
method add_choice (line 677) | def add_choice(self, choice):
method clear_choices (line 680) | def clear_choices(self):
method choose_match (line 683) | def choose_match(self, task):
method choose_item (line 687) | def choose_item(self, task):
method _add_choice_input (line 691) | def _add_choice_input(self):
class TerminalImportMixin (line 713) | class TerminalImportMixin(IOMixin, ImportHelper):
method _get_import_session (line 716) | def _get_import_session(self, import_dir: bytes) -> importer.ImportSes...
class AutotagStub (line 727) | class AutotagStub:
method install (line 741) | def install(self):
method restore (line 755) | def restore(self):
method candidates (line 759) | def candidates(self, items, artist, album, va_likely):
method item_candidates (line 774) | def item_candidates(self, item, artist, title):
method _make_track_match (line 784) | def _make_track_match(self, artist, album, number):
method _make_album_match (line 793) | def _make_album_match(self, artist, album, tracks, distance=0, missing...
class AutotagImportTestCase (line 819) | class AutotagImportTestCase(ImportTestCase):
method setUp (line 822) | def setUp(self):
class FetchImageHelper (line 828) | class FetchImageHelper:
method run (line 834) | def run(self, *args, **kwargs):
method mock_response (line 849) | def mock_response(
class CleanupModulesMixin (line 877) | class CleanupModulesMixin:
method tearDownClass (line 881) | def tearDownClass(cls) -> None:
FILE: beets/ui/__init__.py
class UserError (line 71) | class UserError(Exception):
function _in_encoding (line 80) | def _in_encoding():
function _out_encoding (line 85) | def _out_encoding():
function _stream_encoding (line 90) | def _stream_encoding(stream, default="utf-8"):
function decargs (line 111) | def decargs(arglist):
function print_ (line 122) | def print_(*strings: str, end: str = "\n") -> None:
function _bool_fallback (line 150) | def _bool_fallback(a, b):
function should_write (line 160) | def should_write(write_opt=None):
function should_move (line 167) | def should_move(move_opt=None):
function input_ (line 187) | def input_(prompt=None):
function input_options (line 207) | def input_options(
function input_yn (line 383) | def input_yn(prompt, require=False):
function input_select_objects (line 395) | def input_select_objects(prompt, objs, rep, prompt_all=None):
function get_path_formats (line 433) | def get_path_formats(subview=None):
function get_replacements (line 445) | def get_replacements():
function term_width (line 460) | def term_width() -> int:
function show_model_changes (line 466) | def show_model_changes(
class CommonOptionsParser (line 498) | class CommonOptionsParser(optparse.OptionParser):
method __init__ (line 514) | def __init__(self, *args, **kwargs):
method add_album_option (line 521) | def add_album_option(self, flags=("-a", "--album")):
method _set_format (line 534) | def _set_format(
method add_path_option (line 571) | def add_path_option(self, flags=("-p", "--path")):
method add_format_option (line 591) | def add_format_option(self, flags=("-f", "--format"), target=None):
method add_all_common_options (line 621) | def add_all_common_options(self):
class Subcommand (line 637) | class Subcommand:
method __init__ (line 644) | def __init__(self, name, parser=None, help="", aliases=(), hide=False):
method print_help (line 658) | def print_help(self):
method parse_args (line 661) | def parse_args(self, args):
method root_parser (line 665) | def root_parser(self):
method root_parser (line 669) | def root_parser(self, root_parser):
class SubcommandsOptionParser (line 676) | class SubcommandsOptionParser(CommonOptionsParser):
method __init__ (line 681) | def __init__(self, *args, **kwargs):
method add_subcommand (line 701) | def add_subcommand(self, *cmds):
method format_help (line 708) | def format_help(self, formatter=None):
method _subcommand_for_name (line 760) | def _subcommand_for_name(self, name):
method parse_global_options (line 770) | def parse_global_options(self, args):
method parse_subcommand (line 783) | def parse_subcommand(self, args):
function _setup (line 807) | def _setup(
function _configure (line 831) | def _configure(options):
function _ensure_db_directory_exists (line 868) | def _ensure_db_directory_exists(path):
function _open_library (line 880) | def _open_library(config: confuse.LazyConfig) -> library.Library:
function _raw_main (line 906) | def _raw_main(args: list[str], lib=None) -> None:
function main (line 1000) | def main(args=None):
FILE: beets/ui/commands/__init__.py
function __getattr__ (line 36) | def __getattr__(name: str):
FILE: beets/ui/commands/completion.py
function print_completion (line 13) | def print_completion(*args):
function completion_script (line 44) | def completion_script(commands):
FILE: beets/ui/commands/config.py
function config_func (line 9) | def config_func(lib, opts, args):
function config_edit (line 47) | def config_edit(cli_options):
FILE: beets/ui/commands/fields.py
function _print_keys (line 8) | def _print_keys(query):
function fields_func (line 16) | def fields_func(lib, opts, args):
FILE: beets/ui/commands/help.py
class HelpCommand (line 6) | class HelpCommand(ui.Subcommand):
method __init__ (line 7) | def __init__(self):
method func (line 14) | def func(self, lib, opts, args):
FILE: beets/ui/commands/import_/__init__.py
function paths_from_logfile (line 14) | def paths_from_logfile(path):
function parse_logfiles (line 34) | def parse_logfiles(logfiles):
function import_files (line 49) | def import_files(lib, paths: list[bytes], query):
function import_func (line 81) | def import_func(lib, opts, args: list[str]):
function _store_dict (line 134) | def _store_dict(option, opt_str, value, parser):
FILE: beets/ui/commands/import_/display.py
class ChangeRepresentation (line 30) | class ChangeRepresentation:
method changed_prefix (line 42) | def changed_prefix(self) -> str:
method _indentation_config (line 46) | def _indentation_config(self) -> confuse.Subview:
method indent_header (line 50) | def indent_header(self) -> str:
method indent_detail (line 54) | def indent_detail(self) -> str:
method indent_tracklist (line 58) | def indent_tracklist(self) -> str:
method print_layout (line 61) | def print_layout(self, indent: str, left: Side, right: Side) -> None:
method show_match_header (line 65) | def show_match_header(self) -> None:
method show_match_details (line 99) | def show_match_details(self) -> None:
method make_medium_info_line (line 128) | def make_medium_info_line(self, track_info: hooks.TrackInfo) -> str:
method format_index (line 143) | def format_index(self, track_info: hooks.TrackInfo | Item) -> str:
method make_track_numbers (line 164) | def make_track_numbers(
method make_track_titles (line 187) | def make_track_titles(
method make_track_lengths (line 203) | def make_track_lengths(
method make_line (line 232) | def make_line(
method print_tracklist (line 269) | def print_tracklist(self, lines: list[tuple[Side, Side]]) -> None:
class AlbumChange (line 308) | class AlbumChange(ChangeRepresentation):
method show_match_tracks (line 311) | def show_match_tracks(self) -> None:
class TrackChange (line 366) | class TrackChange(ChangeRepresentation):
function show_change (line 372) | def show_change(
function show_item_change (line 391) | def show_item_change(item: Item, match: hooks.TrackMatch) -> None:
function disambig_string (line 402) | def disambig_string(info: hooks.Info) -> str:
function get_singleton_disambig_fields (line 417) | def get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]:
function get_album_disambig_fields (line 445) | def get_album_disambig_fields(info: hooks.AlbumInfo) -> Sequence[str]:
function dist_string (line 468) | def dist_string(dist: Distance) -> str:
function penalty_string (line 476) | def penalty_string(distance: Distance, limit: int | None = None) -> str:
FILE: beets/ui/commands/import_/session.py
class TerminalImportSession (line 21) | class TerminalImportSession(importer.ImportSession):
method choose_match (line 24) | def choose_match(self, task):
method choose_item (line 102) | def choose_item(self, task):
method resolve_duplicate (line 142) | def resolve_duplicate(self, task, found_duplicates):
method should_resume (line 206) | def should_resume(self, path):
method _get_choices (line 212) | def _get_choices(self, task):
function summarize_items (line 287) | def summarize_items(items, singleton):
function _summary_judgment (line 330) | def _summary_judgment(rec: Recommendation) -> importer.Action | None:
function choose_candidate (line 369) | def choose_candidate(
function manual_search (line 513) | def manual_search(session, task):
function manual_id (line 529) | def manual_id(session, task):
function abort_action (line 544) | def abort_action(session, task):
FILE: beets/ui/commands/list.py
function list_items (line 6) | def list_items(lib, query, album, fmt=""):
function list_func (line 18) | def list_func(lib, opts, args):
FILE: beets/ui/commands/modify.py
function modify_items (line 9) | def modify_items(lib, mods, dels, query, write, move, album, confirm, in...
function print_and_modify (line 66) | def print_and_modify(obj, mods, dels):
function modify_parse_args (line 82) | def modify_parse_args(args):
function modify_func (line 101) | def modify_func(lib, opts, args):
FILE: beets/ui/commands/move.py
function show_path_changes (line 21) | def show_path_changes(path_changes):
function move_items (line 62) | def move_items(
function move_func (line 149) | def move_func(lib, opts, args):
FILE: beets/ui/commands/remove.py
function remove_items (line 8) | def remove_items(lib, query, album, delete, force):
function remove_func (line 70) | def remove_func(lib, opts, args):
FILE: beets/ui/commands/stats.py
function show_stats (line 13) | def show_stats(lib, query, exact):
function stats_func (line 52) | def stats_func(lib, opts, args):
FILE: beets/ui/commands/update.py
function update_items (line 15) | def update_items(lib, query, album, move, pretend, fields, exclude_field...
function update_func (line 133) | def update_func(lib, opts, args):
FILE: beets/ui/commands/utils.py
function do_query (line 6) | def do_query(lib, query, album, also_items=True):
FILE: beets/ui/commands/version.py
function show_version (line 9) | def show_version(*args):
FILE: beets/ui/commands/write.py
function write_items (line 14) | def write_items(lib, query, pretend, force):
function write_func (line 43) | def write_func(lib, opts, args):
FILE: beets/util/__init__.py
class HumanReadableError (line 74) | class HumanReadableError(Exception):
method __init__ (line 90) | def __init__(self, reason, verb, tb=None):
method _gerund (line 96) | def _gerund(self):
method _reasonstr (line 104) | def _reasonstr(self):
method get_message (line 115) | def get_message(self):
method log (line 121) | def log(self, logger):
class FilesystemError (line 130) | class FilesystemError(HumanReadableError):
method __init__ (line 136) | def __init__(self, reason, verb, paths, tb=None):
method get_message (line 140) | def get_message(self):
class MoveOperation (line 158) | class MoveOperation(Enum):
class PromptChoice (line 169) | class PromptChoice(NamedTuple):
function normpath (line 175) | def normpath(path: PathLike) -> bytes:
function ancestry (line 184) | def ancestry(path: AnyStr) -> list[AnyStr]:
function sorted_walk (line 208) | def sorted_walk(
function path_as_posix (line 273) | def path_as_posix(path: bytes) -> bytes:
function mkdirall (line 280) | def mkdirall(path: bytes):
function fnmatch_all (line 294) | def fnmatch_all(names: Sequence[bytes], patterns: Sequence[bytes]) -> bool:
function prune_dirs (line 309) | def prune_dirs(
function components (line 356) | def components(path: AnyStr) -> list[AnyStr]:
function bytestring_path (line 380) | def bytestring_path(path: PathLike) -> bytes:
function displayable_path (line 405) | def displayable_path(
function syspath (line 424) | def syspath(path: PathLike, prefix: bool = True) -> str:
function samefile (line 447) | def samefile(p1: bytes, p2: bytes) -> bool:
function remove (line 457) | def remove(path: PathLike, soft: bool = True):
function copy (line 472) | def copy(path: bytes, dest: bytes, replace: bool = False):
function move (line 492) | def move(path: bytes, dest: bytes, replace: bool = False):
function link (line 553) | def link(path: bytes, dest: bytes, replace: bool = False):
function hardlink (line 576) | def hardlink(path: bytes, dest: bytes, replace: bool = False):
function reflink (line 611) | def reflink(
function unique_path (line 651) | def unique_path(path: bytes) -> bytes:
function sanitize_path (line 688) | def sanitize_path(path: str, replacements: Replacements | None = None) -...
function truncate_str (line 709) | def truncate_str(s: str, length: int) -> str:
function truncate_path (line 722) | def truncate_path(str_path: str) -> str:
function _legalize_stage (line 731) | def _legalize_stage(
function legalize_path (line 754) | def legalize_path(
function str2bool (line 792) | def str2bool(value: str) -> bool:
function as_string (line 797) | def as_string(value: Any) -> str:
function plurality (line 811) | def plurality(objs: Iterable[T]) -> tuple[T, int]:
function get_most_common_tags (line 822) | def get_most_common_tags(
class CommandOutput (line 862) | class CommandOutput(NamedTuple):
function command_output (line 867) | def command_output(
function get_max_filename_length (line 908) | def get_max_filename_length() -> int:
function open_anything (line 929) | def open_anything() -> str:
function editor_command (line 944) | def editor_command() -> str:
function interactive_open (line 957) | def interactive_open(targets: Sequence[str], command: str):
function case_sensitive (line 979) | def case_sensitive(path: bytes) -> bool:
function asciify_path (line 1027) | def asciify_path(path: str, sep_replace: str) -> str:
function par_map (line 1050) | def par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:
class cached_classproperty (line 1064) | class cached_classproperty(Generic[T]):
method __init__ (line 1090) | def __init__(self, getter: Callable[..., T]) -> None:
method __set_name__ (line 1094) | def __set_name__(self, owner: object, name: str) -> None:
method __get__ (line 1098) | def __get__(self, instance: object, owner: type[object]) -> T:
class LazySharedInstance (line 1107) | class LazySharedInstance(Generic[T]):
method __get__ (line 1141) | def __get__(self, instance: T | None, owner: type[T]) -> T:
function get_module_tempdir (line 1154) | def get_module_tempdir(module: str) -> Path:
function clean_module_tempdir (line 1166) | def clean_module_tempdir(module: str) -> None:
function get_temp_filename (line 1175) | def get_temp_filename(
function unique_list (line 1200) | def unique_list(elements: Iterable[T]) -> list[T]:
FILE: beets/util/artresizer.py
function resize_url (line 49) | def resize_url(url: str, maxwidth: int, quality: int = 0) -> str:
class LocalBackendNotAvailableError (line 64) | class LocalBackendNotAvailableError(Exception):
class NotAvailable (line 70) | class NotAvailable(Enum):
class LocalBackend (line 77) | class LocalBackend(ABC):
method version (line 82) | def version(cls) -> Any:
method available (line 89) | def available(cls) -> bool:
method resize (line 99) | def resize(
method get_size (line 114) | def get_size(self, path_in: bytes) -> tuple[int, int] | None:
method deinterlace (line 119) | def deinterlace(
method get_format (line 131) | def get_format(self, path_in: bytes) -> str | None:
method convert_format (line 136) | def convert_format(
method can_compare (line 149) | def can_compare(self) -> bool:
method compare (line 153) | def compare(
method can_write_metadata (line 168) | def can_write_metadata(self) -> bool:
method write_metadata (line 172) | def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> ...
class IMBackend (line 181) | class IMBackend(LocalBackend):
method version (line 191) | def version(cls) -> tuple[int, int, int]:
method __init__ (line 225) | def __init__(self) -> None:
method resize (line 245) | def resize(
method get_size (line 302) | def get_size(self, path_in: bytes) -> tuple[int, int] | None:
method deinterlace (line 334) | def deinterlace(
method get_format (line 357) | def get_format(self, path_in: bytes) -> str | None:
method convert_format (line 369) | def convert_format(
method can_compare (line 392) | def can_compare(self) -> bool:
method compare (line 395) | def compare(
method can_write_metadata (line 489) | def can_write_metadata(self) -> bool:
method write_metadata (line 492) | def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> ...
class PILBackend (line 502) | class PILBackend(LocalBackend):
method version (line 506) | def version(cls) -> None:
method __init__ (line 512) | def __init__(self) -> None:
method resize (line 519) | def resize(
method get_size (line 595) | def get_size(self, path_in: bytes) -> tuple[int, int] | None:
method deinterlace (line 607) | def deinterlace(
method get_format (line 625) | def get_format(self, path_in: bytes) -> str | None:
method convert_format (line 640) | def convert_format(
method can_compare (line 663) | def can_compare(self) -> bool:
method compare (line 666) | def compare(
method can_write_metadata (line 676) | def can_write_metadata(self) -> bool:
method write_metadata (line 679) | def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> ...
class ArtResizer (line 697) | class ArtResizer:
method __init__ (line 702) | def __init__(self) -> None:
method method (line 729) | def method(self) -> str:
method resize (line 735) | def resize(
method deinterlace (line 760) | def deinterlace(
method proxy_url (line 775) | def proxy_url(self, maxwidth: int, url: str, quality: int = 0) -> str:
method local (line 787) | def local(self) -> bool:
method get_size (line 793) | def get_size(self, path_in: bytes) -> tuple[int, int] | None:
method get_format (line 806) | def get_format(self, path_in: bytes) -> str | None:
method reformat (line 817) | def reformat(
method can_compare (line 855) | def can_compare(self) -> bool:
method compare (line 863) | def compare(
method can_write_metadata (line 880) | def can_write_metadata(self) -> bool:
method write_metadata (line 888) | def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> ...
FILE: beets/util/bluelet.py
class Event (line 21) | class Event:
class WaitableEvent (line 30) | class WaitableEvent(Event):
method waitables (line 36) | def waitables(self):
method fire (line 44) | def fire(self):
class ValueEvent (line 51) | class ValueEvent(Event):
method __init__ (line 54) | def __init__(self, value):
class ExceptionEvent (line 58) | class ExceptionEvent(Event):
method __init__ (line 61) | def __init__(self, exc_info):
class SpawnEvent (line 65) | class SpawnEvent(Event):
method __init__ (line 68) | def __init__(self, coro):
class JoinEvent (line 72) | class JoinEvent(Event):
method __init__ (line 77) | def __init__(self, child):
class KillEvent (line 81) | class KillEvent(Event):
method __init__ (line 84) | def __init__(self, child):
class DelegationEvent (line 88) | class DelegationEvent(Event):
method __init__ (line 94) | def __init__(self, coro):
class ReturnEvent (line 98) | class ReturnEvent(Event):
method __init__ (line 103) | def __init__(self, value):
class SleepEvent (line 107) | class SleepEvent(WaitableEvent):
method __init__ (line 110) | def __init__(self, duration):
method time_left (line 113) | def time_left(self):
class ReadEvent (line 117) | class ReadEvent(WaitableEvent):
method __init__ (line 120) | def __init__(self, fd, bufsize):
method waitables (line 124) | def waitables(self):
method fire (line 127) | def fire(self):
class WriteEvent (line 131) | class WriteEvent(WaitableEvent):
method __init__ (line 134) | def __init__(self, fd, data):
method waitable (line 138) | def waitable(self):
method fire (line 141) | def fire(self):
function _event_select (line 148) | def _event_select(events):
class ThreadError (line 206) | class ThreadError(Exception):
method __init__ (line 207) | def __init__(self, coro, exc_info):
method reraise (line 211) | def reraise(self):
class Delegated (line 218) | class Delegated(Event):
method __init__ (line 223) | def __init__(self, child):
function run (line 227) | def run(root_coro):
class SocketClosedError (line 402) | class SocketClosedError(Exception):
class Listener (line 406) | class Listener:
method __init__ (line 409) | def __init__(self, host, port):
method accept (line 419) | def accept(self):
method close (line 428) | def close(self):
class Connection (line 434) | class Connection:
method __init__ (line 437) | def __init__(self, sock, addr):
method close (line 443) | def close(self):
method recv (line 448) | def recv(self, size):
method send (line 461) | def send(self, data):
method sendall (line 469) | def sendall(self, data):
method readline (line 475) | def readline(self, terminator=b"\n", bufsize=1024):
class AcceptEvent (line 496) | class AcceptEvent(WaitableEvent):
method __init__ (line 501) | def __init__(self, listener):
method waitables (line 504) | def waitables(self):
method fire (line 507) | def fire(self):
class ReceiveEvent (line 512) | class ReceiveEvent(WaitableEvent):
method __init__ (line 517) | def __init__(self, conn, bufsize):
method waitables (line 521) | def waitables(self):
method fire (line 524) | def fire(self):
class SendEvent (line 528) | class SendEvent(WaitableEvent):
method __init__ (line 533) | def __init__(self, conn, data, sendall=False):
method waitables (line 538) | def waitables(self):
method fire (line 541) | def fire(self):
function null (line 552) | def null():
function spawn (line 557) | def spawn(coro):
function call (line 566) | def call(coro):
function end (line 576) | def end(value=None):
function read (line 583) | def read(fd, bufsize=None):
function write (line 602) | def write(fd, data):
function connect (line 607) | def connect(host, port):
function sleep (line 616) | def sleep(duration):
function join (line 621) | def join(coro):
function kill (line 628) | def kill(coro):
function server (line 636) | def server(host, port, func):
FILE: beets/util/color.py
function get_color_config (line 120) | def get_color_config() -> dict[ColorName, str]:
function _colorize (line 148) | def _colorize(color_name: ColorName, text: str) -> str:
function colorize (line 154) | def colorize(color_name: ColorName, text: str) -> str:
function dist_colorize (line 162) | def dist_colorize(string: str, dist: Distance) -> str:
function uncolorize (line 175) | def uncolorize(colored_text: str) -> str:
function color_split (line 188) | def color_split(colored_text: str, index: int) -> tuple[str, str]:
function color_len (line 224) | def color_len(colored_text: str) -> int:
FILE: beets/util/config.py
function sanitize_choices (line 9) | def sanitize_choices(
function sanitize_pairs (line 29) | def sanitize_pairs(
FILE: beets/util/deprecation.py
function _format_message (line 15) | def _format_message(old: str, new: str | None = None) -> str:
function deprecate_for_user (line 24) | def deprecate_for_user(
function deprecate_for_maintainers (line 30) | def deprecate_for_maintainers(
function deprecate_imports (line 44) | def deprecate_imports(
FILE: beets/util/diff.py
function colordiff (line 15) | def colordiff(a: str, b: str) -> tuple[str, str]:
function _multi_value_diff (line 37) | def _multi_value_diff(field: str, oldset: set[str], newset: set[str]) ->...
function _field_diff (line 49) | def _field_diff(
function get_model_changes (line 87) | def get_model_changes(
FILE: beets/util/functemplate.py
class Environment (line 46) | class Environment:
method __init__ (line 51) | def __init__(self, values, functions):
function ex_rvalue (line 59) | def ex_rvalue(name):
function ex_literal (line 64) | def ex_literal(val):
function ex_call (line 71) | def ex_call(func, args):
function compile_func (line 87) | def compile_func(arg_names, statements, name="_the_func", debug=False):
class Symbol (line 129) | class Symbol:
method __init__ (line 132) | def __init__(self, ident, original):
method __repr__ (line 136) | def __repr__(self):
method evaluate (line 139) | def evaluate(self, env):
method translate (line 150) | def translate(self):
class Call (line 157) | class Call:
method __init__ (line 160) | def __init__(self, ident, args, original):
method __repr__ (line 165) | def __repr__(self):
method evaluate (line 168) | def evaluate(self, env):
method translate (line 184) | def translate(self):
class Expression (line 216) | class Expression:
method __init__ (line 221) | def __init__(self, parts):
method __repr__ (line 224) | def __repr__(self):
method evaluate (line 227) | def evaluate(self, env):
method translate (line 239) | def translate(self):
class ParseError (line 260) | class ParseError(Exception):
class Parser (line 264) | class Parser:
method __init__ (line 278) | def __init__(self, string, in_argument=False):
method parse_expression (line 300) | def parse_expression(self):
method parse_symbol (line 377) | def parse_symbol(self):
method parse_call (line 423) | def parse_call(self):
method parse_argument_list (line 461) | def parse_argument_list(self):
method _parse_ident (line 492) | def _parse_ident(self):
function _parse (line 502) | def _parse(template):
function template (line 517) | def template(fmt):
class Template (line 522) | class Template:
method __init__ (line 525) | def __init__(self, template):
method __eq__ (line 530) | def __eq__(self, other):
method interpret (line 533) | def interpret(self, values={}, functions={}):
method substitute (line 541) | def substitute(self, values={}, functions={}):
method translate (line 550) | def translate(self):
FILE: beets/util/hidden.py
function is_hidden (line 25) | def is_hidden(path: bytes | Path) -> bool:
FILE: beets/util/id_extractors.py
function extract_release_id (line 50) | def extract_release_id(source: str, id_: str) -> str | None:
FILE: beets/util/layout.py
class Side (line 19) | class Side(NamedTuple):
method rendered (line 33) | def rendered(self) -> str:
method prefix_width (line 38) | def prefix_width(self) -> int:
method suffix_width (line 43) | def suffix_width(self) -> int:
method rendered_width (line 48) | def rendered_width(self) -> int:
function indent (line 53) | def indent(count: int) -> str:
function split_into_lines (line 58) | def split_into_lines(string: str, first_width: int, width: int) -> list[...
function get_column_layout (line 183) | def get_column_layout(
function get_newline_layout (line 296) | def get_newline_layout(
function get_layout_method (line 343) | def get_layout_method() -> Callable[[str, Side, Side, int, str], Iterato...
function get_layout_lines (line 349) | def get_layout_lines(
FILE: beets/util/lyrics.py
class Lyrics (line 20) | class Lyrics:
method __post_init__ (line 39) | def __post_init__(self) -> None:
method from_legacy_text (line 69) | def from_legacy_text(cls, text: str) -> Lyrics:
method from_item (line 84) | def from_item(cls, item: Item) -> Lyrics:
method original_text (line 93) | def original_text(self) -> str:
method _split_lines (line 99) | def _split_lines(self) -> list[tuple[str, str]]:
method timestamps (line 111) | def timestamps(self) -> list[str]:
method text_lines (line 116) | def text_lines(self) -> list[str]:
method synced (line 121) | def synced(self) -> bool:
method translated (line 126) | def translated(self) -> bool:
method full_text (line 131) | def full_text(self) -> str:
FILE: beets/util/m3u.py
class EmptyPlaylistError (line 22) | class EmptyPlaylistError(Exception):
class M3UFile (line 28) | class M3UFile:
method __init__ (line 31) | def __init__(self, path):
method load (line 43) | def load(self):
method set_contents (line 63) | def set_contents(self, media_list, extm3u=True):
method write (line 77) | def write(self):
FILE: beets/util/pipeline.py
function _invalidate_queue (line 54) | def _invalidate_queue(q, val=None, sync=True):
class CountedQueue (line 96) | class CountedQueue(queue.Queue[Tq]):
method __init__ (line 102) | def __init__(self, maxsize=0):
method acquire (line 107) | def acquire(self):
method release (line 116) | def release(self):
class MultiMessage (line 146) | class MultiMessage:
method __init__ (line 151) | def __init__(self, messages):
function multiple (line 155) | def multiple(messages):
function stage (line 172) | def stage(
function mutator_stage (line 200) | def mutator_stage(func: Callable[[Unpack[A], T], R]):
function _allmsgs (line 224) | def _allmsgs(obj):
class PipelineThread (line 237) | class PipelineThread(Thread):
method __init__ (line 240) | def __init__(self, all_threads):
method abort (line 247) | def abort(self):
method abort_all (line 258) | def abort_all(self, exc_info):
class FirstPipelineThread (line 265) | class FirstPipelineThread(PipelineThread):
method __init__ (line 270) | def __init__(self, coro, out_queue, all_threads):
method run (line 276) | def run(self):
class MiddlePipelineThread (line 304) | class MiddlePipelineThread(PipelineThread):
method __init__ (line 309) | def __init__(self, coro, in_queue, out_queue, all_threads):
method run (line 316) | def run(self):
class LastPipelineThread (line 353) | class LastPipelineThread(PipelineThread):
method __init__ (line 358) | def __init__(self, coro, in_queue, all_threads):
method run (line 363) | def run(self):
class Pipeline (line 390) | class Pipeline:
method __init__ (line 396) | def __init__(self, stages):
method run_sequential (line 410) | def run_sequential(self):
method run_parallel (line 417) | def run_parallel(self, queue_size=DEFAULT_QUEUE_SIZE):
method pull (line 473) | def pull(self):
FILE: beets/util/units.py
function raw_seconds_short (line 4) | def raw_seconds_short(string: str) -> float:
function human_seconds_short (line 17) | def human_seconds_short(interval):
function human_bytes (line 25) | def human_bytes(size):
function human_seconds (line 37) | def human_seconds(interval):
FILE: beetsplug/_typing.py
class LRCLibAPI (line 10) | class LRCLibAPI:
class Item (line 11) | class Item(TypedDict):
class GeniusAPI (line 25) | class GeniusAPI:
class DateComponents (line 33) | class DateComponents(TypedDict):
class Artist (line 38) | class Artist(TypedDict):
class Stats (line 48) | class Stats(TypedDict):
class SearchResult (line 52) | class SearchResult(TypedDict):
class SearchHit (line 79) | class SearchHit(TypedDict):
class SearchResponse (line 82) | class SearchResponse(TypedDict):
class Search (line 85) | class Search(TypedDict):
class StatusResponse (line 88) | class StatusResponse(TypedDict):
class Meta (line 92) | class Meta(TypedDict):
class GoogleCustomSearchAPI (line 98) | class GoogleCustomSearchAPI:
class Response (line 99) | class Response(TypedDict):
class Item (line 107) | class Item(TypedDict):
class Pagemap (line 121) | class Pagemap(TypedDict):
class TranslatorAPI (line 127) | class TranslatorAPI:
class Language (line 128) | class Language(TypedDict):
class Translation (line 134) | class Translation(TypedDict):
class Response (line 140) | class Response(TypedDict):
FILE: beetsplug/_utils/art.py
function mediafile_image (line 28) | def mediafile_image(image_path, maxwidth=None):
function get_art (line 36) | def get_art(log, item):
function embed_item (line 47) | def embed_item(
function embed_album (line 97) | def embed_album(
function resize_image (line 137) | def resize_image(log, imagepath, maxwidth, quality):
function check_art_similarity (line 152) | def check_art_similarity(
function extract (line 178) | def extract(log, outpath, item):
function extract_first (line 202) | def extract_first(log, outpath, items):
function clear_item (line 209) | def clear_item(item, log):
function clear (line 215) | def clear(log, lib, query):
FILE: beetsplug/_utils/musicbrainz.py
class LimiterTimeoutSession (line 69) | class LimiterTimeoutSession(LimiterMixin, TimeoutAndRetrySession):
class LookupKwargs (line 91) | class LookupKwargs(TypedDict, total=False):
class PagingKwargs (line 95) | class PagingKwargs(TypedDict, total=False):
class SearchKwargs (line 100) | class SearchKwargs(PagingKwargs):
class BrowseKwargs (line 104) | class BrowseKwargs(LookupKwargs, PagingKwargs, total=False):
class BrowseReleaseGroupsKwargs (line 108) | class BrowseReleaseGroupsKwargs(BrowseKwargs, total=False):
class BrowseRecordingsKwargs (line 115) | class BrowseRecordingsKwargs(BrowseReleaseGroupsKwargs, total=False):
function require_one_of (line 123) | def require_one_of(*keys: str) -> Callable[[Callable[P, R]], Callable[P,...
class MusicBrainzAPI (line 143) | class MusicBrainzAPI(RequestHandler):
method __post_init__ (line 159) | def __post_init__(self) -> None:
method api_root (line 182) | def api_root(self) -> str:
method create_session (line 185) | def create_session(self) -> LimiterTimeoutSession:
method request (line 188) | def request(self, *args, **kwargs) -> Response:
method _get_resource (line 194) | def _get_resource(
method _lookup (line 210) | def _lookup(
method _browse (line 215) | def _browse(self, entity: Entity, **kwargs) -> list[JSONDict]:
method format_search_term (line 219) | def format_search_term(field: str, term: str) -> str:
method search (line 233) | def search(
method get_release (line 254) | def get_release(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> JSO...
method get_recording (line 259) | def get_recording(
method get_work (line 266) | def get_work(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> JSONDict:
method browse_recordings (line 271) | def browse_recordings(
method browse_release_groups (line 281) | def browse_release_groups(
method _group_relations (line 293) | def _group_relations(cls, data: Any) -> Any:
method _ (line 310) | def _(cls, data: list[Any]) -> list[Any]:
method _ (line 315) | def _(cls, data: JSONDict) -> JSONDict:
class MusicBrainzAPIMixin (line 335) | class MusicBrainzAPIMixin:
method mb_api (line 339) | def mb_api(self) -> MusicBrainzAPI:
FILE: beetsplug/_utils/requests.py
class BeetsHTTPError (line 20) | class BeetsHTTPError(requests.exceptions.HTTPError):
method __init__ (line 23) | def __init__(self, *args, **kwargs) -> None:
class HTTPNotFoundError (line 31) | class HTTPNotFoundError(BeetsHTTPError):
class Closeable (line 35) | class Closeable(Protocol):
method close (line 38) | def close(self) -> None: ...
class SingletonMeta (line 44) | class SingletonMeta(type, Generic[C]):
method __call__ (line 55) | def __call__(cls, *args: Any, **kwargs: Any) -> C:
class TimeoutAndRetrySession (line 65) | class TimeoutAndRetrySession(requests.Session, metaclass=SingletonMeta):
method __init__ (line 74) | def __init__(self, *args, **kwargs) -> None:
method request (line 93) | def request(self, *args, **kwargs):
class RequestHandler (line 106) | class RequestHandler:
method create_session (line 133) | def create_session(self) -> TimeoutAndRetrySession:
method session (line 141) | def session(self) -> TimeoutAndRetrySession:
method status_to_error (line 144) | def status_to_error(
method handle_http_error (line 157) | def handle_http_error(self) -> Iterator[None]:
method request (line 172) | def request(self, *args, **kwargs) -> requests.Response:
method get (line 181) | def get(self, *args, **kwargs) -> requests.Response:
method put (line 185) | def put(self, *args, **kwargs) -> requests.Response:
method delete (line 189) | def delete(self, *args, **kwargs) -> requests.Response:
method get_json (line 193) | def get_json(self, *args, **kwargs):
FILE: beetsplug/_utils/vfs.py
class Node (line 29) | class Node(NamedTuple):
function _insert (line 37) | def _insert(node: Node, path: list[str], itemid: int):
function libtree (line 51) | def libtree(lib: Library) -> Node:
FILE: beetsplug/absubmit.py
class ABSubmitError (line 33) | class ABSubmitError(Exception):
function call (line 37) | def call(args):
class AcousticBrainzSubmitPlugin (line 48) | class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
method __init__ (line 49) | def __init__(self):
method commands (line 103) | def commands(self):
method command (line 129) | def command(self, lib, opts, args):
method analyze_submit (line 142) | def analyze_submit(self, item):
method _get_analysis (line 147) | def _get_analysis(self, item):
method _submit_data (line 198) | def _submit_data(self, item, data):
FILE: beetsplug/acousticbrainz.py
class AcousticPlugin (line 58) | class AcousticPlugin(plugins.BeetsPlugin):
method __init__ (line 84) | def __init__(self):
method commands (line 106) | def commands(self):
method import_task_files (line 130) | def import_task_files(self, session, task):
method _get_data (line 134) | def _get_data(self, mbid):
method _fetch_info (line 162) | def _fetch_info(self, items, write, force):
method _map_data_to_scheme (line 201) | def _map_data_to_scheme(self, data, scheme):
method _data_to_scheme_child (line 263) | def _data_to_scheme_child(self, subdata, subscheme, composites):
function _generate_urls (line 301) | def _generate_urls(base_url, mbid):
FILE: beetsplug/advancedrewrite.py
function rewriter (line 30) | def rewriter(field, simple_rules, advanced_rules):
class AdvancedRewritePlugin (line 55) | class AdvancedRewritePlugin(BeetsPlugin):
method __init__ (line 58) | def __init__(self):
method loaded (line 63) | def loaded(self):
FILE: beetsplug/albumtypes.py
class AlbumTypesPlugin (line 29) | class AlbumTypesPlugin(BeetsPlugin):
method __init__ (line 32) | def __init__(self):
method _atypes (line 51) | def _atypes(self, item: Album):
FILE: beetsplug/aura.py
class AURADocument (line 131) | class AURADocument:
method from_app (line 140) | def from_app(cls) -> Self:
method error (line 145) | def error(status, title, detail):
method get_attribute_converter (line 160) | def get_attribute_converter(cls, beets_attr: str) -> type[SQLiteType]:
method translate_filters (line 175) | def translate_filters(self):
method translate_sorts (line 198) | def translate_sorts(self, sort_arg):
method paginate (line 223) | def paginate(self, collection):
method get_included (line 263) | def get_included(self, data, include_str):
method all_resources (line 318) | def all_resources(self):
method single_resource_document (line 348) | def single_resource_document(self, resource_object):
class TrackDocument (line 365) | class TrackDocument(AURADocument):
method get_collection (line 372) | def get_collection(self, query=None, sort=None):
method get_attribute_converter (line 382) | def get_attribute_converter(cls, beets_attr: str) -> type[SQLiteType]:
method get_resource_object (line 395) | def get_resource_object(lib: Library, track):
method single_resource (line 427) | def single_resource(self, track_id):
class AlbumDocument (line 445) | class AlbumDocument(AURADocument):
method get_collection (line 452) | def get_collection(self, query=None, sort=None):
method get_resource_object (line 462) | def get_resource_object(lib: Library, album):
method single_resource (line 511) | def single_resource(self, album_id):
class ArtistDocument (line 529) | class ArtistDocument(AURADocument):
method get_collection (line 536) | def get_collection(self, query=None, sort=None):
method get_resource_object (line 553) | def get_resource_object(lib: Library, artist_id):
method single_resource (line 596) | def single_resource(self, artist_id):
function safe_filename (line 612) | def safe_filename(fn):
class ImageDocument (line 630) | class ImageDocument(AURADocument):
method get_image_path (line 636) | def get_image_path(lib: Library, image_id):
method get_resource_object (line 678) | def get_resource_object(lib: Library, image_id):
method single_resource (line 720) | def single_resource(self, image_id):
function server_info (line 742) | def server_info():
function all_tracks (line 751) | def all_tracks():
function single_track (line 757) | def single_track(track_id):
function audio_file (line 767) | def audio_file(track_id):
function all_albums (line 826) | def all_albums():
function single_album (line 832) | def single_album(album_id):
function all_artists (line 846) | def all_artists():
function single_artist (line 853) | def single_artist(artist_id):
function single_image (line 869) | def single_image(image_id):
function image_file (line 880) | def image_file(image_id):
function create_app (line 900) | def create_app():
class AURAPlugin (line 945) | class AURAPlugin(BeetsPlugin):
method __init__ (line 948) | def __init__(self):
method commands (line 952) | def commands(self):
FILE: beetsplug/autobpm.py
class AutoBPMPlugin (line 31) | class AutoBPMPlugin(BeetsPlugin):
method __init__ (line 32) | def __init__(self) -> None:
method commands (line 45) | def commands(self) -> list[Subcommand]:
method command (line 52) | def command(self, lib: Library, _, args: list[str]) -> None:
method imported (line 55) | def imported(self, _, task: ImportTask) -> None:
method calculate_bpm (line 58) | def calculate_bpm(self, items: list[Item], write: bool = False) -> None:
FILE: beetsplug/badfiles.py
class CheckerCommandError (line 32) | class CheckerCommandError(Exception):
method __init__ (line 42) | def __init__(self, cmd, oserror):
class BadFiles (line 49) | class BadFiles(BeetsPlugin):
method __init__ (line 50) | def __init__(self):
method run_command (line 59) | def run_command(self, cmd):
method check_mp3val (line 76) | def check_mp3val(self, path):
method check_flac (line 83) | def check_flac(self, path):
method check_custom (line 86) | def check_custom(self, command):
method get_checker (line 94) | def get_checker(self, ext):
method check_item (line 107) | def check_item(self, item):
method on_import_task_start (line 157) | def on_import_task_start(self, task, session):
method on_import_task_before_choice (line 171) | def on_import_task_before_choice(self, task, session):
method command (line 194) | def command(self, lib, opts, args):
method commands (line 205) | def commands(self):
FILE: beetsplug/bareasc.py
class BareascQuery (line 29) | class BareascQuery(StringFieldQuery[str]):
method string_match (line 33) | def string_match(cls, pattern, val):
method col_clause (line 46) | def col_clause(self):
class BareascPlugin (line 55) | class BareascPlugin(BeetsPlugin):
method __init__ (line 58) | def __init__(self):
method queries (line 67) | def queries(self):
method commands (line 72) | def commands(self):
method unidecode_list (line 84) | def unidecode_list(self, lib, opts, args):
FILE: beetsplug/beatport.py
class BeatportAPIError (line 52) | class BeatportAPIError(Exception):
class BeatportClient (line 56) | class BeatportClient:
method __init__ (line 59) | def __init__(self, c_key, c_secret, auth_key=None, auth_secret=None):
method get_authorize_url (line 80) | def get_authorize_url(self) -> str:
method get_access_token (line 102) | def get_access_token(self, auth_data: str) -> tuple[str, str]:
method search (line 119) | def search(
method search (line 127) | def search(
method search (line 134) | def search(
method get_release (line 165) | def get_release(self, beatport_id: str) -> BeatportRelease | None:
method get_release_tracks (line 178) | def get_release_tracks(self, beatport_id: str) -> list[BeatportTrack]:
method get_track (line 189) | def get_track(self, beatport_id: str) -> BeatportTrack:
method _make_url (line 198) | def _make_url(self, endpoint: str) -> str:
method _get (line 204) | def _get(self, endpoint: str, **kwargs) -> list[JSONDict]:
class BeatportObject (line 221) | class BeatportObject:
method __init__ (line 230) | def __init__(self, data: JSONDict):
method artists_str (line 245) | def artists_str(self) -> str | None:
class BeatportRelease (line 257) | class BeatportRelease(BeatportObject):
method __init__ (line 265) | def __init__(self, data: JSONDict):
method __str__ (line 277) | def __str__(self) -> str:
class BeatportTrack (line 284) | class BeatportTrack(BeatportObject):
method __init__ (line 293) | def __init__(self, data: JSONDict):
class BeatportPlugin (line 313) | class BeatportPlugin(MetadataSourcePlugin):
method __init__ (line 316) | def __init__(self):
method client (line 331) | def client(self) -> BeatportClient:
method setup (line 338) | def setup(self, session: ImportSession):
method authenticate (line 355) | def authenticate(self, c_key: str, c_secret: str) -> tuple[str, str]:
method _tokenfile (line 382) | def _tokenfile(self) -> str:
method candidates (line 386) | def candidates(
method item_candidates (line 403) | def item_candidates(
method album_for_id (line 413) | def album_for_id(self, album_id: str):
method track_for_id (line 428) | def track_for_id(self, track_id: str):
method _get_releases (line 443) | def _get_releases(self, query: str) -> Iterator[AlbumInfo]:
method _get_album_info (line 458) | def _get_album_info(self, release: BeatportRelease) -> AlbumInfo:
method _get_track_info (line 490) | def _get_track_info(self, track: BeatportTrack) -> TrackInfo:
method _get_artist (line 512) | def _get_artist(self, artists):
method _get_tracks (line 518) | def _get_tracks(self, query):
FILE: beetsplug/bench.py
function aunique_benchmark (line 27) | def aunique_benchmark(lib, prof):
function match_benchmark (line 68) | def match_benchmark(lib, prof, query=None, album_id=None):
class BenchmarkPlugin (line 97) | class BenchmarkPlugin(BeetsPlugin):
method commands (line 100) | def commands(self):
FILE: beetsplug/bpd/__init__.py
class BPDError (line 110) | class BPDError(Exception):
method __init__ (line 115) | def __init__(self, code, message, cmd_name="", index=0):
method response (line 123) | def response(self):
function make_bpd_error (line 138) | def make_bpd_error(s_code, s_message):
function cast_arg (line 158) | def cast_arg(t, val):
class BPDCloseError (line 174) | class BPDCloseError(Exception):
class BPDIdleError (line 180) | class BPDIdleError(Exception):
method __init__ (line 185) | def __init__(self, subsystems):
class BaseServer (line 193) | class BaseServer:
method __init__ (line 206) | def __init__(self, host, port, password, ctrl_port, log, ctrl_host=None):
method connect (line 241) | def connect(self, conn):
method disconnect (line 245) | def disconnect(self, conn):
method run (line 249) | def run(self):
method dispatch_events (line 269) | def dispatch_events(self):
method _ctrl_send (line 276) | def _ctrl_send(self, message):
method _send_event (line 286) | def _send_event(self, event):
method _item_info (line 291) | def _item_info(self, item):
method _item_id (line 297) | def _item_id(self, item):
method _id_to_index (line 301) | def _id_to_index(self, track_id):
method _random_idx (line 312) | def _random_idx(self):
method _succ_idx (line 324) | def _succ_idx(self):
method _prev_idx (line 335) | def _prev_idx(self):
method cmd_ping (line 346) | def cmd_ping(self, conn):
method cmd_idle (line 350) | def cmd_idle(self, conn, *subsystems):
method cmd_kill (line 357) | def cmd_kill(self, conn):
method cmd_close (line 361) | def cmd_close(self, conn):
method cmd_password (line 365) | def cmd_password(self, conn, password):
method cmd_commands (line 373) | def cmd_commands(self, conn):
method cmd_notcommands (line 386) | def cmd_notcommands(self, conn):
method cmd_status (line 400) | def cmd_status(self, conn):
method cmd_clearerror (line 446) | def cmd_clearerror(self, conn):
method cmd_random (line 453) | def cmd_random(self, conn, state):
method cmd_repeat (line 458) | def cmd_repeat(self, conn, state):
method cmd_consume (line 463) | def cmd_consume(self, conn, state):
method cmd_single (line 468) | def cmd_single(self, conn, state):
method cmd_setvol (line 474) | def cmd_setvol(self, conn, vol):
method cmd_volume (line 482) | def cmd_volume(self, conn, vol_delta):
method cmd_crossfade (line 487) | def cmd_crossfade(self, conn, crossfade):
method cmd_mixrampdb (line 496) | def cmd_mixrampdb(self, conn, db):
method cmd_mixrampdelay (line 505) | def cmd_mixrampdelay(self, conn, delay):
method cmd_replay_gain_mode (line 514) | def cmd_replay_gain_mode(self, conn, mode):
method cmd_replay_gain_status (line 522) | def cmd_replay_gain_status(self, conn):
method cmd_clear (line 526) | def cmd_clear(self, conn):
method cmd_delete (line 533) | def cmd_delete(self, conn, index):
method cmd_deleteid (line 549) | def cmd_deleteid(self, conn, track_id):
method cmd_move (line 552) | def cmd_move(self, conn, idx_from, idx_to):
method cmd_moveid (line 573) | def cmd_moveid(self, conn, idx_from, idx_to):
method cmd_swap (line 577) | def cmd_swap(self, conn, i, j):
method cmd_swapid (line 599) | def cmd_swapid(self, conn, i_id, j_id):
method cmd_urlhandlers (line 604) | def cmd_urlhandlers(self, conn):
method cmd_playlistinfo (line 608) | def cmd_playlistinfo(self, conn, index=None):
method cmd_playlistid (line 624) | def cmd_playlistid(self, conn, track_id=None):
method cmd_plchanges (line 630) | def cmd_plchanges(self, conn, version):
method cmd_plchangesposid (line 639) | def cmd_plchangesposid(self, conn, version):
method cmd_currentsong (line 648) | def cmd_currentsong(self, conn):
method cmd_next (line 654) | def cmd_next(self, conn):
method cmd_previous (line 676) | def cmd_previous(self, conn):
method cmd_pause (line 689) | def cmd_pause(self, conn, state=None):
method cmd_play (line 697) | def cmd_play(self, conn, index=-1):
method cmd_playid (line 717) | def cmd_playid(self, conn, track_id=0):
method cmd_stop (line 725) | def cmd_stop(self, conn):
method cmd_seek (line 731) | def cmd_seek(self, conn, index, pos):
method cmd_seekid (line 739) | def cmd_seekid(self, conn, track_id, pos):
method cmd_crash (line 745) | def cmd_crash(self, conn):
class Connection (line 754) | class Connection:
method __init__ (line 757) | def __init__(self, server, sock):
method debug (line 763) | def debug(self, message, kind=" "):
method run (line 767) | def run(self):
method send (line 770) | def send(self, lines):
method handler (line 786) | def handler(cls, server):
class MPDConnection (line 794) | class MPDConnection(Connection):
method __init__ (line 797) | def __init__(self, server, sock):
method do_command (line 804) | def do_command(self, command):
method disconnect (line 816) | def disconnect(self):
method notify (line 821) | def notify(self, event):
method send_notifications (line 825) | def send_notifications(self, force_close_idle=False):
method run (line 838) | def run(self):
class ControlConnection (line 905) | class ControlConnection(Connection):
method __init__ (line 908) | def __init__(self, server, sock):
method debug (line 912) | def debug(self, message, kind=" "):
method run (line 915) | def run(self):
method ctrl_play_finished (line 939) | def ctrl_play_finished(self):
method ctrl_profile (line 943) | def ctrl_profile(self):
method ctrl_nickname (line 950) | def ctrl_nickname(self, oldlabel, newlabel):
class Command (line 960) | class Command:
method __init__ (line 966) | def __init__(self, s):
method delegate (line 985) | def delegate(self, prefix, target, extra_args=0):
method run (line 1017) | def run(self, conn):
class CommandList (line 1064) | class CommandList(list[Command]):
method __init__ (line 1070) | def __init__(self, sequence=None, verbose=False):
method run (line 1079) | def run(self, conn):
class Server (line 1099) | class Server(BaseServer):
method __init__ (line 1104) | def __init__(self, library, host, port, password, ctrl_port, log):
method run (line 1113) | def run(self):
method play_finished (line 1117) | def play_finished(self):
method _item_info (line 1124) | def _item_info(self, item):
method _parse_range (line 1147) | def _parse_range(self, items, accept_single_number=False):
method _item_id (line 1162) | def _item_id(self, item):
method cmd_update (line 1167) | def cmd_update(self, conn, path="/"):
method _resolve_path (line 1180) | def _resolve_path(self, path):
method _path_join (line 1204) | def _path_join(self, p1, p2):
method cmd_lsinfo (line 1209) | def cmd_lsinfo(self, conn, path="/"):
method _listall (line 1226) | def _listall(self, basepath, node, info=False):
method cmd_listall (line 1248) | def cmd_listall(self, conn, path="/"):
method cmd_listallinfo (line 1252) | def cmd_listallinfo(self, conn, path="/"):
method _all_items (line 1258) | def _all_items(self, node):
method _add (line 1272) | def _add(self, path, send_id=False):
method cmd_add (line 1283) | def cmd_add(self, conn, path):
method cmd_addid (line 1289) | def cmd_addid(self, conn, path):
method cmd_status (line 1295) | def cmd_status(self, conn):
method cmd_stats (line 1314) | def cmd_stats(self, conn):
method cmd_decoders (line 1336) | def cmd_decoders(self, conn):
method cmd_tagtypes (line 1370) | def cmd_tagtypes(self, conn):
method _tagtype_lookup (line 1377) | def _tagtype_lookup(self, tag):
method _metadata_query (line 1389) | def _metadata_query(self, query_type, kv, allow_any_query: bool = False):
method cmd_search (line 1416) | def cmd_search(self, conn, *kv):
method cmd_find (line 1424) | def cmd_find(self, conn, *kv):
method cmd_list (line 1430) | def cmd_list(self, conn, show_tag, *kv):
method cmd_count (line 1464) | def cmd_count(self, conn, tag, value):
method cmd_listplaylist (line 1482) | def cmd_listplaylist(self, conn, playlist):
method cmd_listplaylistinfo (line 1485) | def cmd_listplaylistinfo(self, conn, playlist):
method cmd_listplaylists (line 1488) | def cmd_listplaylists(self, conn):
method cmd_load (line 1491) | def cmd_load(self, conn, playlist):
method cmd_playlistadd (line 1494) | def cmd_playlistadd(self, conn, playlist, uri):
method cmd_playlistclear (line 1497) | def cmd_playlistclear(self, conn, playlist):
method cmd_playlistdelete (line 1500) | def cmd_playlistdelete(self, conn, playlist, index):
method cmd_playlistmove (line 1503) | def cmd_playlistmove(self, conn, playlist, from_index, to_index):
method cmd_rename (line 1506) | def cmd_rename(self, conn, playlist, new_name):
method cmd_rm (line 1509) | def cmd_rm(self, conn, playlist):
method cmd_save (line 1512) | def cmd_save(self, conn, playlist):
method cmd_outputs (line 1518) | def cmd_outputs(self, conn):
method cmd_enableoutput (line 1526) | def cmd_enableoutput(self, conn, output_id):
method cmd_disableoutput (line 1531) | def cmd_disableoutput(self, conn, output_id):
method cmd_play (line 1542) | def cmd_play(self, conn, index=-1):
method cmd_pause (line 1554) | def cmd_pause(self, conn, state=None):
method cmd_stop (line 1561) | def cmd_stop(self, conn):
method cmd_seek (line 1565) | def cmd_seek(self, conn, index, pos):
method cmd_setvol (line 1574) | def cmd_setvol(self, conn, vol):
class BPDPlugin (line 1583) | class BPDPlugin(BeetsPlugin):
method __init__ (line 1588) | def __init__(self):
method start_bpd (line 1601) | def start_bpd(self, lib, host, port, password, volume, ctrl_port):
method commands (line 1607) | def commands(self):
FILE: beetsplug/bpd/gstplayer.py
class QueryError (line 45) | class QueryError(Exception):
class GstPlayer (line 49) | class GstPlayer:
method __init__ (line 62) | def __init__(self, finished_callback=None):
method _get_state (line 99) | def _get_state(self):
method _handle_message (line 105) | def _handle_message(self, bus, message):
method _set_volume (line 122) | def _set_volume(self, volume):
method _get_volume (line 128) | def _get_volume(self):
method play_file (line 134) | def play_file(self, path):
method play (line 146) | def play(self):
method pause (line 152) | def pause(self):
method stop (line 156) | def stop(self):
method run (line 162) | def run(self):
method time (line 177) | def time(self):
method seek (line 206) | def seek(self, position):
method block (line 220) | def block(self):
method get_decoders (line 225) | def get_decoders(self):
function get_decoders (line 229) | def get_decoders():
function play_simple (line 281) | def play_simple(paths):
function play_complicated (line 292) | def play_complicated(paths):
FILE: beetsplug/bpm.py
function bpm (line 23) | def bpm(max_strokes):
class BPMPlugin (line 47) | class BPMPlugin(BeetsPlugin):
method __init__ (line 48) | def __init__(self):
method commands (line 57) | def commands(self):
method command (line 65) | def command(self, lib, opts, args):
method get_bpm (line 69) | def get_bpm(self, items, write=False):
FILE: beetsplug/bpsync.py
class BPSyncPlugin (line 24) | class BPSyncPlugin(BeetsPlugin):
method __init__ (line 25) | def __init__(self):
method commands (line 31) | def commands(self):
method func (line 65) | def func(self, lib, opts, args):
method singletons (line 74) | def singletons(self, lib, query, move, pretend, write):
method is_beatport_track (line 100) | def is_beatport_track(item):
method get_album_tracks (line 106) | def get_album_tracks(self, album):
method albums (line 129) | def albums(self, lib, query, move, pretend, write):
FILE: beetsplug/bucket.py
class BucketError (line 27) | class BucketError(Exception):
function pairwise (line 31) | def pairwise(iterable):
function span_from_str (line 38) | def span_from_str(span_str):
function complete_year_spans (line 73) | def complete_year_spans(spans):
function extend_year_spans (line 83) | def extend_year_spans(spans, spanlen, start=1900, end=2014):
function build_year_spans (line 104) | def build_year_spans(year_spans_str):
function str2fmt (line 115) | def str2fmt(s):
function format_span (line 133) | def format_span(fmt, yearfrom, yearto, fromnchars, tonchars):
function extract_modes (line 142) | def extract_modes(spans):
function build_alpha_spans (line 151) | def build_alpha_spans(alpha_spans_str, alpha_regexs):
class BucketPlugin (line 179) | class BucketPlugin(plugins.BeetsPlugin):
method __init__ (line 180) | def __init__(self):
method setup (line 194) | def setup(self):
method find_bucket_year (line 210) | def find_bucket_year(self, year):
method find_bucket_alpha (line 228) | def find_bucket_alpha(self, s):
method _tmpl_bucket (line 237) | def _tmpl_bucket(self, text, field=None):
FILE: beetsplug/chroma.py
function prefix (line 60) | def prefix(it, count):
function releases_key (line 68) | def releases_key(release, countries, original_year):
function acoustid_match (line 91) | def acoustid_match(log, path):
function _all_releases (line 163) | def _all_releases(items):
class AcoustidPlugin (line 182) | class AcoustidPlugin(MetadataSourcePlugin):
method __init__ (line 183) | def __init__(self):
method mb (line 197) | def mb(self) -> MusicBrainzPlugin:
method fingerprint_task (line 200) | def fingerprint_task(self, task, session):
method track_distance (line 203) | def track_distance(self, item, info):
method candidates (line 213) | def candidates(self, items, artist, album, va_likely):
method item_candidates (line 223) | def item_candidates(self, item, artist, title) -> Iterable[TrackInfo]:
method album_for_id (line 236) | def album_for_id(self, *args, **kwargs):
method track_for_id (line 240) | def track_for_id(self, *args, **kwargs):
method commands (line 244) | def commands(self):
function fingerprint_task (line 274) | def fingerprint_task(log, task, session):
function apply_acoustid_metadata (line 283) | def apply_acoustid_metadata(task, session):
function submit_items (line 295) | def submit_items(log, userkey, items, chunksize=64):
function fingerprint_item (line 343) | def fingerprint_item(log, item, write=False):
FILE: beetsplug/convert.py
function replace_ext (line 48) | def replace_ext(path, ext):
function get_format (line 57) | def get_format(fmt=None):
function in_no_convert (line 88) | def in_no_convert(item: Item) -> bool:
function should_transcode (line 98) | def should_transcode(item, fmt, force: bool = False):
class ConvertPlugin (line 119) | class ConvertPlugin(BeetsPlugin):
method __init__ (line 120) | def __init__(self):
method commands (line 174) | def commands(self):
method auto_convert (line 259) | def auto_convert(self, config, task):
method auto_convert_keep (line 266) | def auto_convert_keep(self, config, task):
method encode (line 301) | def encode(self, command, source, dest, pretend=False):
method convert_item (line 363) | def convert_item(
method copy_album_art (line 509) | def copy_album_art(
method convert_func (line 593) | def convert_func(self, lib, opts, args):
method convert_on_import (line 672) | def convert_on_import(self, lib, item):
method _get_art_resize (line 711) | def _get_art_resize(self, artpath):
method _cleanup (line 731) | def _cleanup(self, task, session):
method _get_opts_and_config (line 738) | def _get_opts_and_config(self, opts):
method _parallel_convert (line 785) | def _parallel_convert(
FILE: beetsplug/deezer.py
class DeezerPlugin (line 39) | class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]):
method __init__ (line 51) | def __init__(self) -> None:
method commands (line 54) | def commands(self):
method album_for_id (line 68) | def album_for_id(self, album_id: str) -> AlbumInfo | None:
method track_for_id (line 151) | def track_for_id(self, track_id: str) -> None | TrackInfo:
method _get_track (line 195) | def _get_track(self, track_data: JSONDict) -> TrackInfo:
method get_search_query_with_filters (line 220) | def get_search_query_with_filters(
method get_search_response (line 234) | def get_search_response(self, params: SearchParams) -> list[IDResponse]:
method deezerupdate (line 249) | def deezerupdate(self, items: Sequence[Item], write: bool):
method fetch_data (line 276) | def fetch_data(self, url: str):
FILE: beetsplug/discogs/__init__.py
class DiscogsPlugin (line 93) | class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]):
method __init__ (line 94) | def __init__(self):
method extra_discogs_field_by_tag (line 121) | def extra_discogs_field_by_tag(self) -> dict[str, str]:
method setup (line 139) | def setup(self, session=None) -> None:
method reset_auth (line 165) | def reset_auth(self) -> None:
method _tokenfile (line 170) | def _tokenfile(self) -> str:
method authenticate (line 174) | def authenticate(self, c_key: str, c_secret: str) -> tuple[str, str]:
method get_track_from_album (line 203) | def get_track_from_album(
method item_candidates (line 217) | def item_candidates(
method album_for_id (line 228) | def album_for_id(self, album_id: str) -> AlbumInfo | None:
method track_for_id (line 257) | def track_for_id(self, track_id: str) -> TrackInfo | None:
method get_search_query_with_filters (line 264) | def get_search_query_with_filters(
method get_search_response (line 308) | def get_search_response(self, params: SearchParams) -> Sequence[IDResp...
method get_master_year (line 315) | def get_master_year(self, master_id: str) -> int | None:
method get_media_and_albumtype (line 342) | def get_media_and_albumtype(
method get_album_info (line 353) | def get_album_info(self, result: Release) -> AlbumInfo | None:
method select_cover_art (line 476) | def select_cover_art(self, result: Release) -> str | None:
method get_tracks (line 486) | def get_tracks(
method _coalesce_tracks (line 568) | def _coalesce_tracks(self, raw_tracklist: list[Track]) -> list[Track]:
method _add_merged_subtracks (line 613) | def _add_merged_subtracks(
method strip_disambiguation (line 656) | def strip_disambiguation(self, text: str) -> str:
method get_track_info (line 664) | def get_track_info(
method get_track_index (line 704) | def get_track_index(
method get_track_length (line 720) | def get_track_length(self, duration: str) -> int | None:
FILE: beetsplug/discogs/states.py
class ArtistState (line 40) | class ArtistState:
class ValidArtist (line 50) | class ValidArtist(NamedTuple):
method get_artist (line 64) | def get_artist(self, property_name: str) -> str:
method info (line 81) | def info(self) -> ArtistInfo:
method strip_disambiguation (line 85) | def strip_disambiguation(self, text: str) -> str:
method valid_artists (line 97) | def valid_artists(self) -> list[ValidArtist]:
method artists_ids (line 128) | def artists_ids(self) -> list[str]:
method artist_id (line 133) | def artist_id(self) -> str:
method artists (line 138) | def artists(self) -> list[str]:
method artists_credit (line 143) | def artists_credit(self) -> list[str]:
method artist (line 148) | def artist(self) -> str:
method artist_credit (line 153) | def artist_credit(self) -> str:
method join_artists (line 157) | def join_artists(self, property_name: str) -> str:
method from_config (line 178) | def from_config(
class TracklistState (line 196) | class TracklistState:
method info (line 206) | def info(self) -> TracklistInfo:
method build (line 210) | def build(
FILE: beetsplug/discogs/types.py
class ReleaseFormat (line 25) | class ReleaseFormat(TypedDict):
class Artist (line 31) | class Artist(TypedDict):
class Track (line 41) | class Track(TypedDict):
class ArtistInfo (line 51) | class ArtistInfo(TypedDict):
class TracklistInfo (line 60) | class TracklistInfo(TypedDict):
FILE: beetsplug/duplicates.py
class DuplicatesPlugin (line 34) | class DuplicatesPlugin(BeetsPlugin):
method __init__ (line 37) | def __init__(self):
method commands (line 144) | def commands(self):
method _process_item (line 215) | def _process_item(
method _checksum (line 245) | def _checksum(self, item, prog):
method _group_by (line 278) | def _group_by(self, objs, keys, strict):
method _order (line 308) | def _order(self, objs, tiebreak=None):
method _merge_items (line 345) | def _merge_items(self, objs):
method _merge_albums (line 369) | def _merge_albums(self, objs):
method _merge (line 392) | def _merge(self, objs):
method _duplicates (line 403) | def _duplicates(self, objs, keys, full, strict, tiebreak, merge):
FILE: beetsplug/edit.py
class ParseError (line 41) | class ParseError(Exception):
function edit (line 47) | def edit(filename, log):
function dump (line 58) | def dump(arg):
function load (line 67) | def load(s):
function _safe_value (line 90) | def _safe_value(obj, key, value):
function flatten (line 101) | def flatten(obj, fields):
function apply_ (line 127) | def apply_(obj, data):
class EditPlugin (line 145) | class EditPlugin(plugins.BeetsPlugin):
method __init__ (line 146) | def __init__(self):
method commands (line 163) | def commands(self):
method _edit_command (line 182) | def _edit_command(self, lib, opts, args):
method _get_fields (line 198) | def _get_fields(self, album, extra):
method edit (line 215) | def edit(self, album, objs, fields):
method edit_objects (line 230) | def edit_objects(self, objs, fields):
method apply_data (line 301) | def apply_data(self, objs, old_data, new_data):
method save_changes (line 332) | def save_changes(self, objs):
method before_choose_candidate_listener (line 342) | def before_choose_candidate_listener(self, session, task):
method importer_edit (line 356) | def importer_edit(self, session, task):
method importer_edit_candidate (line 385) | def importer_edit_candidate(self, session, task):
FILE: beetsplug/embedart.py
function _confirm (line 31) | def _confirm(objs, album):
class EmbedCoverArtPlugin (line 52) | class EmbedCoverArtPlugin(BeetsPlugin):
method __init__ (line 55) | def __init__(self):
method commands (line 89) | def commands(self):
method process_album (line 261) | def process_album(self, album):
method remove_artfile (line 275) | def remove_artfile(self, album):
method import_task_files (line 286) | def import_task_files(self, session, task):
FILE: beetsplug/embyupdate.py
function api_url (line 19) | def api_url(host, port, endpoint):
function password_data (line 52) | def password_data(username, password):
function create_headers (line 69) | def create_headers(user_id, token=None):
function get_token (line 97) | def get_token(host, port, headers, auth_data):
function get_user (line 122) | def get_user(host, port, username):
class EmbyUpdate (line 141) | class EmbyUpdate(BeetsPlugin):
method __init__ (line 142) | def __init__(self):
method listen_for_db_change (line 163) | def listen_for_db_change(self, lib, model):
method update (line 167) | def update(self, lib):
FILE: beetsplug/export.py
class ExportEncoder (line 30) | class ExportEncoder(json.JSONEncoder):
method default (line 33) | def default(self, o):
class ExportPlugin (line 39) | class ExportPlugin(BeetsPlugin):
method __init__ (line 40) | def __init__(self):
method commands (line 81) | def commands(self):
method run (line 123) | def run(self, lib, opts, args):
class ExportFormat (line 169) | class ExportFormat:
method __init__ (line 172) | def __init__(self, file_path, file_mode="w", encoding="utf-8"):
method factory (line 184) | def factory(cls, file_type, **kwargs):
method export (line 194) | def export(self, data, **kwargs):
class JsonFormat (line 198) | class JsonFormat(ExportFormat):
method __init__ (line 201) | def __init__(self, file_path, file_mode="w", encoding="utf-8"):
method export (line 204) | def export(self, data, **kwargs):
class CSVFormat (line 209) | class CSVFormat(ExportFormat):
method __init__ (line 212) | def __init__(self, file_path, file_mode="w", encoding="utf-8"):
method export (line 215) | def export(self, data, **kwargs):
class XMLFormat (line 222) | class XMLFormat(ExportFormat):
method __init__ (line 225) | def __init__(self, file_path, file_mode="w", encoding="utf-8"):
method export (line 228) | def export(self, data, **kwargs):
FILE: beetsplug/fetchart.py
class ImageAction (line 57) | class ImageAction(Enum):
class MetadataMatch (line 68) | class MetadataMatch(Enum):
class Candidate (line 78) | class Candidate:
method __init__ (line 83) | def __init__(
method _validate (line 100) | def _validate(
method validate (line 236) | def validate(
method resize (line 244) | def resize(self, plugin: FetchArtPlugin) -> None:
method _resize (line 261) | def _resize(
function _logged_get (line 298) | def _logged_get(log: Logger, *args, **kwargs) -> requests.Response:
class RequestMixin (line 338) | class RequestMixin:
method request (line 345) | def request(self, *args, **kwargs) -> requests.Response:
class ArtSource (line 356) | class ArtSource(RequestMixin, ABC):
method __init__ (line 367) | def __init__(
method description (line 378) | def description(self) -> str:
method add_default_config (line 382) | def add_default_config(config: confuse.ConfigView) -> None:
method available (line 386) | def available(cls, log: Logger, config: confuse.ConfigView) -> bool:
method get (line 393) | def get(
method _candidate (line 401) | def _candidate(self, **kwargs) -> Candidate:
method fetch_image (line 405) | def fetch_image(self, candidate: Candidate, plugin: FetchArtPlugin) ->...
method cleanup (line 414) | def cleanup(self, candidate: Candidate) -> None:
class LocalArtSource (line 418) | class LocalArtSource(ArtSource):
method fetch_image (line 421) | def fetch_image(self, candidate: Candidate, plugin: FetchArtPlugin) ->...
class RemoteArtSource (line 425) | class RemoteArtSource(ArtSource):
method fetch_image (line 428) | def fetch_image(self, candidate: Candidate, plugin: FetchArtPlugin) ->...
method cleanup (line 512) | def cleanup(self, candidate: Candidate) -> None:
class CoverArtArchive (line 520) | class CoverArtArchive(RemoteArtSource):
method get (line 529) | def get(
class Amazon (line 595) | class Amazon(RemoteArtSource):
method get (line 601) | def get(
class AlbumArtOrg (line 616) | class AlbumArtOrg(RemoteArtSource):
method get (line 622) | def get(
class GoogleImages (line 648) | class GoogleImages(RemoteArtSource):
method __init__ (line 653) | def __init__(self, *args, **kwargs):
method add_default_config (line 659) | def add_default_config(config: confuse.ConfigView):
method available (line 670) | def available(cls, log: Logger, config: confuse.ConfigView) -> bool:
method get (line 676) | def get(
class FanartTV (line 722) | class FanartTV(RemoteArtSource):
method __init__ (line 731) | def __init__(self, *args, **kwargs):
method add_default_config (line 736) | def add_default_config(config: confuse.ConfigView):
method get (line 744) | def get(
class ITunesStore (line 809) | class ITunesStore(RemoteArtSource):
method get (line 814) | def get(
class Wikipedia (line 893) | class Wikipedia(RemoteArtSource):
method get (line 920) | def get(
class FileSystem (line 1041) | class FileSystem(LocalArtSource):
method filename_priority (line 1046) | def filename_priority(
method get (line 1057) | def get(
class LastFM (line 1128) | class LastFM(RemoteArtSource):
method __init__ (line 1145) | def __init__(self, *args, **kwargs) -> None:
method add_default_config (line 1150) | def add_default_config(config: confuse.ConfigView) -> None:
method available (line 1159) | def available(cls, log: Logger, config: confuse.ConfigView) -> bool:
method get (line 1165) | def get(
class Spotify (line 1219) | class Spotify(RemoteArtSource):
method available (line 1226) | def available(cls, log: Logger, config: confuse.ConfigView) -> bool:
method get (line 1235) | def get(
class CoverArtUrl (line 1274) | class CoverArtUrl(RemoteArtSource):
method get (line 1283) | def get(
class FetchArtPlugin (line 1326) | class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
method __init__ (line 1330) | def __init__(self) -> None:
method _is_source_file_removal_enabled (line 1445) | def _is_source_file_removal_enabled() -> bool:
method _is_candidate_fallback (line 1450) | def _is_candidate_fallback(self, candidate: Candidate) -> bool:
method fetch_art (line 1461) | def fetch_art(self, session: ImportSession, task: ImportTask) -> None:
method _set_art (line 1487) | def _set_art(
method assign_art (line 1500) | def assign_art(self, session: ImportSession, task: ImportTask):
method commands (line 1512) | def commands(self) -> list[ui.Subcommand]:
method art_for_album (line 1539) | def art_for_album(
method batch_fetch_art (line 1583) | def batch_fetch_art(
FILE: beetsplug/filefilter.py
class FileFilterPlugin (line 25) | class FileFilterPlugin(BeetsPlugin):
method __init__ (line 26) | def __init__(self):
method import_task_created_event (line 47) | def import_task_created_event(self, session, task):
method file_filter (line 67) | def file_filter(self, full_path):
FILE: beetsplug/fish.py
class FishPlugin (line 69) | class FishPlugin(BeetsPlugin):
method commands (line 70) | def commands(self):
method run (line 98) | def run(self, lib, opts, args):
function _escape (line 141) | def _escape(name):
function get_cmds_list (line 148) | def get_cmds_list(cmds_names):
function get_standard_fields (line 153) | def get_standard_fields(fields):
function get_extravalues (line 159) | def get_extravalues(lib, extravalues):
function get_set_of_values_for_field (line 170) | def get_set_of_values_for_field(lib, fields):
function get_basic_beet_options (line 181) | def get_basic_beet_options():
function get_subcommands (line 204) | def get_subcommands(cmd_name_and_help, nobasicfields, extravalues):
function get_all_commands (line 236) | def get_all_commands(beetcmds):
function clean_whitespace (line 287) | def clean_whitespace(word):
function wrap (line 292) | def wrap(word):
FILE: beetsplug/freedesktop.py
class FreedesktopPlugin (line 21) | class FreedesktopPlugin(BeetsPlugin):
method commands (line 22) | def commands(self):
method deprecation_message (line 30) | def deprecation_message(self, lib, opts, args):
FILE: beetsplug/fromfilename.py
function equal (line 45) | def equal(seq):
function equal_fields (line 50) | def equal_fields(matchdict, field):
function all_matches (line 58) | def all_matches(names, pattern):
function bad_title (line 77) | def bad_title(title):
function apply_matches (line 87) | def apply_matches(d, log):
class FromFilenamePlugin (line 137) | class FromFilenamePlugin(plugins.BeetsPlugin):
method __init__ (line 138) | def __init__(self):
method filename_task (line 142) | def filename_task(self, task, session):
FILE: beetsplug/ftintitle.py
function split_on_feat (line 54) | def split_on_feat(
function contains_feat (line 94) | def contains_feat(title: str, custom_words: list[str] | None = None) -> ...
function find_feat_part (line 105) | def find_feat_part(
function _album_artist_no_feat (line 144) | def _album_artist_no_feat(album: Album) -> str:
class FtInTitlePlugin (line 149) | class FtInTitlePlugin(plugins.BeetsPlugin):
method bracket_keywords (line 151) | def bracket_keywords(self) -> list[str]:
method _bracket_position_pattern (line 156) | def _bracket_position_pattern(keywords: tuple[str, ...]) -> re.Pattern...
method __init__ (line 184) | def __init__(self) -> None:
method commands (line 219) | def commands(self) -> list[ui.Subcommand]:
method imported (line 245) | def imported(self, session: ImportSession, task: ImportTask) -> None:
method update_metadata (line 262) | def update_metadata(
method ft_in_title (line 304) | def ft_in_title(
method find_bracket_position (line 347) | def find_bracket_position(
method insert_ft_into_title (line 358) | def insert_ft_into_title(
FILE: beetsplug/fuzzy.py
class FuzzyQuery (line 24) | class FuzzyQuery(StringFieldQuery[str]):
method __init__ (line 25) | def __init__(self, field_name: str, pattern: str, *_) -> None:
method string_match (line 30) | def string_match(cls, pattern: str, val: str) -> bool:
class FuzzyPlugin (line 51) | class FuzzyPlugin(BeetsPlugin):
method __init__ (line 52) | def __init__(self) -> None:
method queries (line 61) | def queries(self):
FILE: beetsplug/hook.py
class BytesToStrFormatter (line 28) | class BytesToStrFormatter(string.Formatter):
method convert_field (line 31) | def convert_field(self, value: Any, conversion: str | None) -> Any:
class HookPlugin (line 44) | class HookPlugin(BeetsPlugin):
method __init__ (line 47) | def __init__(self):
method create_and_register_hook (line 62) | def create_and_register_hook(self, event, command):
FILE: beetsplug/ihate.py
function summary (line 26) | def summary(task):
class IHatePlugin (line 36) | class IHatePlugin(BeetsPlugin):
method __init__ (line 37) | def __init__(self):
method do_i_hate_this (line 50) | def do_i_hate_this(cls, task, action_patterns):
method import_task_choice_event (line 64) | def import_task_choice_event(self, session, task):
FILE: beetsplug/importadded.py
class ImportAddedPlugin (line 13) | class ImportAddedPlugin(BeetsPlugin):
method __init__ (line 14) | def __init__(self):
method check_config (line 43) | def check_config(self, task, session):
method reimported_item (line 46) | def reimported_item(self, item):
method reimported_album (line 49) | def reimported_album(self, album):
method record_if_inplace (line 52) | def record_if_inplace(self, task, session):
method record_reimported (line 71) | def record_reimported(self, task, session):
method write_file_mtime (line 79) | def write_file_mtime(self, path, mtime):
method write_item_mtime (line 84) | def write_item_mtime(self, item, mtime):
method record_import_mtime (line 92) | def record_import_mtime(self, item, source, destination):
method update_album_times (line 103) | def update_album_times(self, lib, album):
method update_item_times (line 128) | def update_item_times(self, lib, item):
method update_after_write_time (line 146) | def update_after_write_time(self, item, path):
FILE: beetsplug/importfeeds.py
function _build_m3u_session_filename (line 32) | def _build_m3u_session_filename(basename):
function _build_m3u_filename (line 45) | def _build_m3u_filename(basename):
function _write_m3u (line 59) | def _write_m3u(m3u_path, items_paths):
class ImportFeedsPlugin (line 67) | class ImportFeedsPlugin(BeetsPlugin):
method __init__ (line 68) | def __init__(self):
method get_feeds_dir (line 91) | def get_feeds_dir(self):
method _record_items (line 97) | def _record_items(self, lib, basename, items):
method album_imported (line 141) | def album_imported(self, lib, album):
method item_imported (line 144) | def item_imported(self, lib, item):
method import_begin (line 147) | def import_begin(self, session):
FILE: beetsplug/importsource.py
class ImportSourcePlugin (line 17) | class ImportSourcePlugin(BeetsPlugin):
method __init__ (line 20) | def __init__(self):
method prevent_suggest_removal (line 41) | def prevent_suggest_removal(self, session, task):
method import_stage (line 48) | def import_stage(self, _, task):
method suggest_removal (line 61) | def suggest_removal(self, item):
FILE: beetsplug/info.py
function tag_data (line 27) | def tag_data(lib, args, album=False):
function tag_fields (line 41) | def tag_fields():
function tag_data_emitter (line 47) | def tag_data_emitter(path):
function library_data (line 72) | def library_data(lib, args, album=False):
function library_data_emitter (line 77) | def library_data_emitter(item):
function update_summary (line 86) | def update_summary(summary, tags):
function print_data (line 95) | def print_data(data, item=None, fmt=None):
function print_data_keys (line 131) | def print_data_keys(data, item=None):
class InfoPlugin (line 148) | class InfoPlugin(BeetsPlugin):
method commands (line 149) | def commands(self):
method run (line 187) | def run(self, lib, opts, args):
FILE: beetsplug/inline.py
class InlineError (line 26) | class InlineError(Exception):
method __init__ (line 29) | def __init__(self, code, exc):
function _compile_func (line 35) | def _compile_func(body):
class InlinePlugin (line 47) | class InlinePlugin(BeetsPlugin):
method __init__ (line 48) | def __init__(self):
method compile_inline (line 75) | def compile_inline(self, python_code, album, field_name):
FILE: beetsplug/ipfs.py
class IPFSPlugin (line 25) | class IPFSPlugin(BeetsPlugin):
method __init__ (line 26) | def __init__(self):
method commands (line 38) | def commands(self):
method auto_add (line 104) | def auto_add(self, session, task):
method ipfs_play (line 109) | def ipfs_play(self, lib, opts, args):
method ipfs_add (line 118) | def ipfs_add(self, album):
method ipfs_get (line 164) | def ipfs_get(self, lib, query):
method ipfs_get_from_hash (line 176) | def ipfs_get_from_hash(self, lib, _hash):
method ipfs_publish (line 197) | def ipfs_publish(self, lib):
method ipfs_import (line 213) | def ipfs_import(self, lib, args):
method already_added (line 252) | def already_added(self, check, jlib):
method ipfs_list (line 258) | def ipfs_list(self, lib, args):
method query (line 269) | def query(self, lib, args):
method get_remote_lib (line 274) | def get_remote_lib(self, lib):
method ipfs_added_albums (line 282) | def ipfs_added_albums(self, rlib, tmpname):
method create_new_album (line 293) | def create_new_album(self, album, tmplib):
FILE: beetsplug/keyfinder.py
class KeyFinderPlugin (line 24) | class KeyFinderPlugin(BeetsPlugin):
method __init__ (line 25) | def __init__(self):
method commands (line 38) | def commands(self):
method command (line 45) | def command(self, lib, opts, args):
method imported (line 48) | def imported(self, session, task):
method find_key (line 51) | def find_key(self, items, write=False):
FILE: beetsplug/kodiupdate.py
function update_kodi (line 31) | def update_kodi(host, port, user, password):
class KodiUpdate (line 53) | class KodiUpdate(BeetsPlugin):
method __init__ (line 54) | def __init__(self):
method listen_for_db_change (line 66) | def listen_for_db_change(self, lib, model):
method update (line 70) | def update(self, lib):
FILE: beetsplug/lastgenre/__init__.py
function flatten_tree (line 58) | def flatten_tree(
function find_parents (line 79) | def find_parents(candidate: str, branches: CanonTree) -> list[str]:
function get_depth (line 92) | def get_depth(tag: str, branches: CanonTree) -> int | None:
function sort_by_depth (line 100) | def sort_by_depth(tags: list[str], branches: CanonTree) -> list[str]:
class LastGenrePlugin (line 114) | class LastGenrePlugin(plugins.BeetsPlugin):
method __init__ (line 115) | def __init__(self) -> None:
method setup (line 137) | def setup(self) -> None:
method _load_whitelist (line 149) | def _load_whitelist(self) -> Whitelist:
method _load_c14n_tree (line 167) | def _load_c14n_tree(self) -> tuple[CanonTree, bool]:
method sources (line 191) | def sources(self) -> tuple[str, ...]:
method _resolve_genres (line 205) | def _resolve_genres(self, tags: list[str]) -> list[str]:
method _filter_valid (line 262) | def _filter_valid(self, genres: Iterable[str]) -> list[str]:
method _format_genres (line 278) | def _format_genres(self, tags: list[str]) -> list[str]:
method _get_existing_genres (line 285) | def _get_existing_genres(self, obj: LibModel) -> list[str]:
method _combine_resolve_and_log (line 294) | def _combine_resolve_and_log(
method _get_genre (line 303) | def _get_genre(self, obj: LibModel) -> tuple[list[str], str]:
method _fetch_and_log_genre (line 458) | def _fetch_and_log_genre(self, obj: LibModel) -> None:
method _process (line 467) | def _process(self, obj: LibModel, write: bool) -> None:
method _process_track (line 472) | def _process_track(self, obj: Item, write: bool) -> None:
method _process_album (line 479) | def _process_album(self, obj: Album, write: bool) -> None:
method commands (line 491) | def commands(self) -> list[ui.Subcommand]:
method imported (line 562) | def imported(self, _: ImportSession, task: ImportTask) -> None:
FILE: beetsplug/lastgenre/client.py
class LastFmClient (line 48) | class LastFmClient:
method __init__ (line 51) | def __init__(self, log: BeetsLogger, min_weight: int):
method fetch_genre (line 60) | def fetch_genre(
method _tags_for (line 68) | def _tags_for(
method _last_lookup (line 108) | def _last_lookup(
method fetch_album_genre (line 132) | def fetch_album_genre(self, albumartist: str, albumtitle: str) -> list...
method fetch_artist_genre (line 138) | def fetch_artist_genre(self, artist: str) -> list[str]:
method fetch_track_genre (line 142) | def fetch_track_genre(self, trackartist: str, tracktitle: str) -> list...
FILE: beetsplug/lastimport.py
class LastImportPlugin (line 25) | class LastImportPlugin(plugins.BeetsPlugin):
method __init__ (line 26) | def __init__(self):
method commands (line 46) | def commands(self):
class CustomUser (line 56) | class CustomUser(pylast.User):
method __init__ (line 63) | def __init__(self, *args, **kwargs):
method _get_things (line 66) | def _get_things(
method get_top_tracks_by_page (line 91) | def get_top_tracks_by_page(
function import_lastfm (line 116) | def import_lastfm(lib, log):
function fetch_tracks (line 173) | def fetch_tracks(user, page, limit):
function process_tracks (line 200) | def process_tracks(lib, tracks, log):
FILE: beetsplug/limit.py
function lslimit (line 31) | def lslimit(lib, opts, args):
class LimitPlugin (line 67) | class LimitPlugin(BeetsPlugin):
method commands (line 70) | def commands(self):
method queries (line 74) | def queries(self):
FILE: beetsplug/listenbrainz.py
class ListenBrainzPlugin (line 14) | class ListenBrainzPlugin(MusicBrainzAPIMixin, BeetsPlugin):
method __init__ (line 19) | def __init__(self):
method commands (line 27) | def commands(self):
method _lbupdate (line 39) | def _lbupdate(self, lib, log):
method _make_request (line 54) | def _make_request(self, url, params=None):
method get_listens (line 69) | def get_listens(self, min_ts=None, max_ts=None, count=None):
method get_tracks_from_listens (line 106) | def get_tracks_from_listens(self, listens):
method get_mb_recording_id (line 133) | def get_mb_recording_id(self, track) -> str | None:
method get_playlists_createdfor (line 144) | def get_playlists_createdfor(self, username):
method get_listenbrainz_playlists (line 149) | def get_listenbrainz_playlists(self):
method get_playlist (line 184) | def get_playlist(self, identifier):
method get_tracks_from_playlist (line 189) | def get_tracks_from_playlist(self, playlist):
method get_track_info (line 206) | def get_track_info(self, tracks):
method get_weekly_playlist (line 238) | def get_weekly_playlist(self, playlist_type, most_recent=True):
method get_weekly_exploration (line 261) | def get_weekly_exploration(self):
method get_weekly_jams (line 264) | def get_weekly_jams(self):
method get_last_weekly_exploration (line 267) | def get_last_weekly_exploration(self):
method get_last_weekly_jams (line 270) | def get_last_weekly_jams(self):
FILE: beetsplug/loadext.py
class LoadExtPlugin (line 23) | class LoadExtPlugin(BeetsPlugin):
method __init__ (line 24) | def __init__(self):
method library_opened (line 36) | def library_opened(self, lib):
FILE: beetsplug/lyrics.py
class CaptchaError (line 62) | class CaptchaError(requests.exceptions.HTTPError):
method __init__ (line 63) | def __init__(self, *args, **kwargs) -> None:
class GeniusHTTPError (line 67) | class GeniusHTTPError(requests.exceptions.HTTPError):
function search_pairs (line 74) | def search_pairs(item):
function slug (line 150) | def slug(text: str) -> str:
class LyricsRequestHandler (line 164) | class LyricsRequestHandler(RequestHandler):
method status_to_error (line 167) | def status_to_error(self, code: int) -> type[requests.HTTPError] | None:
method debug (line 176) | def debug(self, message: str, *args) -> None:
method info (line 180) | def info(self, message: str, *args) -> None:
method warn (line 184) | def warn(self, message: str, *args) -> None:
method format_url (line 189) | def format_url(url: str, params: JSONDict | None) -> str:
method get_text (line 195) | def get_text(
method get_json (line 209) | def get_json(self, url: str, params: JSONDict | None = None, **kwargs):
method post_json (line 215) | def post_json(self, url: str, params: JSONDict | None = None, **kwargs):
method handle_request (line 222) | def handle_request(self) -> Iterator[None]:
class BackendClass (line 231) | class BackendClass(type):
method name (line 233) | def name(cls) -> str:
class Backend (line 238) | class Backend(LyricsRequestHandler, metaclass=BackendClass):
method __init__ (line 241) | def __init__(self, config: confuse.Subview, log: Logger) -> None:
method fetch (line 245) | def fetch(
class LRCLyrics (line 254) | class LRCLyrics:
method __le__ (line 267) | def __le__(self, other: LRCLyrics) -> bool:
method verify_synced_lyrics (line 272) | def verify_synced_lyrics(
method make (line 288) | def make(
method duration_dist (line 305) | def duration_dist(self) -> float:
method is_valid (line 310) | def is_valid(self) -> bool:
method dist (line 321) | def dist(self) -> tuple[bool, float]:
method get_text (line 333) | def get_text(self, want_synced: bool) -> str:
class LRCLib (line 344) | class LRCLib(Backend):
method fetch_candidates (line 351) | def fetch_candidates(
method pick_best_match (line 375) | def pick_best_match(cls, lyrics: Iterable[LRCLyrics]) -> LRCLyrics | N...
method fetch (line 379) | def fetch(
class MusiXmatch (line 396) | class MusiXmatch(Backend):
method encode (line 409) | def encode(cls, text: str) -> str:
method build_url (line 416) | def build_url(cls, *args: str) -> str:
method fetch (line 419) | def fetch(self, artist: str, title: str, *_) -> Lyrics | None:
class Html (line 444) | class Html:
method normalize_space (line 469) | def normalize_space(cls, text: str) -> str:
method remove_ads (line 474) | def remove_ads(cls, text: str) -> str:
method merge_paragraphs (line 478) | def merge_paragraphs(cls, text: str) -> str:
class SoupMixin (line 482) | class SoupMixin:
method pre_process_html (line 484) | def pre_process_html(cls, html: str) -> str:
method get_soup (line 489) | def get_soup(cls, html: str) -> BeautifulSoup:
class SearchResult (line 493) | class SearchResult(NamedTuple):
method source (line 499) | def source(self) -> str:
class SearchBackend (line 503) | class SearchBackend(SoupMixin, Backend):
method dist_thresh (line 505) | def dist_thresh(self) -> float:
method check_match (line 508) | def check_match(
method search (line 534) | def search(self, artist: str, title: str) -> Iterable[SearchResult]:
method get_results (line 538) | def get_results(self, artist: str, title: str) -> Iterable[SearchResult]:
method fetch (line 544) | def fetch(self, artist: str, title: str, *_) -> Lyrics | None:
method scrape (line 555) | def scrape(cls, html: str) -> str | None:
class Genius (line 560) | class Genius(SearchBackend):
method headers (line 573) | def headers(self) -> dict[str, str]:
method get_json (line 576) | def get_json(self, *args, **kwargs) -> GeniusAPI.Search:
method search (line 584) | def search(self, artist: str, title: str) -> Iterable[SearchResult]:
method scrape (line 594) | def scrape(cls, html: str) -> str | None:
class Tekstowo (line 602) | class Tekstowo(SearchBackend):
method build_url (line 608) | def build_url(self, artist, title):
method search (line 613) | def search(self, artist: str, title: str) -> Iterable[SearchResult]:
method scrape (line 625) | def scrape(cls, html: str) -> str | None:
class Google (line 634) | class Google(SearchBackend):
method pre_process_html (line 678) | def pre_process_html(cls, html: str) -> str:
method get_text (line 683) | def get_text(self, *args, **kwargs) -> str:
method get_part_dist (line 694) | def get_part_dist(artist: str, title: str, part: str) -> float:
method make_search_result (line 703) | def make_search_result(
method search (line 734) | def search(self, artist: str, title: str) -> Iterable[SearchResult]:
method get_results (line 750) | def get_results(self, *args) -> Iterable[SearchResult]:
method scrape (line 760) | def scrape(cls, html: str) -> str | None:
class Translator (line 769) | class Translator(LyricsRequestHandler):
method from_config (line 781) | def from_config(
method get_translations (line 796) | def get_translations(self, texts: Iterable[str]) -> list[str]:
method translate (line 816) | def translate(self, lyrics: Lyrics, old_lyrics: Lyrics) -> Lyrics:
class RestFiles (line 855) | class RestFiles:
method artists_dir (line 890) | def artists_dir(self) -> Path:
method write_indexes (line 895) | def write_indexes(self) -> None:
method write_artist (line 908) | def write_artist(self, artist: str, items: Iterable[Item]) -> None:
method write (line 927) | def write(self, items: list[Item]) -> None:
class LyricsPlugin (line 950) | class LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin):
method backends (line 959) | def backends(self) -> list[Backend]:
method translator (line 970) | def translator(self) -> Translator | None:
method __init__ (line 976) | def __init__(self):
method commands (line 1015) | def commands(self):
method imported (line 1066) | def imported(self, _, task: ImportTask) -> None:
method find_lyrics (line 1071) | def find_lyrics(self, item: Item) -> Lyrics | None:
method add_item_lyrics (line 1082) | def add_item_lyrics(self, item: Item, write: bool) -> None:
method get_lyrics (line 1125) | def get_lyrics(self, artist: str, title: str, *args) -> Lyrics | None:
FILE: beetsplug/mbcollection.py
class MusicBrainzUserAPI (line 45) | class MusicBrainzUserAPI(MusicBrainzAPI):
method __post_init__ (line 61) | def __post_init__(self) -> None:
method request (line 69) | def request(self, *args, **kwargs) -> Response:
method browse_collections (line 76) | def browse_collections(self) -> list[JSONDict]:
class MBCollection (line 82) | class MBCollection:
method id (line 97) | def id(self) -> str:
method release_count (line 102) | def release_count(self) -> int:
method releases_url (line 107) | def releases_url(self) -> str:
method releases (line 112) | def releases(self) -> list[JSONDict]:
method get_releases (line 121) | def get_releases(self, offset: int) -> list[JSONDict]:
method get_id_chunks (line 129) | def get_id_chunks(cls, id_list: list[str]) -> Iterator[list[str]]:
method add_releases (line 138) | def add_releases(self, releases: list[str]) -> None:
method remove_releases (line 144) | def remove_releases(self, releases: list[str]) -> None:
function submit_albums (line 151) | def submit_albums(collection: MBCollection, release_ids):
class MusicBrainzCollectionPlugin (line 158) | class MusicBrainzCollectionPlugin(BeetsPlugin):
method __init__ (line 159) | def __init__(self) -> None:
method mb_api (line 172) | def mb_api(self) -> MusicBrainzUserAPI:
method collection (line 176) | def collection(self) -> MBCollection:
method commands (line 198) | def commands(self):
method update_collection (line 211) | def update_collection(self, lib: Library, opts, args) -> None:
method imported (line 216) | def imported(self, session: ImportSession, task: ImportTask) -> None:
method update_album_list (line 223) | def update_album_list(
FILE: beetsplug/mbpseudo.py
class MusicBrainzPseudoReleasePlugin (line 49) | class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
method __init__ (line 50) | def __init__(self) -> None:
method _on_plugins_loaded (line 100) | def _on_plugins_loaded(self):
method candidates (line 111) | def candidates(
method album_info (line 136) | def album_info(self, release: JSONDict) -> AlbumInfo:
method _intercept_mb_release (line 164) | def _intercept_mb_release(self, data: JSONDict) -> list[str]:
method _has_desired_script (line 176) | def _has_desired_script(self, release: JSONDict) -> bool:
method _wanted_pseudo_release_id (line 184) | def _wanted_pseudo_release_id(
method _replace_artist_with_alias (line 208) | def _replace_artist_with_alias(
method _add_custom_tags (line 243) | def _add_custom_tags(
method _adjust_final_album_match (line 260) | def _adjust_final_album_match(self, match: AlbumMatch):
method _extract_id (line 275) | def _extract_id(self, url: str) -> str | None:
class PseudoAlbumInfo (line 279) | class PseudoAlbumInfo(AlbumInfo):
method __init__ (line 294) | def __init__(
method get_official_release (line 307) | def get_official_release(self) -> AlbumInfo:
method determine_best_ref (line 310) | def determine_best_ref(self, items: Sequence[Item]) -> str:
method _compute_distance (line 324) | def _compute_distance(self, items: Sequence[Item]) -> Distance:
method use_pseudo_as_ref (line 328) | def use_pseudo_as_ref(self):
method use_official_as_ref (line 331) | def use_official_as_ref(self):
method __getattr__ (line 334) | def __getattr__(self, attr: str) -> Any:
method __deepcopy__ (line 341) | def __deepcopy__(self, memo):
FILE: beetsplug/mbsubmit.py
class MBSubmitPlugin (line 33) | class MBSubmitPlugin(BeetsPlugin):
method __init__ (line 34) | def __init__(self):
method before_choose_candidate_event (line 59) | def before_choose_candidate_event(self, session, task):
method picard (line 66) | def picard(self, session, task):
method print_tracks (line 77) | def print_tracks(self, session, task):
method commands (line 81) | def commands(self):
method _mbsubmit (line 95) | def _mbsubmit(self, items):
FILE: beetsplug/mbsync.py
class MBSyncPlugin (line 23) | class MBSyncPlugin(BeetsPlugin):
method __init__ (line 24) | def __init__(self):
method commands (line 27) | def commands(self):
method func (line 61) | def func(self, lib, opts, args):
method singletons (line 70) | def singletons(self, lib, query, move, pretend, write):
method albums (line 96) | def albums(self, lib, query, move, pretend, write):
FILE: beetsplug/metasync/__init__.py
class MetaSource (line 40) | class MetaSource(metaclass=ABCMeta):
method __init__ (line 43) | def __init__(self, config, log):
method sync_from_source (line 48) | def sync_from_source(self, item):
function load_meta_sources (line 52) | def load_meta_sources():
function load_item_types (line 68) | def load_item_types():
class MetaSyncPlugin (line 76) | class MetaSyncPlugin(BeetsPlugin):
method __init__ (line 79) | def __init__(self):
method commands (line 82) | def commands(self):
method func (line 104) | def func(self, lib, opts, args):
FILE: beetsplug/metasync/amarok.py
function import_dbus (line 28) | def import_dbus():
class Amarok (line 38) | class Amarok(MetaSource):
method __init__ (line 55) | def __init__(self, config, log):
method sync_from_source (line 65) | def sync_from_source(self, item):
FILE: beetsplug/metasync/itunes.py
function create_temporary_copy (line 35) | def create_temporary_copy(path):
function _norm_itunes_path (line 45) | def _norm_itunes_path(path):
class Itunes (line 61) | class Itunes(MetaSource):
method __init__ (line 71) | def __init__(self, config, log):
method sync_from_source (line 104) | def sync_from_source(self, item):
FILE: beetsplug/missing.py
function _missing_count (line 60) | def _missing_count(album):
function _item (line 65) | def _item(track_info, album_info, album_id):
class MissingPlugin (line 115) | class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin):
method __init__ (line 122) | def __init__(self):
method commands (line 172) | def commands(self):
method _missing_tracks (line 183) | def _missing_tracks(self, lib, query):
method _missing_albums (line 210) | def _missing_albums(self, lib: Library, query: list[str]) -> None:
method _missing (line 263) | def _missing(self, album: Album) -> Iterator[Item]:
FILE: beetsplug/mpdstats.py
function is_url (line 37) | def is_url(path):
class MPDClientWrapper (line 44) | class MPDClientWrapper:
method __init__ (line 45) | def __init__(self, log):
method connect (line 60) | def connect(self):
method disconnect (line 81) | def disconnect(self):
method get (line 86) | def get(self, command, retries=RETRIES):
method currentsong (line 109) | def currentsong(self):
method status (line 130) | def status(self):
method events (line 134) | def events(self):
class MPDStats (line 141) | class MPDStats:
method __init__ (line 142) | def __init__(self, lib, log):
method rating (line 155) | def rating(self, play_count, skip_count, rating, skipped):
method get_item (line 166) | def get_item(self, path):
method update_item (line 175) | def update_item(self, item, attribute, value=None, increment=None):
method update_rating (line 198) | def update_rating(self, item, skipped):
method handle_song_change (line 215) | def handle_song_change(self, song):
method handle_played (line 235) | def handle_played(self, song):
method handle_skipped (line 240) | def handle_skipped(self, song):
method on_stop (line 245) | def on_stop(self, status):
method on_pause (line 255) | def on_pause(self, status):
method on_play (line 259) | def on_play(self, status):
method run (line 303) | def run(self):
class MPDStatsPlugin (line 321) | class MPDStatsPlugin(plugins.BeetsPlugin):
method __init__ (line 329) | def __init__(self):
method commands (line 345) | def commands(self):
FILE: beetsplug/mpdupdate.py
class BufferedSocket (line 34) | class BufferedSocket:
method __init__ (line 37) | def __init__(self, host, port, sep=b"\n"):
method readline (line 47) | def readline(self):
method send (line 59) | def send(self, data):
method close (line 62) | def close(self):
class MPDUpdatePlugin (line 66) | class MPDUpdatePlugin(BeetsPlugin):
method __init__ (line 67) | def __init__(self):
method db_change (line 86) | def db_change(self, lib, model):
method update (line 89) | def update(self, lib):
method update_mpd (line 96) | def update_mpd(self, host="localhost", port=6600, password=None):
FILE: beetsplug/musicbrainz.py
function _preferred_alias (line 74) | def _preferred_alias(
function _key_with_preferred_alias (line 108) | def _key_with_preferred_alias(obj: JSONDict, key: str) -> str:
function _multi_artist_credit (line 113) | def _multi_artist_credit(
function track_url (line 156) | def track_url(trackid: str) -> str:
function _flatten_artist_credit (line 160) | def _flatten_artist_credit(credit: list[JSONDict]) -> tuple[str, str, str]:
function _artist_ids (line 175) | def _artist_ids(credit: list[JSONDict]) -> list[str]:
function _get_related_artist_names (line 188) | def _get_related_artist_names(relations, relation_type):
function album_url (line 201) | def album_url(albumid: str) -> str:
function _preferred_release_event (line 205) | def _preferred_release_event(
function _set_date_str (line 228) | def _set_date_str(
function _merge_pseudo_and_actual_album (line 252) | def _merge_pseudo_and_actual_album(
class MusicBrainzPlugin (line 292) | class MusicBrainzPlugin(
method genres_field (line 296) | def genres_field(self) -> str:
method __init__ (line 299) | def __init__(self):
method track_info (line 329) | def track_info(
method album_info (line 434) | def album_info(self, release: JSONDict) -> beets.autotag.hooks.AlbumInfo:
method extra_mb_field_by_tag (line 694) | def extra_mb_field_by_tag(self) -> dict[str, str]:
method get_album_criteria (line 710) | def get_album_criteria(
method get_search_query_with_filters (line 732) | def get_search_query_with_filters(
method get_search_response (line 751) | def get_search_response(self, params: SearchParams) -> Sequence[IDResp...
method album_for_id (line 761) | def album_for_id(
method track_for_id (line 804) | def track_for_id(
FILE: beetsplug/parentwork.py
class ParentWorkPlugin (line 31) | class ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin):
method __init__ (line 32) | def __init__(self):
method commands (line 45) | def commands(self):
method imported (line 74) | def imported(self, session, task):
method get_info (line 82) | def get_info(self, item, work_info):
method find_work (line 125) | def find_work(self, item, force, verbose):
method find_parentwork_info (line 194) | def find_parentwork_info(
FILE: beetsplug/permissions.py
function convert_perm (line 17) | def convert_perm(perm):
function check_permissions (line 27) | def check_permissions(path, permission):
function assert_permissions (line 34) | def assert_permissions(path, permission, log):
function dirs_in_library (line 48) | def dirs_in_library(library, item):
class Permissions (line 55) | class Permissions(BeetsPlugin):
method __init__ (line 56) | def __init__(self):
method fix (line 71) | def fix(self, lib, item=None, album=None):
method fix_art (line 84) | def fix_art(self, album):
method set_permissions (line 89) | def set_permissions(self, files=[], dirs=[]):
FILE: beetsplug/play.py
function play (line 38) | def play(
class PlayPlugin (line 66) | class PlayPlugin(BeetsPlugin):
method __init__ (line 67) | def __init__(self):
method commands (line 85) | def commands(self):
method _play_command (line 111) | def _play_command(self, lib, opts, args):
method _command_str (line 174) | def _command_str(self, args=None):
method _playlist_or_paths (line 189) | def _playlist_or_paths(self, paths):
method _exceeds_threshold (line 197) | def _exceeds_threshold(
method _create_tmp_playlist (line 222) | def _create_tmp_playlist(self, paths_list):
method before_choose_candidate_listener (line 235) | def before_choose_candidate_listener(self, session, task):
method importer_play (line 239) | def importer_play(self, session, task):
FILE: beetsplug/playlist.py
function is_m3u_file (line 30) | def is_m3u_file(path: str) -> bool:
class PlaylistQuery (line 34) | class PlaylistQuery(InQuery[bytes]):
method subvals (line 38) | def subvals(self) -> Sequence[BLOB_TYPE]:
method __init__ (line 41) | def __init__(self, _, pattern: str, __):
class PlaylistPlugin (line 89) | class PlaylistPlugin(beets.plugins.BeetsPlugin):
method __init__ (line 94) | def __init__(self):
method item_moved (line 124) | def item_moved(self, item, source, destination):
method item_removed (line 127) | def item_removed(self, item):
method cli_exit (line 131) | def cli_exit(self, lib):
method find_playlists (line 145) | def find_playlists(self):
method update_playlist (line 160) | def update_playlist(self, filename, base_dir):
FILE: beetsplug/plexupdate.py
function get_music_section (line 20) | def get_music_section(
function update_plex (line 41) | def update_plex(host, port, token, library_name, secure, ignore_cert_err...
function append_token (line 66) | def append_token(url, token):
function get_protocol (line 73) | def get_protocol(secure):
class PlexUpdate (line 80) | class PlexUpdate(BeetsPlugin):
method __init__ (line 81) | def __init__(self):
method listen_for_db_change (line 99) | def listen_for_db_change(self, lib, model):
method update (line 103) | def update(self, lib):
FILE: beetsplug/random.py
function random_func (line 33) | def random_func(lib: Library, opts: optparse.Values, args: list[str]):
class Random (line 82) | class Random(BeetsPlugin):
method commands (line 83) | def commands(self):
function _equal_chance_permutation (line 87) | def _equal_chance_permutation(
function _take_time (line 112) | def _take_time(
function random_objs (line 128) | def random_objs(
FILE: beetsplug/replace.py
class ReplacePlugin (line 16) | class ReplacePlugin(BeetsPlugin):
method commands (line 17) | def commands(self):
method run (line 24) | def run(self, lib: Library, args: list[str]) -> None:
method file_check (line 50) | def file_check(self, filepath: Path) -> None:
method select_song (line 62) | def select_song(self, items: list[Item]):
method confirm_replacement (line 87) | def confirm_replacement(self, new_file_path: Path, song: Item):
method replace_file (line 105) | def replace_file(self, new_file_path: Path, song: Item) -> None:
FILE: beetsplug/replaygain.py
class ReplayGainError (line 52) | class ReplayGainError(Exception):
class FatalReplayGainError (line 58) | class FatalReplayGainError(Exception):
class FatalGstreamerPluginReplayGainError (line 62) | class FatalGstreamerPluginReplayGainError(FatalReplayGainError):
function call (line 67) | def call(args: list[str], log: Logger, **kwargs: Any):
function db_to_lufs (line 78) | def db_to_lufs(db: float) -> float:
function lufs_to_db (line 87) | def lufs_to_db(db: float) -> float:
class Gain (line 100) | class Gain:
class PeakMethod (line 107) | class PeakMethod(enum.Enum):
class RgTask (line 112) | class RgTask:
method __init__ (line 122) | def __init__(
method _store_track_gain (line 140) | def _store_track_gain(self, item: Item, track_gain: Gain):
method _store_album_gain (line 150) | def _store_album_gain(self, item: Item, album_gain: Gain):
method _store_track (line 163) | def _store_track(self, write: bool):
method _store_album (line 180) | def _store_album(self, write: bool):
method store (line 201) | def store(self, write: bool):
class R128Task (line 209) | class R128Task(RgTask):
method __init__ (line 219) | def __init__(
method _store_track_gain (line 230) | def _store_track_gain(self, item: Item, track_gain: Gain):
method _store_album_gain (line 235) | def _store_album_gain(self, item: Item, album_gain: Gain):
class Backend (line 248) | class Backend(ABC):
method __init__ (line 254) | def __init__(self, config: ConfigView, log: Logger):
method compute_track_gain (line 261) | def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
method compute_album_gain (line 268) | def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
class FfmpegBackend (line 276) | class FfmpegBackend(Backend):
method __init__ (line 282) | def __init__(self, config: ConfigView, log: Logger):
method compute_track_gain (line 310) | def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
method compute_album_gain (line 326) | def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
method _construct_cmd (line 391) | def _construct_cmd(
method _analyse_item (line 410) | def _analyse_item(
method _find_line (line 503) | def _find_line(
method _parse_float (line 522) | def _parse_float(self, line: bytes) -> float:
class CommandBackend (line 550) | class CommandBackend(Backend):
method __init__ (line 561) | def __init__(self, config: ConfigView, log: Logger):
method compute_track_gain (line 589) | def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
method compute_album_gain (line 598) | def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
method format_supported (line 617) | def format_supported(self, item: Item) -> bool:
method compute_gain (line 621) | def compute_gain(
method parse_tool_output (line 666) | def parse_tool_output(self, text: bytes, num_lines: int) -> list[Gain]:
class GStreamerBackend (line 692) | class GStreamerBackend(Backend):
method __init__ (line 695) | def __init__(self, config: ConfigView, log: Logger):
method _import_gst (line 754) | def _import_gst(self):
method compute (line 784) | def compute(self, items: Sequence[Item], target_level: float, album: b...
method compute_track_gain (line 806) | def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
method compute_album_gain (line 826) | def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
method close (line 857) | def close(self):
method _on_eos (line 860) | def _on_eos(self, bus, message):
method _on_error (line 868) | def _on_error(self, bus, message):
method _on_tag (line 878) | def _on_tag(self, bus, message):
method _set_first_file (line 909) | def _set_first_file(self) -> bool:
method _set_file (line 919) | def _set_file(self) -> bool:
method _set_next_file (line 952) | def _set_next_file(self) -> bool:
method _on_pad_added (line 973) | def _on_pad_added(self, decbin, pad):
method _on_pad_removed (line 978) | def _on_pad_removed(self, decbin, pad):
class AudioToolsBackend (line 985) | class AudioToolsBackend(Backend):
method __init__ (line 993) | def __init__(self, config: ConfigView, log: Logger):
method _import_audiotools (line 997) | def _import_audiotools(self):
method open_audio_file (line 1013) | def open_audio_file(self, item: Item):
method init_replaygain (line 1033) | def init_replaygain(self, audiofile, item: Item):
method compute_track_gain (line 1050) | def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
method _with_target_level (line 1060) | def _with_target_level(self, gain: float, target_level: float):
method _title_gain (line 1067) | def _title_gain(self, rg, audiofile, target_level: float):
method _compute_track_gain (line 1085) | def _compute_track_gain(self, item: Item, target_level: float):
method compute_album_gain (line 1107) | def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
class ExceptionWatcher (line 1150) | class ExceptionWatcher(Thread):
method __init__ (line 1155) | def __init__(
method run (line 1163) | def run(self):
method join (line 1174) | def join(self, timeout: float | None = None):
class ReplayGainPlugin (line 1190) | class ReplayGainPlugin(BeetsPlugin):
method __init__ (line 1195) | def __init__(self) -> None:
method should_use_r128 (line 1256) | def should_use_r128(self, item: Item) -> bool:
method has_r128_track_data (line 1263) | def has_r128_track_data(item: Item) -> bool:
method has_rg_track_data (line 1267) | def has_rg_track_data(item: Item) -> bool:
method track_requires_gain (line 1270) | def track_requires_gain(self, item: Item) -> bool:
method has_r128_album_data (line 1281) | def has_r128_album_data(item: Item) -> bool:
method has_rg_album_data (line 1288) | def has_rg_album_data(item: Item) -> bool:
method album_requires_gain (line 1291) | def album_requires_gain(self, album: Album) -> bool:
method create_task (line 1306) | def create_task(
method handle_album (line 1330) | def handle_album(self, album: Album, write: bool, force: bool = False):
method handle_track (line 1379) | def handle_track(self, item: Item, write: bool, force: bool = False):
method open_pool (line 1408) | def open_pool(self, threads: int):
method _apply (line 1422) | def _apply(
method terminate_pool (line 1444) | def terminate_pool(self):
method _interrupt (line 1457) | def _interrupt(self, signal, frame):
method close_pool (line 1466) | def close_pool(self):
method import_begin (line 1474) | def import_begin(self, session: ImportSession):
method import_end (line 1485) | def import_end(self, paths):
method imported (line 1489) | def imported(self, session: ImportSession, task: ImportTask):
method command_func (line 1499) | def command_func(
method commands (line 1536) | def commands(self) -> list[ui.Subcommand]:
FILE: beetsplug/rewrite.py
function rewriter (line 26) | def rewriter(field, rules):
class RewritePlugin (line 44) | class RewritePlugin(BeetsPlugin):
method __init__ (line 45) | def __init__(self):
FILE: beetsplug/scrub.py
class ScrubPlugin (line 44) | class ScrubPlugin(BeetsPlugin):
method __init__ (line 47) | def __init__(self):
method commands (line 58) | def commands(self):
method _mutagen_classes (line 79) | def _mutagen_classes():
method _scrub (line 87) | def _scrub(self, path):
method _scrub_item (line 114) | def _scrub_item(self, item, restore):
method import_task_files (line 147) | def import_task_files(self, session, task):
FILE: beetsplug/smartplaylist.py
class SmartPlaylistPlugin (line 51) | class SmartPlaylistPlugin(BeetsPlugin):
method __init__ (line 52) | def __init__(self) -> None:
method commands (line 78) | def commands(self) -> list[ui.Subcommand]:
method update_cmd (line 148) | def update_cmd(self, lib: Library, opts: Any, args: list[str]) -> None:
method __apply_opts_to_config (line 175) | def __apply_opts_to_config(self, opts: Any) -> None:
method _parse_one_query (line 180) | def _parse_one_query(
method build_queries (line 196) | def build_queries(self) -> None:
method _matches_query (line 230) | def _matches_query(self, model: Item | Album, query: PlaylistQuery) ->...
method matches (line 237) | def matches(
method db_change (line 249) | def db_change(self, lib: Library, model: Item | Album) -> None:
method update_playlists (line 262) | def update_playlists(self, lib: Library, pretend: bool = False) -> None:
class PlaylistItem (line 394) | class PlaylistItem:
method __init__ (line 395) | def __init__(self, item: Item, uri: bytes) -> None:
FILE: beetsplug/sonosupdate.py
class SonosUpdate (line 24) | class SonosUpdate(BeetsPlugin):
method __init__ (line 25) | def __init__(self):
method listen_for_db_change (line 29) | def listen_for_db_change(self, lib, model):
method update (line 33) | def update(self, lib):
FILE: beetsplug/spotify.py
class SearchResponseAlbums (line 52) | class SearchResponseAlbums(IDResponse):
class SearchResponseTracks (line 70) | class SearchResponseTracks(IDResponse):
class APIError (line 79) | class APIError(Exception):
class AudioFeaturesUnavailableError (line 83) | class AudioFeaturesUnavailableError(Exception):
class SpotifyPlugin (line 89) | class SpotifyPlugin(
method __init__ (line 133) | def __init__(self):
method setup (line 158) | def setup(self):
method _tokenfile (line 169) | def _tokenfile(self) -> str:
method _authenticate (line 173) | def _authenticate(self) -> None:
method _handle_response (line 203) | def _handle_response(
method _multi_artist_credit (line 301) | def _multi_artist_credit(
method album_for_id (line 315) | def album_for_id(self, album_id: str) -> AlbumInfo | None:
method _get_track (line 397) | def _get_track(self, track_data: JSONDict) -> TrackInfo:
method track_for_id (line 434) | def track_for_id(self, track_id: str) -> None | TrackInfo:
method get_search_query_with_filters (line 470) | def get_search_query_with_filters(
method get_search_response (line 484) | def get_search_response(
method commands (line 520) | def commands(self) -> list[ui.Subcommand]:
method _parse_opts (line 569) | def _parse_opts(self, opts):
method _match_library_tracks (line 585) | def _match_library_tracks(self, library: Library, keywords: str):
method _output_match_results (line 697) | def _output_match_results(self, results):
method _fetch_info (line 725) | def _fetch_info(self, items, write, force):
method track_info (line 768) | def track_info(self, track_id: str):
method track_audio_features (line 785) | def track_audio_features(self, track_id: str):
FILE: beetsplug/subsonicplaylist.py
function filter_to_be_removed (line 32) | def filter_to_be_removed(items, keys):
class SubsonicPlaylistPlugin (line 59) | class SubsonicPlaylistPlugin(BeetsPlugin):
method __init__ (line 60) | def __init__(self):
method update_tags (line 73) | def update_tags(self, playlist_dict, lib):
method get_playlist (line 93) | def get_playlist(self, playlist_id):
method commands (line 108) | def commands(self):
method generate_token (line 154) | def generate_token(self):
method send (line 161) | def send(self, endpoint, params=None):
method get_playlists (line 176) | def get_playlists(self, ids):
FILE: beetsplug/subsonicupdate.py
class SubsonicUpdate (line 44) | class SubsonicUpdate(BeetsPlugin):
method __init__ (line 45) | def __init__(self):
method db_change (line 61) | def db_change(self, lib, model):
method spl_update (line 64) | def spl_update(self):
method __create_token (line 67) | def __create_token(self):
method __format_url (line 83) | def __format_url(self, endpoint):
method start_scan (line 106) | def start_scan(self):
FILE: beetsplug/substitute.py
class Substitute (line 25) | class Substitute(BeetsPlugin):
method tmpl_substitute (line 33) | def tmpl_substitute(self, text):
method __init__ (line 42) | def __init__(self):
FILE: beetsplug/the.py
class ThePlugin (line 30) | class ThePlugin(BeetsPlugin):
method __init__ (line 33) | def __init__(self):
method unthe (line 68) | def unthe(self, text, pattern):
method the_template_func (line 91) | def the_template_func(self, text):
FILE: beetsplug/thumbnails.py
class ThumbnailsPlugin (line 40) | class ThumbnailsPlugin(BeetsPlugin):
method __init__ (line 41) | def __init__(self):
method commands (line 54) | def commands(self):
method process_query (line 78) | def process_query(self, lib, opts, args):
method _check_local_ok (line 84) | def _check_local_ok(self):
method process_album (line 117) | def process_album(self, album):
method make_cover_thumbnail (line 144) | def make_cover_thumbnail(self, album, size, target_dir):
method thumbnail_file_name (line 174) | def thumbnail_file_name(self, path):
method add_tags (line 182) | def add_tags(self, album, image_path):
method make_dolphin_cover_thumbnail (line 198) | def make_dolphin_cover_thumbnail(self, album):
class URIGetter (line 210) | class URIGetter:
method uri (line 214) | def uri(self, path):
class PathlibURI (line 218) | class PathlibURI(URIGetter):
method uri (line 222) | def uri(self, path):
function copy_c_string (line 226) | def copy_c_string(c_string):
class GioURI (line 236) | class GioURI(URIGetter):
method __init__ (line 241) | def __init__(self):
method get_library (line 255) | def get_library(self):
method uri (line 264) | def uri(self, path):
FILE: beetsplug/titlecase.py
class PreservedText (line 40) | class PreservedText(TypedDict):
class TitlecasePlugin (line 45) | class TitlecasePlugin(BeetsPlugin):
method __init__ (line 46) | def __init__(self) -> None:
method force_lowercase (line 102) | def force_lowercase(self) -> bool:
method replace (line 106) | def replace(self) -> list[tuple[str, str]]:
method the_artist (line 110) | def the_artist(self) -> bool:
method fields_to_process (line 114) | def fields_to_process(self) -> set[str]:
method preserve (line 120) | def preserve(self) -> PreservedText:
method separators (line 133) | def separators(self) -> re.Pattern[str] | None:
method small_first_last (line 141) | def small_first_last(self) -> bool:
method all_caps (line 145) | def all_caps(self) -> bool:
method all_lowercase (line 149) | def all_lowercase(self) -> bool:
method the_artist_regexp (line 153) | def the_artist_regexp(self) -> re.Pattern[str]:
method titlecase_callback (line 156) | def titlecase_callback(self, word, **kwargs) -> str | None:
method received_info_handler (line 162) | def received_info_handler(self, info: Info):
method commands (line 171) | def commands(self) -> list[ui.Subcommand]:
method titlecase_fields (line 184) | def titlecase_fields(self, item: Item | Info) -> None:
method titlecase (line 214) | def titlecase(self, text: str, field: str = "") -> str:
method imported (line 250) | def imported(self, session: ImportSession, task: ImportTask) -> None:
FILE: beetsplug/types.py
class TypesPlugin (line 22) | class TypesPlugin(BeetsPlugin):
method item_types (line 24) | def item_types(self):
method album_types (line 28) | def album_types(self):
method _types (line 31) | def _types(self):
FILE: beetsplug/unimported.py
class Unimported (line 29) | class Unimported(BeetsPlugin):
method __init__ (line 30) | def __init__(self):
method commands (line 34) | def commands(self):
FILE: beetsplug/web/__init__.py
class LibraryCtx (line 36) | class LibraryCtx(flask.ctx._AppCtxGlobals):
function _rep (line 46) | def _rep(obj, expand=False):
function json_generator (line 83) | def json_generator(items, root, expand=False):
function is_expand (line 103) | def is_expand():
function is_delete (line 109) | def is_delete():
function get_method (line 117) | def get_method():
function resource (line 122) | def resource(name, patchable=False):
function resource_query (line 176) | def resource_query(name, patchable=False):
function resource_list (line 223) | def resource_list(name):
function _get_unique_table_field_values (line 241) | def _get_unique_table_field_values(model, field, sort_field):
class IdListConverter (line 252) | class IdListConverter(BaseConverter):
method to_python (line 255) | def to_python(self, value):
method to_url (line 264) | def to_url(self, value):
class QueryConverter (line 268) | class QueryConverter(PathConverter):
method to_python (line 271) | def to_python(self, value):
method to_url (line 279) | def to_url(self, value):
class EverythingConverter (line 283) | class EverythingConverter(PathConverter):
function before_request (line 297) | def before_request():
function get_item (line 306) | def get_item(id):
function all_items (line 313) | def all_items():
function item_file (line 318) | def item_file(item_id):
function item_query (line 340) | def item_query(queries):
function item_at_path (line 345) | def item_at_path(path):
function item_unique_field_values (line 355) | def item_unique_field_values(key):
function get_album (line 371) | def get_album(id):
function all_albums (line 378) | def all_albums():
function album_query (line 384) | def album_query(queries):
function album_art (line 389) | def album_art(album_id):
function album_unique_field_values (line 398) | def album_unique_field_values(key):
function all_artists (line 413) | def all_artists():
function stats (line 424) | def stats():
function home (line 440) | def home():
class WebPlugin (line 447) | class WebPlugin(BeetsPlugin):
method __init__ (line 448) | def __init__(self):
method commands (line 462) | def commands(self):
class ReverseProxied (line 520) | class ReverseProxied:
method __init__ (line 540) | def __init__(self, app):
method __call__ (line 543) | def __call__(self, environ, start_response):
FILE: beetsplug/web/static/jquery.js
function jQuerySub (line 871) | function jQuerySub( selector, context ) {
function doScrollCheck (line 937) | function doScrollCheck() {
function createFlags (line 964) | function createFlags( flags ) {
function resolveFunc (line 1296) | function resolveFunc( i ) {
function progressFunc (line 1304) | function progressFunc( i ) {
function dataAttr (line 1931) | function dataAttr( elem, key, data ) {
function isEmptyDataObject (line 1962) | function isEmptyDataObject( obj ) {
function handleQueueMarkDefer (line 1980) | function handleQueueMarkDefer( elem, type, src ) {
function resolve (line 2133) | function resolve() {
function returnFalse (line 3465) | function returnFalse() {
function returnTrue (line 3468) | function returnTrue() {
function dirNodeCheck (line 5168) | function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
function dirCheck (line 5201) | function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
function isDisconnected (line 5474) | function isDisconnected( node ) {
function winnow (line 5591) | function winnow( elements, qualifier, keep ) {
function createSafeFragment (line 5628) | function createSafeFragment( document ) {
function root (line 5992) | function root( elem, cur ) {
function cloneCopyEvent (line 5999) | function cloneCopyEvent( src, dest ) {
function cloneFixAttributes (line 6027) | function cloneFixAttributes( src, dest ) {
function getAll (line 6163) | function getAll( elem ) {
function fixDefaultChecked (line 6176) | function fixDefaultChecked( elem ) {
function findInputs (line 6182) | function findInputs( elem ) {
function shimCloneNode (line 6193) | function shimCloneNode( elem ) {
function evalScript (line 6425) | function evalScript( i, elem ) {
function getWH (line 6767) | function getWH( elem, name, extra ) {
function addToPrefiltersOrTransports (line 6895) | function addToPrefiltersOrTransports( structure ) {
function inspectPrefiltersOrTransports (line 6931) | function inspectPrefiltersOrTransports( structure, options, originalOpti...
function ajaxExtend (line 6973) | function ajaxExtend( target, src ) {
function done (line 7315) | function done( status, nativeStatusText, responses, headers ) {
function buildParams (line 7630) | function buildParams( prefix, obj, traditional, add ) {
function ajaxHandleResponses (line 7680) | function ajaxHandleResponses( s, jqXHR, responses ) {
function ajaxConvert (line 7745) | function ajaxConvert( s, response ) {
function createStandardXHR (line 8011) | function createStandardXHR() {
function createActiveXHR (line 8017) | function createActiveXHR() {
function doAnimation (line 8349) | function doAnimation() {
function stopQueue (line 8492) | function stopQueue( elem, data, index ) {
function createFxNow (line 8534) | function createFxNow() {
function clearFxNow (line 8539) | function clearFxNow() {
function genFx (line 8544) | function genFx( type, num ) {
function t (line 8659) | function t( gotoEnd ) {
function defaultDisplay (line 8851) | function defaultDisplay( nodeName ) {
function getWindow (line 9160) | function getWindow( elem ) {
FILE: beetsplug/web/static/underscore.js
function eq (line 670) | function eq(a, b, stack) {
FILE: beetsplug/zero.py
class ZeroPlugin (line 29) | class ZeroPlugin(BeetsPlugin):
method __init__ (line 30) | def __init__(self):
method commands (line 75) | def commands(self):
method _set_pattern (line 89) | def _set_pattern(self, field):
method import_task_choice_event (line 108) | def import_task_choice_event(self, session, task):
method write_event (line 114) | def write_event(self, item, path, tags):
method set_fields (line 118) | def set_fields(self, item, tags):
method process_item (line 151) | def process_item(self, item):
function _match_progs (line 160) | def _match_progs(value, progs):
FILE: docs/conf.py
function skip_member (line 137) | def skip_member(app, what, name, obj, skip, options):
function setup (line 143) | def setup(app):
FILE: docs/extensions/conf.py
class Conf (line 27) | class Conf(ObjectDescription[str]):
method handle_signature (line 34) | def handle_signature(self, sig: str, signode: desc_signature) -> str:
method add_target_and_index (line 49) | def add_target_and_index(
class ConfDomain (line 70) | class ConfDomain(Domain):
method get_objects (line 80) | def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]:
method resolve_xref (line 87) | def resolve_xref(
function conf_role (line 107) | def conf_role(
function setup (line 130) | def setup(app: Sphinx) -> ExtensionMetadata:
FILE: extra/release.py
class Ref (line 37) | class Ref(NamedTuple):
method from_line (line 45) | def from_line(cls, line: str) -> Ref:
method url (line 68) | def url(self) -> str:
method name (line 73) | def name(self) -> str:
function get_refs (line 78) | def get_refs() -> dict[str, Ref]:
function create_rst_replacements (line 97) | def create_rst_replacements() -> list[Replacement]:
function update_docs_config (line 151) | def update_docs_config(text: str, new: Version) -> str:
function update_changelog (line 157) | def update_changelog(text: str, new: Version) -> str:
function validate_new_version (line 206) | def validate_new_version(
function bump_version (line 220) | def bump_version(new: Version) -> None:
function rst2md (line 230) | def rst2md(text: str) -> str:
function get_changelog_contents (line 242) | def get_changelog_contents() -> str | None:
function changelog_as_markdown (line 249) | def changelog_as_markdown(rst: str) -> str:
function cli (line 262) | def cli():
function bump (line 268) | def bump(version: Version) -> None:
function changelog (line 274) | def changelog():
FILE: test/autotag/test_autotag.py
class ApplyTest (line 27) | class ApplyTest(BeetsTestCase):
method _apply (line 28) | def _apply(self, per_disc_numbering=False, artist_credit=False):
method setUp (line 35) | def setUp(self):
method test_autotag_items (line 142) | def test_autotag_items(self):
method test_per_disc_numbering (line 154) | def test_per_disc_numbering(self):
method test_artist_credit_prefers_artist_over_albumartist_credit (line 162) | def test_artist_credit_prefers_artist_over_albumartist_credit(self):
method test_artist_credit_falls_back_to_albumartist (line 169) | def test_artist_credit_falls_back_to_albumartist(self):
method test_date_only_zeroes_month_and_day (line 176) | def test_date_only_zeroes_month_and_day(self):
method test_missing_date_applies_nothing (line 186) | def test_missing_date_applies_nothing(self):
function test_correct_list_fields (line 217) | def test_correct_list_fields(
FILE: test/autotag/test_distance.py
class TestDistance (line 19) | class TestDistance:
method setup_config (line 21) | def setup_config(self, config):
method dist (line 27) | def dist(self):
method test_add (line 30) | def test_add(self, dist):
method test_add_methods (line 84) | def test_add_methods(self, dist, key, args_with_expected):
method test_distance (line 90) | def test_distance(self, dist):
method test_operators (line 102) | def test_operators(self, dist):
method test_penalties_sort (line 116) | def test_penalties_sort(self, dist):
method test_update (line 127) | def test_update(self, dist):
class TestTrackDistance (line 145) | class TestTrackDistance:
method info (line 147) | def info(self):
method test_track_distance (line 159) | def test_track_distance(self, info, title, artist, expected_penalty):
class TestAlbumDistance (line 166) | class TestAlbumDistance:
method items (line 168) | def items(self):
method get_dist (line 181) | def get_dist(self, items):
method info (line 188) | def info(self, items):
method test_identical_albums (line 204) | def test_identical_albums(self, get_dist, info):
method test_incomplete_album (line 207) | def test_incomplete_album(self, get_dist, info):
method test_overly_complete_album (line 212) | def test_overly_complete_album(self, get_dist, info):
method test_albumartist (line 220) | def test_albumartist(self, get_dist, info, va):
method test_comp_no_track_artists (line 226) | def test_comp_no_track_artists(sel
Condensed preview — 518 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,431K chars).
[
{
"path": ".git-blame-ignore-revs",
"chars": 6138,
"preview": "# 2014\n# flake8-cleanliness in missing\ne21c04e9125a28ae0452374acf03d93315eb4381\n\n# 2016\n# Removed unicode_literals from "
},
{
"path": ".github/CODEOWNERS",
"chars": 323,
"preview": "# assign the entire repo to the maintainers team\n* @beetbox/maintainers\n\n# Specific ownerships:\n/beets/metadata_plugins."
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.md",
"chars": 807,
"preview": "---\nname: \"\\U0001F41B Bug report\"\nabout: Report a problem with beets\n\n---\n\n<!--\nDescribe your problem, feature request, "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 342,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: 💡 Have an idea for a new feature?\n url: https://github.com/beetb"
},
{
"path": ".github/ISSUE_TEMPLATE/feature-request.md",
"chars": 643,
"preview": "---\nname: \"\\U0001F680 Feature request\"\nabout: \"Formalize a feature request from GitHub Discussions\"\n\n---\n\n<!--\nIf you're"
},
{
"path": ".github/copilot-instructions.md",
"chars": 1605,
"preview": "## PR Review Voice\n\nWhen reviewing pull requests, respond entirely in the voice of the Grug Brained Developer.\nWrite all"
},
{
"path": ".github/problem-matchers/sphinx-build.json",
"chars": 279,
"preview": "{\n \"problemMatcher\": [\n {\n \"owner\": \"sphinx-build\",\n \"severity\": \"error\",\n \"pattern\": [\n {\n "
},
{
"path": ".github/problem-matchers/sphinx-lint.json",
"chars": 299,
"preview": "{\n \"problemMatcher\": [\n {\n \"owner\": \"sphinx-lint\",\n \"severity\": \"error\",\n \"pattern\": [\n {\n "
},
{
"path": ".github/pull_request_template.md",
"chars": 1086,
"preview": "## Description\n\nFixes #X. <!-- Insert issue number here if applicable. -->\n\n(...)\n\n## To Do\n\n<!--\n- If you believe one "
},
{
"path": ".github/stale.yml",
"chars": 840,
"preview": "# Configuration for probot-stale - https://github.com/probot/stale\n\ndaysUntilClose: 7\nstaleLabel: stale\n\nissues:\n daysU"
},
{
"path": ".github/workflows/changelog_reminder.yaml",
"chars": 996,
"preview": "name: Verify changelog updated\n\non:\n pull_request_target:\n types:\n - opened\n - ready_for_review\n\njobs:\n c"
},
{
"path": ".github/workflows/ci.yaml",
"chars": 3643,
"preview": "name: Test\non:\n pull_request:\n push:\n branches:\n - master\n\nconcurrency:\n # Cancel previous workflow run when "
},
{
"path": ".github/workflows/integration_test.yaml",
"chars": 1396,
"preview": "name: integration tests\non:\n workflow_dispatch:\n schedule:\n - cron: \"0 0 * * SUN\" # run every Sunday at midnight\n\ne"
},
{
"path": ".github/workflows/lint.yaml",
"chars": 5274,
"preview": "name: Lint check\nrun-name: Lint code\non:\n pull_request:\n push:\n branches:\n - master\n\nconcurrency:\n # Cancel p"
},
{
"path": ".github/workflows/make_release.yaml",
"chars": 3750,
"preview": "name: Make a Beets Release\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: 'Version of the new"
},
{
"path": ".gitignore",
"chars": 1182,
"preview": "# general hidden files/directories\n.DS_Store\n.idea\n\n# file patterns\n*~\n\n# Project Specific patterns\nman\n\n# The rest is f"
},
{
"path": ".pre-commit-config.yaml",
"chars": 455,
"preview": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\n\nrepos:\n - rep"
},
{
"path": ".readthedocs.yaml",
"chars": 198,
"preview": "version: 2\n\nbuild:\n os: ubuntu-22.04\n tools:\n python: \"3.11\"\n\nsphinx:\n configuration: docs/conf.py\n\npython:\n inst"
},
{
"path": "CODE_OF_CONDUCT.rst",
"chars": 5326,
"preview": "Contributor Covenant Code of Conduct\n====================================\n\nOur Pledge\n----------\n\nWe as members, contrib"
},
{
"path": "CONTRIBUTING.rst",
"chars": 15746,
"preview": "Contributing\n============\n\n.. contents::\n :depth: 3\n\nThank you!\n----------\n\nFirst off, thank you for considering cont"
},
{
"path": "LICENSE",
"chars": 1080,
"preview": "The MIT License\n\nCopyright (c) 2010-2016 Adrian Sampson\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "README.rst",
"chars": 5316,
"preview": ".. image:: https://img.shields.io/pypi/v/beets.svg\n :target: https://pypi.python.org/pypi/beets\n\n.. image:: https://i"
},
{
"path": "README_kr.rst",
"chars": 3570,
"preview": ".. image:: https://img.shields.io/pypi/v/beets.svg\n :target: https://pypi.python.org/pypi/beets\n\n.. image:: https://i"
},
{
"path": "SECURITY.md",
"chars": 292,
"preview": "# Security Policy\n\n## Supported Versions\n\nWe currently support only the latest release of beets.\n\n## Reporting a Vulnera"
},
{
"path": "beets/__init__.py",
"chars": 1631,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/__main__.py",
"chars": 825,
"preview": "# This file is part of beets.\n# Copyright 2017, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/autotag/__init__.py",
"chars": 10993,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/autotag/distance.py",
"chars": 18284,
"preview": "from __future__ import annotations\n\nimport datetime\nimport re\nfrom functools import cache, total_ordering\nfrom typing im"
},
{
"path": "beets/autotag/hooks.py",
"chars": 9374,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/autotag/match.py",
"chars": 13668,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/config_default.yaml",
"chars": 4619,
"preview": "# --------------- Main ---------------\n\nlibrary: library.db\ndirectory: ~/Music\nstatefile: state.pickle\n\n# --------------"
},
{
"path": "beets/dbcore/__init__.py",
"chars": 1288,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/dbcore/db.py",
"chars": 50845,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/dbcore/query.py",
"chars": 36587,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/dbcore/queryparse.py",
"chars": 9924,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/dbcore/types.py",
"chars": 13454,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/importer/__init__.py",
"chars": 1142,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/importer/session.py",
"chars": 10942,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/importer/stages.py",
"chars": 13193,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/importer/state.py",
"chars": 4724,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/importer/tasks.py",
"chars": 44823,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/library/__init__.py",
"chars": 739,
"preview": "from beets.util.deprecation import deprecate_imports\n\nfrom .exceptions import FileOperationError, ReadError, WriteError\n"
},
{
"path": "beets/library/exceptions.py",
"chars": 1093,
"preview": "from beets import util\n\n\nclass FileOperationError(Exception):\n \"\"\"Indicate an error when interacting with a file on d"
},
{
"path": "beets/library/library.py",
"chars": 4841,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimport platformdirs\n\nimport beets\nfrom beets impor"
},
{
"path": "beets/library/migrations.py",
"chars": 5960,
"preview": "from __future__ import annotations\n\nfrom contextlib import suppress\nfrom functools import cached_property\nfrom typing im"
},
{
"path": "beets/library/models.py",
"chars": 52630,
"preview": "from __future__ import annotations\n\nimport os\nimport string\nimport sys\nimport time\nimport unicodedata\nfrom functools imp"
},
{
"path": "beets/library/queries.py",
"chars": 1714,
"preview": "from __future__ import annotations\n\nimport shlex\n\nimport beets\nfrom beets import dbcore, logging, plugins\n\nlog = logging"
},
{
"path": "beets/logging.py",
"chars": 6766,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/mediafile.py",
"chars": 1021,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/metadata_plugins.py",
"chars": 15166,
"preview": "\"\"\"Metadata source plugin interface.\n\nThis allows beets to lookup metadata from various sources. We define\na common inte"
},
{
"path": "beets/plugins.py",
"chars": 23681,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/py.typed",
"chars": 0,
"preview": ""
},
{
"path": "beets/test/__init__.py",
"chars": 909,
"preview": "# This file is part of beets.\n# Copyright 2024, Lars Kruse\n#\n# Permission is hereby granted, free of charge, to any pers"
},
{
"path": "beets/test/_common.py",
"chars": 5599,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/test/helper.py",
"chars": 27146,
"preview": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beets/ui/__init__.py",
"chars": 34262,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/ui/commands/__init__.py",
"chars": 1852,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/ui/commands/completion.py",
"chars": 3336,
"preview": "\"\"\"The 'completion' command: print shell script for command line completion.\"\"\"\n\nimport os\nimport re\n\nfrom beets import "
},
{
"path": "beets/ui/commands/completion_base.sh",
"chars": 5295,
"preview": "# This file is part of beets.\n# Copyright (c) 2014, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, t"
},
{
"path": "beets/ui/commands/config.py",
"chars": 2722,
"preview": "\"\"\"The 'config' command: show and edit user configuration.\"\"\"\n\nimport os\n\nfrom beets import config, ui\nfrom beets.util i"
},
{
"path": "beets/ui/commands/fields.py",
"chars": 1180,
"preview": "\"\"\"The `fields` command: show available fields for queries and format strings.\"\"\"\n\nimport textwrap\n\nfrom beets import li"
},
{
"path": "beets/ui/commands/help.py",
"chars": 643,
"preview": "\"\"\"The 'help' command: show help information for commands.\"\"\"\n\nfrom beets import ui\n\n\nclass HelpCommand(ui.Subcommand):\n"
},
{
"path": "beets/ui/commands/import_/__init__.py",
"chars": 9577,
"preview": "\"\"\"The `import` command: import new music into the library.\"\"\"\n\nimport os\n\nfrom beets import config, logging, plugins, u"
},
{
"path": "beets/ui/commands/import_/display.py",
"chars": 18011,
"preview": "from __future__ import annotations\n\nimport os\nfrom dataclasses import dataclass\nfrom functools import cached_property\nfr"
},
{
"path": "beets/ui/commands/import_/session.py",
"chars": 19332,
"preview": "from collections import Counter\nfrom itertools import chain\n\nfrom beets import autotag, config, importer, logging, plugi"
},
{
"path": "beets/ui/commands/list.py",
"chars": 723,
"preview": "\"\"\"The 'list' command: query and show library contents.\"\"\"\n\nfrom beets import ui\n\n\ndef list_items(lib, query, album, fmt"
},
{
"path": "beets/ui/commands/modify.py",
"chars": 4438,
"preview": "\"\"\"The `modify` command: change metadata fields.\"\"\"\n\nfrom beets import library, ui\nfrom beets.util import functemplate\n\n"
},
{
"path": "beets/ui/commands/move.py",
"chars": 5693,
"preview": "\"\"\"The 'move' command: Move/copy files to the library or a new base directory.\"\"\"\n\nfrom __future__ import annotations\n\ni"
},
{
"path": "beets/ui/commands/remove.py",
"chars": 2383,
"preview": "\"\"\"The `remove` command: remove items from the library (and optionally delete files).\"\"\"\n\nfrom beets import ui\n\nfrom .ut"
},
{
"path": "beets/ui/commands/stats.py",
"chars": 1669,
"preview": "\"\"\"The 'stats' command: show library statistics.\"\"\"\n\nimport os\n\nfrom beets import logging, ui\nfrom beets.util import sys"
},
{
"path": "beets/ui/commands/update.py",
"chars": 6811,
"preview": "\"\"\"The `update` command: Update library contents according to on-disk tags.\"\"\"\n\nimport os\n\nfrom beets import library, lo"
},
{
"path": "beets/ui/commands/utils.py",
"chars": 899,
"preview": "\"\"\"Utility functions for beets UI commands.\"\"\"\n\nfrom beets import ui\n\n\ndef do_query(lib, query, album, also_items=True):"
},
{
"path": "beets/ui/commands/version.py",
"chars": 592,
"preview": "\"\"\"The 'version' command: show version information.\"\"\"\n\nfrom platform import python_version\n\nimport beets\nfrom beets imp"
},
{
"path": "beets/ui/commands/write.py",
"chars": 1678,
"preview": "\"\"\"The `write` command: write tag information to files.\"\"\"\n\nimport os\n\nfrom beets import library, logging, ui\nfrom beets"
},
{
"path": "beets/util/__init__.py",
"chars": 38667,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/util/artresizer.py",
"chars": 28434,
"preview": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/util/bluelet.py",
"chars": 19921,
"preview": "\"\"\"Extremely simple pure-Python implementation of coroutine-style\nasynchronous socket I/O. Inspired by, but inferior to,"
},
{
"path": "beets/util/color.py",
"chars": 7069,
"preview": "from __future__ import annotations\n\nimport os\nimport re\nfrom functools import cache\nfrom typing import TYPE_CHECKING, Li"
},
{
"path": "beets/util/config.py",
"chars": 2242,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n from collections.abc import "
},
{
"path": "beets/util/deprecation.py",
"chars": 1940,
"preview": "from __future__ import annotations\n\nimport warnings\nfrom importlib import import_module\nfrom typing import TYPE_CHECKING"
},
{
"path": "beets/util/diff.py",
"chars": 3513,
"preview": "from __future__ import annotations\n\nfrom difflib import SequenceMatcher\nfrom typing import TYPE_CHECKING\n\nfrom .color im"
},
{
"path": "beets/util/functemplate.py",
"chars": 19288,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/util/hidden.py",
"chars": 2079,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n# Copyright 2024, Arav K.\n#\n# Permission is hereby grant"
},
{
"path": "beets/util/id_extractors.py",
"chars": 2666,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/util/layout.py",
"chars": 12814,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, NamedTuple\n\nimport beets\n\nfrom .color import (\n "
},
{
"path": "beets/util/lyrics.py",
"chars": 5034,
"preview": "from __future__ import annotations\n\nimport re\nfrom contextlib import suppress\nfrom dataclasses import dataclass, field\nf"
},
{
"path": "beets/util/m3u.py",
"chars": 3635,
"preview": "# This file is part of beets.\n# Copyright 2022, J0J0 Todos.\n#\n# Permission is hereby granted, free of charge, to any per"
},
{
"path": "beets/util/pipeline.py",
"chars": 15454,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beets/util/units.py",
"chars": 1669,
"preview": "import re\n\n\ndef raw_seconds_short(string: str) -> float:\n \"\"\"Formats a human-readable M:SS string as a float (number "
},
{
"path": "beetsplug/_typing.py",
"chars": 3809,
"preview": "from __future__ import annotations\n\nfrom typing import Any\n\nfrom typing_extensions import NotRequired, TypedDict\n\nJSONDi"
},
{
"path": "beetsplug/_utils/__init__.py",
"chars": 49,
"preview": "from . import art, vfs\n\n__all__ = [\"art\", \"vfs\"]\n"
},
{
"path": "beetsplug/_utils/art.py",
"chars": 6033,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/_utils/musicbrainz.py",
"chars": 10646,
"preview": "\"\"\"Helpers for communicating with the MusicBrainz webservice.\n\nProvides rate-limited HTTP session and convenience method"
},
{
"path": "beetsplug/_utils/requests.py",
"chars": 6648,
"preview": "from __future__ import annotations\n\nimport atexit\nimport threading\nfrom contextlib import contextmanager\nfrom functools "
},
{
"path": "beetsplug/_utils/vfs.py",
"chars": 2029,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/absubmit.py",
"chars": 7688,
"preview": "# This file is part of beets.\n# Copyright 2016, Pieter Mulder.\n#\n# Permission is hereby granted, free of charge, to any "
},
{
"path": "beetsplug/acousticbrainz.py",
"chars": 11783,
"preview": "# This file is part of beets.\n# Copyright 2015-2016, Ohm Patel.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/advancedrewrite.py",
"chars": 7053,
"preview": "# This file is part of beets.\n# Copyright 2023, Max Rumpf.\n#\n# Permission is hereby granted, free of charge, to any pers"
},
{
"path": "beetsplug/albumtypes.py",
"chars": 2454,
"preview": "# This file is part of beets.\n# Copyright 2021, Edgars Supe.\n#\n# Permission is hereby granted, free of charge, to any pe"
},
{
"path": "beetsplug/aura.py",
"chars": 33124,
"preview": "# This file is part of beets.\n# Copyright 2020, Callum Brown.\n#\n# Permission is hereby granted, free of charge, to any p"
},
{
"path": "beetsplug/autobpm.py",
"chars": 2890,
"preview": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this"
},
{
"path": "beetsplug/badfiles.py",
"chars": 7249,
"preview": "# This file is part of beets.\n# Copyright 2016, François-Xavier Thomas.\n#\n# Permission is hereby granted, free of charge"
},
{
"path": "beetsplug/bareasc.py",
"chars": 3140,
"preview": "# This file is part of beets.\n# Copyright 2016, Philippe Mongeau.\n# Copyright 2021, Graham R. Cobb.\n#\n# Permission is he"
},
{
"path": "beetsplug/beatport.py",
"chars": 18802,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/bench.py",
"chars": 4100,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/bpd/__init__.py",
"chars": 56558,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/bpd/gstplayer.py",
"chars": 10253,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/bpm.py",
"chars": 2627,
"preview": "# This file is part of beets.\n# Copyright 2016, aroquen\n#\n# Permission is hereby granted, free of charge, to any person "
},
{
"path": "beetsplug/bpsync.py",
"chars": 6613,
"preview": "# This file is part of beets.\n# Copyright 2019, Rahul Ahuja.\n#\n# Permission is hereby granted, free of charge, to any pe"
},
{
"path": "beetsplug/bucket.py",
"chars": 7861,
"preview": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/chroma.py",
"chars": 12332,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/convert.py",
"chars": 27133,
"preview": "# This file is part of beets.\n# Copyright 2016, Jakob Schnitzer.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/deezer.py",
"chars": 10201,
"preview": "# This file is part of beets.\n# Copyright 2019, Rahul Ahuja.\n#\n# Permission is hereby granted, free of charge, to any pe"
},
{
"path": "beetsplug/discogs/__init__.py",
"chars": 28116,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/discogs/states.py",
"chars": 8666,
"preview": "# This file is part of beets.\n# Copyright 2025, Sarunas Nejus, Henry Oberholtzer.\n#\n# Permission is hereby granted, free"
},
{
"path": "beetsplug/discogs/types.py",
"chars": 1685,
"preview": "# This file is part of beets.\n# Copyright 2025, Sarunas Nejus, Henry Oberholtzer.\n#\n# Permission is hereby granted, free"
},
{
"path": "beetsplug/duplicates.py",
"chars": 13740,
"preview": "# This file is part of beets.\n# Copyright 2016, Pedro Silva.\n#\n# Permission is hereby granted, free of charge, to any pe"
},
{
"path": "beetsplug/edit.py",
"chars": 13573,
"preview": "# This file is part of beets.\n# Copyright 2016\n#\n# Permission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "beetsplug/embedart.py",
"chars": 10078,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/embyupdate.py",
"chars": 6116,
"preview": "\"\"\"Updates the Emby Library whenever the beets library is changed.\n\nemby:\n host: localhost\n port: 8096\n usernam"
},
{
"path": "beetsplug/export.py",
"chars": 8007,
"preview": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this"
},
{
"path": "beetsplug/fetchart.py",
"chars": 56532,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/filefilter.py",
"chars": 2890,
"preview": "# This file is part of beets.\n# Copyright 2016, Malte Ried.\n#\n# Permission is hereby granted, free of charge, to any per"
},
{
"path": "beetsplug/fish.py",
"chars": 10158,
"preview": "# This file is part of beets.\n# Copyright 2015, winters jean-marie.\n# Copyright 2020, Justin Mayer <https://justinmayer."
},
{
"path": "beetsplug/freedesktop.py",
"chars": 1414,
"preview": "# This file is part of beets.\n# Copyright 2016, Matt Lichtenberg.\n#\n# Permission is hereby granted, free of charge, to a"
},
{
"path": "beetsplug/fromfilename.py",
"chars": 5725,
"preview": "# This file is part of beets.\n# Copyright 2016, Jan-Erik Dahlin\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/ftintitle.py",
"chars": 12799,
"preview": "# This file is part of beets.\n# Copyright 2016, Verrus, <github.com/Verrus/beets-plugin-featInTitle>\n#\n# Permission is h"
},
{
"path": "beetsplug/fuzzy.py",
"chars": 2257,
"preview": "# This file is part of beets.\n# Copyright 2016, Philippe Mongeau.\n#\n# Permission is hereby granted, free of charge, to a"
},
{
"path": "beetsplug/hook.py",
"chars": 3066,
"preview": "# This file is part of beets.\n# Copyright 2015, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/ihate.py",
"chars": 2849,
"preview": "# This file is part of beets.\n# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.\n#\n# Permission is hereby gra"
},
{
"path": "beetsplug/importadded.py",
"chars": 5723,
"preview": "\"\"\"Populate an item's `added` and `mtime` fields by using the file\nmodification time (mtime) of the item's source file b"
},
{
"path": "beetsplug/importfeeds.py",
"chars": 5293,
"preview": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/importsource.py",
"chars": 5848,
"preview": "\"\"\"Adds a `source_path` attribute to imported albums indicating from what path\nthe album was imported from. Also suggest"
},
{
"path": "beetsplug/info.py",
"chars": 7088,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/inline.py",
"chars": 4522,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/ipfs.py",
"chars": 10363,
"preview": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this"
},
{
"path": "beetsplug/keyfinder.py",
"chars": 3119,
"preview": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/kodiupdate.py",
"chars": 3383,
"preview": "# This file is part of beets.\n# Copyright 2017, Pauli Kettunen.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/lastgenre/__init__.py",
"chars": 21033,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/lastgenre/client.py",
"chars": 5142,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n# Copyright 2026, J0J0 Todos.\n#\n# Permission is hereby g"
},
{
"path": "beetsplug/lastgenre/genres-tree.yaml",
"chars": 16156,
"preview": "- african:\n - african heavy metal\n - african hip hop\n - afrobeat\n - apala\n - benga\n - bikutsi\n - bo"
},
{
"path": "beetsplug/lastgenre/genres.txt",
"chars": 17282,
"preview": "2 tone\n2-step garage\n4-beat\n4x4 garage\n8-bit\nacapella\nacid\nacid breaks\nacid house\nacid jazz\nacid rock\nacoustic music\naco"
},
{
"path": "beetsplug/lastimport.py",
"chars": 9421,
"preview": "# This file is part of beets.\n# Copyright 2016, Rafael Bodill https://github.com/rafi\n#\n# Permission is hereby granted, "
},
{
"path": "beetsplug/limit.py",
"chars": 3000,
"preview": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this"
},
{
"path": "beetsplug/listenbrainz.py",
"chars": 10037,
"preview": "\"\"\"Adds Listenbrainz support to Beets.\"\"\"\n\nimport datetime\n\nimport requests\n\nfrom beets import config, ui\nfrom beets.plu"
},
{
"path": "beetsplug/loadext.py",
"chars": 1517,
"preview": "# This file is part of beets.\n# Copyright 2019, Jack Wilsdon <jack.wilsdon@gmail.com>\n#\n# Permission is hereby granted, "
},
{
"path": "beetsplug/lyrics.py",
"chars": 39084,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/mbcollection.py",
"chars": 8914,
"preview": "# This file is part of beets.\n# Copyright (c) 2011, Jeffrey Aylesworth <mail@jeffrey.red>\n#\n# Permission is hereby grant"
},
{
"path": "beetsplug/mbpseudo.py",
"chars": 12522,
"preview": "# This file is part of beets.\n# Copyright 2025, Alexis Sarda-Espinosa.\n#\n# Permission is hereby granted, free of charge,"
},
{
"path": "beetsplug/mbsubmit.py",
"chars": 3378,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson and Diego Moreda.\n#\n# Permission is hereby granted, free "
},
{
"path": "beetsplug/mbsync.py",
"chars": 7104,
"preview": "# This file is part of beets.\n# Copyright 2016, Jakob Schnitzer.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/metasync/__init__.py",
"chars": 4390,
"preview": "# This file is part of beets.\n# Copyright 2016, Heinz Wiesinger.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/metasync/amarok.py",
"chars": 4020,
"preview": "# This file is part of beets.\n# Copyright 2016, Heinz Wiesinger.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/metasync/itunes.py",
"chars": 4599,
"preview": "# This file is part of beets.\n# Copyright 2016, Tom Jaspers.\n#\n# Permission is hereby granted, free of charge, to any pe"
},
{
"path": "beetsplug/missing.py",
"chars": 9286,
"preview": "# This file is part of beets.\n# Copyright 2016, Pedro Silva.\n# Copyright 2017, Quentin Young.\n#\n# Permission is hereby g"
},
{
"path": "beetsplug/mpdstats.py",
"chars": 12489,
"preview": "# This file is part of beets.\n# Copyright 2016, Peter Schnebel and Johann Klähn.\n#\n# Permission is hereby granted, free "
},
{
"path": "beetsplug/mpdupdate.py",
"chars": 4102,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/musicbrainz.py",
"chars": 28826,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/parentwork.py",
"chars": 7614,
"preview": "# This file is part of beets.\n# Copyright 2017, Dorian Soergel.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/permissions.py",
"chars": 4197,
"preview": "\"\"\"Fixes file permissions after the file gets written on import. Put something\nlike the following in your config.yaml to"
},
{
"path": "beetsplug/play.py",
"chars": 8764,
"preview": "# This file is part of beets.\n# Copyright 2016, David Hamp-Gonsalves\n#\n# Permission is hereby granted, free of charge, t"
},
{
"path": "beetsplug/playlist.py",
"chars": 7160,
"preview": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this"
},
{
"path": "beetsplug/plexupdate.py",
"chars": 3594,
"preview": "\"\"\"Updates an Plex library whenever the beets library is changed.\n\nPlex Home users enter the Plex Token to enable updati"
},
{
"path": "beetsplug/random.py",
"chars": 4722,
"preview": "# This file is part of beets.\n# Copyright 2016, Philippe Mongeau.\n# Copyright 2025, Sebastian Mohr.\n#\n# Permission is he"
},
{
"path": "beetsplug/replace.py",
"chars": 4050,
"preview": "from __future__ import annotations\n\nimport shutil\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport medi"
},
{
"path": "beetsplug/replaygain.py",
"chars": 54112,
"preview": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson.\n#\n# Permission is h"
},
{
"path": "beetsplug/rewrite.py",
"chars": 2734,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/scrub.py",
"chars": 5115,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/smartplaylist.py",
"chars": 14906,
"preview": "# This file is part of beets.\n# Copyright 2016, Dang Mai <contact@dangmai.net>.\n#\n# Permission is hereby granted, free o"
},
{
"path": "beetsplug/sonosupdate.py",
"chars": 1608,
"preview": "# This file is part of beets.\n# Copyright 2018, Tobias Sauerwein.\n#\n# Permission is hereby granted, free of charge, to a"
},
{
"path": "beetsplug/spotify.py",
"chars": 28905,
"preview": "# This file is part of beets.\n# Copyright 2019, Rahul Ahuja.\n# Copyright 2022, Alok Saboo.\n#\n# Permission is hereby gran"
},
{
"path": "beetsplug/subsonicplaylist.py",
"chars": 6469,
"preview": "# This file is part of beets.\n# Copyright 2019, Joris Jensen\n#\n# Permission is hereby granted, free of charge, to any pe"
},
{
"path": "beetsplug/subsonicupdate.py",
"chars": 5282,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/substitute.py",
"chars": 1773,
"preview": "# This file is part of beets.\n# Copyright 2023, Daniele Ferone.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/the.py",
"chars": 3258,
"preview": "# This file is part of beets.\n# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.\n#\n# Permission is hereby gra"
},
{
"path": "beetsplug/thumbnails.py",
"chars": 9714,
"preview": "# This file is part of beets.\n# Copyright 2016, Bruno Cauet\n#\n# Permission is hereby granted, free of charge, to any per"
},
{
"path": "beetsplug/titlecase.py",
"chars": 9797,
"preview": "# This file is part of beets.\n# Copyright 2025, Henry Oberholtzer\n#\n# Permission is hereby granted, free of charge, to a"
},
{
"path": "beetsplug/types.py",
"chars": 1581,
"preview": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to an"
},
{
"path": "beetsplug/unimported.py",
"chars": 2488,
"preview": "# This file is part of beets.\n# Copyright 2019, Joris Jensen\n#\n# Permission is hereby granted, free of charge, to any pe"
},
{
"path": "beetsplug/web/__init__.py",
"chars": 15625,
"preview": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any"
},
{
"path": "beetsplug/web/static/backbone.js",
"chars": 42593,
"preview": "// Backbone.js 0.5.3\n// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.\n// Backbone may be freely distributed u"
},
{
"path": "beetsplug/web/static/beets.css",
"chars": 2951,
"preview": "body {\n font-family: Helvetica, Arial, sans-serif;\n}\n\n#header {\n position: fixed;\n left: 0;\n right: 0;\n t"
},
{
"path": "beetsplug/web/static/beets.js",
"chars": 9869,
"preview": "// Format times as minutes and seconds.\nvar timeFormat = function(secs) {\n if (secs == undefined || isNaN(secs)) {\n "
},
{
"path": "beetsplug/web/static/jquery.js",
"chars": 248235,
"preview": "/*!\n * jQuery JavaScript Library v1.7.1\n * http://jquery.com/\n *\n * Copyright 2016, John Resig\n * Dual licensed under th"
},
{
"path": "beetsplug/web/static/underscore.js",
"chars": 34498,
"preview": "// Underscore.js 1.2.2\n// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.\n// Underscore is freely distributable"
},
{
"path": "beetsplug/web/templates/index.html",
"chars": 3831,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "beetsplug/zero.py",
"chars": 5891,
"preview": "# This file is part of beets.\n# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.\n#\n# Permission is hereby gra"
},
{
"path": "codecov.yml",
"chars": 309,
"preview": "comment:\n layout: \"header, diff, files\"\n require_changes: true\n\n# Sets non-blocking status checks\n# https://docs.codec"
},
{
"path": "docs/.gitignore",
"chars": 17,
"preview": "_build\ngenerated/"
},
{
"path": "docs/Makefile",
"chars": 4806,
"preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS =\nSPHINXBUILD "
},
{
"path": "docs/_static/beets.css",
"chars": 314,
"preview": "html[data-theme=\"light\"] {\n --pst-color-secondary: #a23632;\n}\nhtml[data-theme=\"light\"] {\n --pst-color-inline-code:"
},
{
"path": "docs/_templates/autosummary/base.rst",
"chars": 104,
"preview": "{{ fullname | escape | underline}}\n.. currentmodule:: {{ module }}\n.. auto{{ objtype }}:: {{ objname }}\n"
},
{
"path": "docs/_templates/autosummary/class.rst",
"chars": 887,
"preview": "{{ name | escape | underline}}\n\n.. currentmodule:: {{ module }}\n\n.. autoclass:: {{ objname }}\n :members: "
},
{
"path": "docs/_templates/autosummary/module.rst",
"chars": 172,
"preview": "{{ fullname | escape | underline}}\n{% block modules %}\n{% if modules %}\n.. rubric:: Modules\n\n{% for item in modules %}\n{"
},
{
"path": "docs/_templates/autosummary/namedtuple.rst",
"chars": 100,
"preview": "{{ name | escape | underline }}\n\n.. currentmodule:: {{ module }}\n\n.. autonamedtuple:: {{ objname }}\n"
},
{
"path": "docs/api/database.rst",
"chars": 491,
"preview": "Database\n========\n\n.. currentmodule:: beets.library\n\nLibrary\n-------\n\n.. autosummary::\n :toctree: generated/\n\n Lib"
},
{
"path": "docs/api/index.rst",
"chars": 123,
"preview": "API Reference\n=============\n\n.. toctree::\n :maxdepth: 2\n :titlesonly:\n\n plugins\n plugin_utilities\n databa"
},
{
"path": "docs/api/plugin_utilities.rst",
"chars": 255,
"preview": "Plugin Utilities\n================\n\n.. currentmodule:: beetsplug._utils.requests\n\n.. autosummary::\n :toctree: generate"
},
{
"path": "docs/api/plugins.rst",
"chars": 273,
"preview": "Plugins\n=======\n\n.. currentmodule:: beets.plugins\n\n.. autosummary::\n :toctree: generated/\n\n BeetsPlugin\n\n.. curren"
},
{
"path": "docs/changelog.rst",
"chars": 304243,
"preview": "Changelog\n=========\n\nChangelog goes here! Please add your entry to the bottom of one of the lists\nbelow!\n\n.. Uncomment t"
}
]
// ... and 318 more files (download for full content)
About this extraction
This page contains the full source code of the beetbox/beets GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 518 files (4.9 MB), approximately 1.3M tokens, and a symbol index with 5087 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.