Skip to content

Repository files navigation

mufify-cli

A terminal music player for local libraries. FLAC first, plus MP3/WAV/OGG/M4A. Your folders are your playlists. Nothing leaves your machine.

The command-line sibling of Mufify, an offline Android music player.

Status: 1.0.0. Scanning, configuration, the mpv engine, the terminal interface, listening statistics, shuffle and key bindings all work, and have been used against real FLAC libraries on macOS and Windows. That is the author's own use, not a crowd's — see Platform verification for what is proven and what is only derived, and HANDOFF.md for the long version.


Requirements

  • Node.js 22 or newer
  • mpv — the audio engine, and a real dependency you must install yourself

mpv is required for playback

mufify-cli does not bundle an audio decoder. It drives mpv over its JSON IPC socket, which is why it plays FLAC, MP3, WAV, OGG, Opus, M4A and the rest without any native compilation at install time.

brew install mpv          # macOS
choco install mpv         # Windows — see the note below
sudo apt install mpv      # Debian, Ubuntu
sudo dnf install mpv      # Fedora
sudo pacman -S mpv        # Arch

On Windows, winget install mpv.mpv does not fetch mpv, and scoop needs scoop update before scoop install mpv will work. Chocolatey is the one that installs it without a dance. Reported from a real Windows run.

Full instructions: https://mpv.io/installation/

mufify-cli will never install mpv for you. Installing system software on your behalf is not something a music player should do quietly. If mpv is missing you get the command above, not a stack trace.

Why an external player instead of a native Node audio binding? Because the native options were measured and they do not survive a modern npm install — npm 12 blocks package install scripts by default, so every prebuilt binary in that ecosystem silently fails to arrive. The evidence is written up in ADR 001.


Install

npm install -g mufify-cli

The package has no install scripts. Nothing is compiled or downloaded when you install it.


Quick start

mufify-cli config set musicDir ~/Music
mufify-cli

That last one opens your library: playlists, albums, artists, all songs and favourites. Move with ↑↓ or j/k, open with , go back with , quit with q.


Commands

Command What it does
mufify-cli Open the library
mufify-cli play [playlist] The same, or jump straight into one playlist
mufify-cli stats Show listening statistics
mufify-cli keybindings Change any shortcut
mufify-cli favorites list Show every marked track
mufify-cli favorites add <path> Mark a track
mufify-cli favorites remove <path> Unmark a track
mufify-cli doctor Check that mpv is installed and usable
mufify-cli scan [path] Walk the music root and report the playlists it finds
mufify-cli config list Show every setting
mufify-cli config get <key> Print one setting
mufify-cli config set <key> <value> Change one setting
mufify-cli config path Print the config file location

Global flags override persisted settings for one run only:

mufify-cli --shuffle balanced --volume 80 scan

--music-dir <path>, --shuffle off|pure|balanced|discovery|favorites|album, --repeat off|all|one, --volume 0-100. Add -y / --yes to scan to skip confirmation prompts in scripts.

Run mufify-cli --help or mufify-cli <command> --help for the full reference.

Browsing

Default Does
↑↓ or j/k move (wraps at both ends)
⏎ or → open
⌫ or ← back — at the top level this quits
PgUp/PgDn jump a screenful
g / e first / last row
f mark the highlighted track as favourite
/ search — filters as you type
tab open the player
q quit

Every shortcut here and in the player can be changed: Key bindings in the menu, or mufify-cli keybindings. Pick a row, press , then press the key you want; x unbinds one and d puts it back. The footer is drawn from your bindings, so it always shows the keys you actually have. Changes go to keybindings.json next to your config, and only the ones you changed are stored — a later release that improves a default still gives it to you.

Hidden actions are in that list too. Page up and down, jump to first and last, "back to the playing track": all rebindable, even though the footer has no room to advertise them.

While playing

Default Does
space play / pause
n / p next / previous (previous restarts after 10s in)
← / → seek 5 seconds
↑ / ↓ volume 5%
r cycle repeat: off → all → one
f mark / unmark the current track as a favourite
tab back to the library
j / k move down / up the queue
play the highlighted track
g put the cursor back on what is playing
q quit

Holding an arrow key is collapsed into a single command rather than flooding the player, so seeking stays responsive instead of chasing the keyboard. Held keys that arrive several at a time are each honoured rather than dropped.

The queue scrolls to keep the cursor in view and grows to fill the terminal. j and k browse it — the arrows stay on seek and volume — and the cursor stops following playback until you press g or play something with , so the list never jumps out from under you mid-read.

Shuffle

mufify-cli config set shuffle balanced   # persists
mufify-cli --shuffle album play          # this run only
Mode Does
off Plays in library order
pure Uniform random. Honest, and it clusters — that is what uniform means
balanced Spreads each artist evenly, so nothing plays three deep by one act
discovery Weights toward what you have played least, without excluding the rest
album Shuffles the albums and leaves the running order inside each one alone

