No description
  • Python 98.4%
  • Shell 1.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-08 23:51:48 +01:00
.pi initial commit 2026-07-30 09:02:41 +01:00
beetsplug/playlistmanager replace relative_to option with music_directory 2026-09-08 23:51:48 +01:00
ci add arch package 2026-09-06 15:58:10 +01:00
packaging/arch add arch package 2026-09-06 15:58:10 +01:00
tests replace relative_to option with music_directory 2026-09-08 23:51:48 +01:00
.gitignore initial commit 2026-07-30 09:02:41 +01:00
.python-version initial commit 2026-07-30 09:02:41 +01:00
AGENTS.md sort query results in TUI by artist name -> album name -> track number 2026-07-30 09:39:58 +01:00
mise.toml initial commit 2026-07-30 09:02:41 +01:00
pipeline.yaml add arch package 2026-09-06 15:58:10 +01:00
pyproject.toml replace relative_to option with music_directory 2026-09-08 23:51:48 +01:00
README.md replace relative_to option with music_directory 2026-09-08 23:51:48 +01:00
uv.lock use a legacy token for plex auth 2026-08-03 23:41:18 +01:00

beets-playlistmanager

A playlist management plugin for beets.

Configuration

Enable the plugin and, optionally, choose where its sidecar SQLite database is stored:

plugins: playlistmanager

playlistmanager:
  database: /path/to/playlist.db
  targets:
    - name: plists
      kind: m3u
      directory: /run/media/sam/M0PRO/Playlists
      music_directory: ../Music

The database defaults to playlist.db in the beets configuration directory, alongside the default config.yaml and library.db. Relative database paths are also resolved from that directory.

Each rendering target needs a unique name. Supported kinds are m3u and files. A target's directory is created when needed, and relative target directories are resolved from the beets library directory.

M3U targets

music_directory is the location of the music as seen from the generated playlist. Tracks keep their paths within the beets library, prefixed with this directory. A relative value is relative to the playlist file's directory. For example, the configuration above renders a library track at Artist/Album/Song.flac as ../Music/Artist/Album/Song.flac, referring to /run/media/sam/M0PRO/Music/Artist/Album/Song.flac when played from the mount. The host's library location is not included in that playlist entry.

  • Omit music_directory to point to the existing library files using paths relative to the generated playlist.
  • Use . when music is stored alongside the playlists, retaining the library's directory layout.
  • Use an absolute path such as /Music to write absolute track paths on the player. ~ and environment variables are expanded on the host.

An M3U target only writes playlists. When using music_directory, the music must already exist at that location with the same layout as the beets library. Tracks outside the library directory cannot be remapped and cause a render error.

relative_to has been replaced and now reports a configuration error. Replace relative_to: library with music_directory: .; remove relative_to: playlist to use the default behavior. For an old absolute relative_to value, choose the music's actual location from the playlist's perspective instead of copying the old base directory into the new option.

Audio file targets

Use a files target for a player that browses directories instead of playlists:

playlistmanager:
  targets:
    - name: mp3-player
      kind: files
      directory: /media/sam/PLAYER/Playlists
      paths:
        default: $playlist/$playlistnum $title - $artist
      prune: true

Run beet playlist sync to create independent audio copies such as Road Trip/01 Song Title - Artist.mp3. Source files, library paths, and embedded tags stay unchanged. Copies keep their original audio encoding; this target does not transcode, so the source formats must already work on the player.

paths uses beets' path-template syntax, including metadata fields, flexible fields, template functions, and ordered query overrides. The special comp and singleton keys work as in beets. For example:

paths:
  'genre:Classical': $playlist/$playlistnum $composer - $title
  default: $playlist/$playlistnum $title - $artist

If omitted, paths defaults to the first example. If supplied, it must contain default. Paths are relative to the target directory and have the source file extension appended automatically (lowercased, as in beets). The target uses the library's path replacements, asciify_paths, and filename-length handling. It does not change beets' global paths settings.

These additional fields are available during export:

Field Meaning
$playlist Playlist name, with path separators sanitized.
$playlistnum One-based position, padded to at least two digits, or three for a playlist of 100–999 entries, and so on.
$playlisttotal Total playlist entries, including duplicate and missing items.
$playlistuuid Stable playlist UUID, useful in custom directory layouts.

Duplicate occurrences produce separate copies. Missing library items are skipped with warnings and leave gaps in numbering. Missing or unreadable source files abort the export before replacing or pruning audio. Empty playlists create an empty playlist directory with the default layout; custom layouts do not create directories for empty playlists. Numbered filenames support lexical playback order; check how your particular player sorts directory entries.

Choose an initially empty, dedicated export directory outside the beets library tree. A hidden .playlistmanager directory records ownership by target name and library database path, file hashes, and pending work. Keep this state with the export; do not share the root between targets. Files are hashed on each sync, and unchanged copies are left in place.

By default, prune: true removes previously exported files after copying succeeds when entries or playlists are deleted, renamed, reordered, or get new paths. Only owned files are removed. prune: false retains obsolete copies and their ownership records, and reports how many were retained. Unrelated files are left alone; only empty directories created by the exporter are removed.

Sync refuses to overwrite untracked files or overwrite/delete externally modified exports. It also rejects path traversal, symlinks in destination paths, and collisions after sanitization, truncation, case folding, or Unicode normalization. Add $playlistnum or $playlistuuid to resolve filename collisions. Case-only renames that collide with existing paths also need an intermediate distinct name. Playlist names that become the same directory after sanitization (for example, Road/Trip and Road:Trip) are rejected rather than merged. Use a directory template such as $playlist/$playlistuuid to distinguish them. Literal shared parent directories, such as Playlists/$playlist, remain supported.

Changed audio is staged on the destination before replacement, which requires space for the changed copies alongside the previous export. Commits are atomic per file, not for the whole target. After a failed or interrupted sync, fix the reported problem and rerun beet playlist sync; pending ownership changes are reconciled automatically. Do not remove the pending state to bypass an error. If state is missing or corrupt, restore a matching backup of the export and its state, or choose a new empty export directory. To keep an externally edited file, move it out of its managed path before retrying.

The prototype uses POSIX advisory locking to prevent concurrent syncs to the same root and is supported on Linux and macOS hosts.

Commands

Create an empty playlist:

beet playlist create "Road Trip"

List the stored playlists, one name per line:

beet playlist list

List a playlist's tracks in playlist order, using the normal beets item format. Duplicate occurrences are printed more than once:

beet playlist get "Road Trip"

Entries referring to items no longer present in the library are skipped with a warning.

Add all items matching a beets query to the end of a playlist:

beet playlist add "J-Pop" "artist:YOASOBI"

Remove all occurrences of items matching a query from a playlist:

beet playlist remove "J-Pop" "artist:YOASOBI"

The add and remove commands print the proposed item-level changes and ask for confirmation before updating the playlist.

Delete a playlist and all of its entries:

beet playlist delete "Road Trip"

The delete command asks for explicit confirmation before removing the playlist.

Render every stored playlist to every configured target:

beet playlist sync

An M3U target writes a UTF-8 <playlist name>.m3u file containing one track path per line. Playlist order and duplicate track occurrences are preserved. Entries referring to items no longer present in the beets library are skipped with a warning. Sync overwrites files for current playlists but does not delete stale or unrelated M3U files from the target directory.

Plex authentication

Start the classic Plex PIN login with:

beet playlist plex login

The command prints the Plex Auth App URL and also tries to open it in a web browser. You can open the printed URL on another machine when you use SSH. The command waits for authorization and then stores the long-lived Plex account token. The token has no automatic refresh. It stays valid until Plex revokes it or you remove the authorized device.

Use beet playlist plex status to see whether login is required. The command never displays the token.

List the Plex Media Server instances associated with the account:

beet playlist plex list

The command labels each server's display name and machine identifier. Display names can be duplicated or changed. The stable machine identifier is the value for target.server.

Use that identifier to list the server's library sections:

beet playlist plex list MACHINE_IDENTIFIER

This command shows each section name, section ID, and Plex type. The section ID is the value for target.library. Only a section with the Plex type artist can be used as a music playlist target. Resource discovery uses the account token. Requests to a selected server use its resource-specific access token.

Remove the local credentials with:

beet playlist plex logout

Local logout does not revoke the authorized device at Plex. To revoke it remotely, also remove the device from the Authorized Devices page in Plex account settings.

The client identifier and account token are in the configured playlistmanager.database, not in config.yaml, an environment variable, or a command argument. SQLite does not encrypt them. Protect the database, its journal and WAL files, and all backups as secrets. New database files have owner-only permissions where the platform supports them. The plugin warns about an existing file with broader permissions but does not modify it.

The classic-token schema migration deletes an existing JWT login because that login cannot authenticate Plex Media Server. Run beet playlist plex login once after migration. Playlist and playlist-entry data are not changed.

Copying or synchronizing the playlist database also copies the Plex client identifier and account token. Use a separate database per machine, or accept that all copies act as the same authorized Plex device.

Querying by playlist

Use the playlist query field anywhere beets accepts an item query:

beet list "playlist:My Mix"

Playlist names are matched case-insensitively. Quote the query when the name contains spaces. The filter composes with other beets queries and sorting:

beet list "playlist:My Mix" "genre:Rock" year+

This is a membership query: results use the normal beets sort order, and a track appears once even if the playlist contains it more than once.

Interactive editor

Install the optional TUI dependency:

pip install 'beets-playlistmanager[tui]'

Then open an existing playlist in the interactive editor:

beet playlist edit "Road Trip"

Enter a beets query and press Enter to populate the search pane. Space marks tracks in the focused pane; A appends marked search results and D removes marked playlist occurrences. Changes are staged until Ctrl+S saves them. Esc cancels, asking for confirmation when there are unsaved changes.

The playlist pane represents occurrences rather than unique track IDs, so a single occurrence of a duplicated track can be removed independently.

Arch Linux releases

pipeline.yaml builds this plugin as beets-playlistmanager-VERSION-1-any.pkg.tar.zst and publishes it to the coop Arch registry, in the arch group. Source comes from things/beets-playlistmanager; publishing uses the existing ((coop-forgejo.token)) credential, which needs repository read and package write access.

Install or update the pipeline in the coop team:

fly -t ci set-pipeline --team=coop -p beets-playlistmanager-release -c pipeline.yaml --check-creds -n
fly -t ci unpause-pipeline --team=coop -p beets-playlistmanager-release

To release:

  1. Update project.version in pyproject.toml, run uv lock, and commit and push the release source, including the CI and packaging files.

  2. Tag that commit with the matching stable version and push the tag:

    git tag v1.2.3
    git push origin v1.2.3
    

The Git resource matches v* tags and processes every detected version serially. The build requires a stable vMAJOR.MINOR.PATCH tag matching the Python package version. Untagged commits, mismatched versions, and prerelease tags cannot publish. Apply pipeline configuration changes with set-pipeline before releasing; the build scripts and PKGBUILD come from the tagged commit.

The build runs in archlinux:base-devel as an unprivileged user, runs the full pytest suite including the Textual editor tests, then installs the package. It checks beet playlist --help with an isolated BEETSDIR and verifies that the installed SQL migrations can initialize a database and run again safely. Publishing runs only after these checks pass. Transient upload failures are retried; existing releases are never deleted or replaced, and duplicate uploads fail. Forgejo manages repository metadata and signatures.

On an Arch host already configured for the coop/arch pacman repository:

sudo pacman -Syu beets-playlistmanager

For the optional interactive editor, also install python-textual (supported versions: >=1.0,<9). It is a test dependency during packaging and an optional runtime dependency. The package includes the SQL migration files. No license has been declared in this project, so the PKGBUILD does not invent one.

To test the build without publishing, use a clean local checkout with a matching release tag:

fly -t ci execute --team=coop -c ci/build-package.yaml \
  -i source=. -o package=/tmp/beets-playlistmanager-package

Use a clean checkout to avoid uploading local credentials or unrelated files.