Skip to content

0.25.0

Latest

Choose a tag to compare

@ad-repo ad-repo released this 15 Jun 21:23
· 1 commit to main since this release
41f207f

0.25.0

New Features

  • .cue sheet playback — virtual split (#273) — opening a .cue file (via File → Open, drag-and-drop, or double-click), or opening an audio file that has a sibling .cue next to it, now plays the single backing file virtually split into its cue tracks: one row per track in the now-playing playlist, with the title, performer, and duration taken from the cue. Prev/Next move per cue track and seeking stays within the current track, while playback crosses track boundaries gaplessly — the backing file is scheduled as one continuous stream and a boundary detector advances the playlist row (updating title, seek bar, Now Playing, and history) without touching the audio. Gapless applies with shuffle and repeat-single off; in those modes boundaries still advance correctly but a small gap is expected. Nothing is written to disk and nothing is added to the Local Library; a missing/renamed backing file simply shows its rows as unplayable. The parser reads the first FILE entry (extra ones warn), prefers INDEX 01 (falling back to INDEX 00), and is the exact inverse of the Stream Ripper's chapter-.cue writer, so a rip's own cue round-trips. .cue is now also offered in the File → Open panel's file-type filter.

  • Split .cue albums on import (library, off by default) — a new Library → Split .cue Albums on Import toggle (default off) makes the Local Library scan physically split a single-file album into per-track files when it finds a .cue next to it. When on, each track is cut with ffmpeg and re-encoded to FLAC (re-encoding, not stream-copy, so cuts are sample-accurate) into a per-album subfolder named from the source file's own ALBUM/ARTIST tags (e.g. Artist - Album/), falling back to the cue's performer/title, then the cue filename. Each track inherits the source's metadata (date, genre, embedded cover art) with the title/track-number and album/album-artist set from the source tags; the split tracks are added to the library in the same scan and the original backing file is excluded. The split is idempotent (a re-scan does no work if the per-track files already exist) and filenames are sanitized and de-duplicated so they can't collide or escape the album folder. If ffmpeg isn't installed — or a write fails (permissions, read-only volume, out of space) — splitting is skipped with a one-time notice and the original file imports normally as a single track (it's only hidden once real split tracks exist). When the toggle is off, .cue files are ignored by the scan entirely and the backing file imports as one normal track. Direct-play (above) is unaffected by this toggle. Changing the toggle takes effect on the next scan.

  • Local library reads FLAC/M4A album-artist and track/disc numbers — metadata parsing previously read album-artist and track/disc numbers only from MP3 ID3 frames (TPE2/TRCK/TPOS), so FLAC/OGG (Vorbis ALBUMARTIST/TRACKNUMBER/DISCNUMBER) and M4A (aART/trkn/disk) tracks came back without them — causing single albums to fragment by per-track artist and lose their track ordering. These tags are now read across all containers (handling the 1/10 track form).

  • Stream Ripper — download a URL to FLAC/MP3 or a video file — a new Output → Streaming → Rip URL… action opens a dialog where you paste a URL (auto-filled from the clipboard when it holds a web link) and choose an output type: Audio — FLAC (lossless), Audio — MP3, or video at a resolution/bitrate profile you pick (720p/2.5 Mbps, 1080p/4 Mbps recommended, 1080p/8 Mbps high quality, 1440p/16 Mbps, 4K/35 Mbps, Full/50 Mbps max). Ripping shells out to a system-installed yt-dlp (+ffmpeg); if either isn't found it shows an install hint (brew install yt-dlp ffmpeg) rather than failing silently. Quality is prioritized for audio (bestaudio, then lossless FLAC encode or top-VBR MP3). Video grabs the best source streams within the selected height cap (or no height cap for Full), then ffmpeg creates a playback-safe H.264/AAC MP4 with yuv420p pixels and fast-start metadata so the app and cast targets do not receive VLC-only files. The video source file is temporary ([source]) and is removed after the compatible MP4 is written; existing MP4s are not overwritten. Output is tagged with the source's metadata (title/artist/album/date) and, for audio, the thumbnail is embedded as cover art; the final file is named Artist - Title from that metadata into a folder you pick. If the source has chapter timestamps (common on album/mix uploads), a matching .cue sheet is written alongside the audio — one TRACK per chapter. Progress shows as a spinner + message band at the top of the main window for the duration of the rip (works in both classic and modern UI). When it finishes, a dialog offers Play Now (audio loads into the player; video opens in the video player window, cast-aware), Reveal in Finder, or Done.

  • Local .m3u/.pls playlists in the Plists tab (#269) — the local library browser's Plists tab now lists .m3u, .m3u8, and .pls playlist files found on disk, matching what the Plex/Subsonic/Jellyfin/Emby sources already show there (previously the tab was always empty for the local source). Playlist files are discovered during the normal library scan and their locations persisted in a small library_playlists table — the track contents are not stored, but parsed lazily the first time you expand a playlist. Expanding shows each entry as a row: entries that match a file already in your library carry its metadata and duration, while unmatched paths still appear and remain playable. Double-clicking a playlist row loads and plays the whole list; double-clicking a single entry plays just that track; the disclosure triangle expands as usual. Removing a playlist file from disk drops it from the tab on the next scan, while a transiently unreachable network folder leaves the list intact (the same offline-volume safety guard used for tracks). Implemented identically in both the modern and classic library browsers. Pairs with the earlier "browse by folder structure" work under the same "organize by what's actually on disk" philosophy.

  • Remove Orphaned Entries (library maintenance) — new Library → Clear…Remove Orphaned Entries… action removes library entries whose files are no longer inside any watched folder. These orphans are typically left behind by an older buggy removal that deleted the watch folder but not its entries, so they can't be cleared by removing a folder (none owns them). The action previews the count, auto-creates a backup first, deletes from both memory and the SQLite store (tracks, movies, episodes, playlists), and never touches files on disk. Path matching uses the resolved url.path so it stays fast on large libraries.

  • Browse local library by folder structure — the local library can now be browsed by its actual on-disk folder hierarchy instead of by Artist/Album/Playlist metadata. Rather than adding a ninth tab, the existing Plists tab slot doubles as a toggle: double-click it (local source only) to flip between Plists and Folders; single-click selects whichever the slot currently shows, and the choice persists across launches. The Folders view reflects what is actually on disk right now — including files that haven't been scanned into the library yet — read lazily one directory level at a time as folders are expanded; library metadata (title, duration) enriches a file row only when that file is in the database. Folders sort first, then files, case-insensitively; symlinked directories are skipped to avoid loops. Right-click a folder for Play / Play and Replace Queue / Play Next / Add to Queue / Show in Finder, which recursively collect every supported audio file beneath it. Filesystem enumeration and database lookups run off the main thread (with per-click cancellation and a loading spinner) so large network/NAS folders don't stall the UI. Implemented independently in both the modern and classic library browsers.

Improvements

  • Keychain credentials hardened (#253) — saved server credentials (Plex, Subsonic, Jellyfin, Emby) are now stored with the kSecAttrAccessibleWhenUnlockedThisDeviceOnly accessibility class, so they are only readable while the Mac is unlocked and never sync off the device. The previous permissive per-item ACL has been removed. Entries written by earlier versions are upgraded automatically and lazily the first time each one is read.

Bug Fixes

  • Main-window right-click menu simplified — the main window context menu no longer shows Sleep Timer or Remember State controls. Those settings remain available from the macOS menu bar, keeping the right-click menu focused on playback/window actions.
  • Removing a watch folder now actually deletes its tracks and persists — removing a watched folder removed its tracks/movies/episodes from the in-memory arrays but never deleted the rows from the SQLite store, and the folder row itself often failed to delete too. Because the library browser reads from the store (not the in-memory arrays), removed tracks kept appearing and never went away, and the "removed" folders reappeared on next launch. Two underlying bugs: (1) removeWatchFolder(removeEntries:) only updated memory and called store.deleteWatchFolder for the folder — it now also deletes the matching track/movie/episode rows via new chunked, transactional bulk deletes; (2) MediaLibraryStore.deleteWatchFolder reconstructed the folder URL with URL(fileURLWithPath:), which on an offline network volume can't stat the path to add the trailing slash that stored directory URLs carry (file:///Volumes/home/MUSIC/), so the WHERE clause matched nothing — it now matches the trailing-slash, no-slash, and raw-path forms so offline folders delete correctly. (Pre-existing orphans left by the old behavior aren't retroactively removed by removing a folder — use the new Remove Orphaned Entries… action to clean them.)
  • Watch-folder removal confirmation was invisible — the "Remove Watched Folder?" confirmation opened as a free-floating alert at the normal window level, below the Manage Watch Folders window (which sits at .modalPanel for issue #254) and below any always-on-top windows, so it was hidden off-screen-behind and could never be confirmed — clicking Remove… appeared to do nothing. It's now a window-modal sheet attached to the manager window: always visible, and it blocks the folder table beneath it (you can no longer select another row while it's open). This was the root cause behind "removing folders does nothing."
  • Removing a watch folder took ~20 seconds on large librariesremovalCountsForWatchFolder() and removeWatchFolder() called resolvingSymlinksInPath() once per track to normalize paths — roughly one filesystem call per library item (~60k on a large library). They now compare track.url.path directly (already resolved at scan time), the same optimization watchFolderSummaries() received, making removal near-instant.
  • Removing a watch folder no longer beachballs the app — in the Manage Watch Folders window, clicking Remove… ran removalCountsForWatchFolder() and removeWatchFolder() directly on the main thread. Both block on MediaLibrary's internal dataQueue.sync, and a running import scan (e.g. right after adding/rescanning a folder) holds that queue for many seconds — so the app froze with a spinning beachball until the scan finished. These calls now run off the main thread (only the confirmation alert stays on main, where it must), matching the pattern the window's folder-list reload already used. Also hardened the modern Library Browser's Folders view, which made the same blocking watchFolderSummaries() call on the main thread just before handing off to its background walk — that snapshot now happens inside the background task.
  • Popup dialogs no longer hide behind always-on-top windows (#254) — with Always on Top enabled, opening a popup dialog (e.g. Add Radio Station) appeared to do nothing: the dialog opened at the normal window level, below the main window which had been raised to the floating level, so it was completely obscured until the main window was dragged aside. These transient dialogs now open at the .modalPanel level so they always sit above the app's floating windows, matching the tag-editor and Plex link dialogs that already did this. Covers Add/Edit Radio Station, the Subsonic/Jellyfin/Emby link and server-list sheets, the watch-folder manager, and the auto-tag album candidate picker.
  • Non-Retina classic skin colors fixed (#256) — removed the blanket blue→grayscale conversion in SkinLoader.processForNonRetina() that ran on 1× displays, converting every blue-dominant pixel to gray across all classic skin sprites and stripping legitimate blue tones from every skin. The conversion never ran on Retina, which had masked the bug.
  • Non-Retina Data tab text/chart blur fixed (#257) — the modern Library Browser "Data" tab hosting view is now opaque (isOpaque = true with an opaque skin background, kept in sync on skin change). A clear, non-opaque layer had disabled AppKit font smoothing, blurring text and charts on 1× displays; it now mirrors the classic PlexBrowser twin.
  • Local library expand re-sort fixed (#262) — with a column sort active, double-clicking an Artist or Album to expand it no longer reshuffles the top-level list. The list shown before expanding came from the in-memory column sort (LibraryTextSorter: diacritic-insensitive, numeric, leading-article-aware), but expanding a row rebuilt it from the store's SQLite BINARY collation order and skipped re-sorting once nested rows were present — so the order silently snapped to the raw store order, most visibly around names with special characters, mixed case, or leading articles. The local-library views in ModernLibraryBrowserView and PlexBrowserView now re-sort top-level groups (each leader plus its expanded children) with the same comparator, keeping visible order stable through expand/collapse.
  • HTTP-only internet radio streams now play (#255) — adding a station whose stream URL is plain http:// (e.g. many Icecast/SHOUTcast servers on custom ports) silently failed to play: it sat buffering forever and never started. Internet radio plays through the AudioStreaming library, which fetches over URLSession, but the app's App Transport Security config only declared NSAllowsArbitraryLoadsForMedia — a key that exempts AVFoundation media loads, not URLSession — so cleartext connections were blocked by ATS. The reported ".mp3 links work, others don't" pattern was a coincidence: the working stations happened to be https://, and the real distinction was scheme, not file extension or audio format. Info.plist now sets NSAllowsArbitraryLoads so http stations connect.