Favourites are marked with f while playing, or with mufify-cli favorites add. They are stored beside the play counts, and a track can be marked before it has ever been played.

A track with no artist tag is its own artist, and one with no album is its own album — untagged files are usually unrelated, so treating them as a single act would spread them as if they were one. Album order inside an album is the order the files sort in; neither mufify-cli nor Mufify reads the track-number tag.

play needs an interactive terminal. Piped into anything it exits with a message rather than drawing escape codes into a file — use scan and stats for that, both of which pipe cleanly.


What order the songs come in

An album plays in the order it was recorded in: disc number, then track number, read from the tags. Files with nothing to go on sort last rather than being guessed at, and fall back to the title. An artist plays one album at a time, each in its own order — not every album's track 1 followed by every album's track 2.

Playlists are the exception, on purpose. A playlist is a folder, and a folder can be a mixtape whose files you numbered yourself; those files carry the track numbers of the albums they came from, so tag order would undo a sequence you chose. Playlists stay in filename order. If your folder is an album, open it under Albums to hear it in order. ADR 014.


How playlists work

Every first-level folder inside your music root is one playlist, named after the folder.

~/Music/
├── Chill/            → playlist "Chill"
│   ├── Disc 1/       → not its own playlist; its tracks belong to "Chill"
│   └── Disc 2/
├── Metal/            → playlist "Metal"
└── stray.flac        → playlist "Music", named after the root folder
  • Nested folders are not separate playlists. Their tracks are collected upward into the first-level folder above them.
  • A first-level folder with no audio anywhere beneath it produces no playlist.
  • Files loose in the root become one playlist named after the root folder itself.
  • The number of playlists always equals the number of non-empty first-level folders.

Symlinks work, and are a good way to curate: Chill/ambient -> Albums/Ambient One puts that album in the Chill playlist as well as Albums. Link loops are detected and skipped rather than followed forever, and a link pointing outside the music root is ignored.

Full reasoning: ADR 005.

Scanning a very large folder

If the music root looks far too broad — a drive root, your home directory, Desktop, Downloads, a system directory — or if it holds more than 50,000 audio files, scan stops and asks before walking it. The threshold is configurable:

mufify-cli config set scanFileLimit 200000

Where your data lives

Platform Location
Linux ~/.config/mufify-cli/
macOS ~/Library/Preferences/mufify-cli/
Windows %APPDATA%\mufify-cli\Config\

config.json holds your settings; stats.json will hold listening statistics. Both are written atomically — a crash or Ctrl+C mid-write cannot corrupt them — and both carry a schema version so future releases can migrate them.

Override the whole directory with MUFIFY_CONFIG_DIR.


Platform verification

This table is a factual claim about what has actually been run, not a compatibility promise. Please keep it honest.

Platform Status
macOS (arm64) Developed and run here. Scanning, config, and playback against a real mpv 0.41.0 verified end to end.
Linux Not run. Same POSIX paths and the same process watchdog as macOS; no known reason to fail.
Windows Run. The named pipe, playback, browsing and key bindings all work. Two gaps remain, listed below.

Windows was written blind — the platform is passed in as an argument so it could be unit-tested from macOS — and has since been run for real, including the whole test suite. That run found a genuine bug: replacing a file by renaming over it is atomic on POSIX but fails on Windows when anything else holds the target open, so statistics writes are now retried. What the unit tests prove is still only that the derivation is right.

Two of the suite's tests are skipped on Windows, visibly rather than quietly. chmod there does nothing to a directory, and one process cannot send another a real SIGINT — Node turns it into a hard kill. Neither is a gap in the program: a real Ctrl+C in a console does take the graceful path, and that ordering is pinned in-process on every platform. See ADR 004.

One guarantee is weaker on Windows

On macOS and Linux, mpv is given an inherited pipe and started with --input-ipc-client, so it quits by itself the moment our end closes — even if mufify-cli is SIGKILLed and no cleanup code of ours can run. mpv's manual requires a specially constructed Win32 named pipe for that mechanism, which we cannot build without a machine to test it on.

So on Windows, a hard kill of mufify-cli may leave an mpv process running. Ordinary exits and Ctrl+C are handled everywhere; it is only the uncatchable case that differs. See ADR 003.


Development

npm install
npm run dev -- --help
npm run lint
npm run typecheck
npm test
npm run build

mpv must be installed to run the tests. The audio engine is verified against a real mpv process, and those tests fail loudly rather than skipping when it is missing — a skipped process-lifecycle test reports green while proving nothing. Test playback runs with --ao=null, so the suite decodes real audio without making any noise.

Start with CONTRIBUTING.md. House rules are in AGENTS.md, and the reasoning behind the awkward parts is in docs/adr/.


License

MIT — see LICENSE.

About

Free, open-source, totally local, statistic tracking, FLAC-supporting music player that works on your terminal.

Topics

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages