Changelog
All notable changes to WinMusic are documented in this file.
The format follows Keep a Changelog,
and this project adheres to Semantic Versioning.
[1.0.1] - 2026-08-02
Fixed
scripts/build_installer.ps1: auto-detectsISCC.exe(Inno Setup
compiler) instead of assuming it's onPATH. Checks, in order: the
currentPATH, the default Inno Setup 7 (64-bit) install location
(C:\Program Files\Inno Setup 7\), then the default Inno Setup 6
(32-bit) location (C:\Program Files (x86)\Inno Setup 6\). Raises a
clear, actionable error (download link +-IsccPathoverride
instructions) instead of PowerShell's raw "term not recognized" error
when Inno Setup isn't found anywhere.
[1.0.0] - 2026-08-02
First stable release. Feature set is now frozen; the 1.0.x line will
only receive bug fixes.
Added
scripts/generate_screenshot.py: rendersMainWindowoff-screen
against a small in-memory demo library to produce a genuine, reviewable
assets/docs/screenshot.pngfor the README (not part of the shipped
application).
Fixed
LibraryFilterProxyModel: replaced the deprecated
QSortFilterProxyModel.invalidateFilter()calls with the
beginFilterChange()/endFilterChange()pair recommended since Qt
6.6, removing the last remaining deprecation warnings from the test
suite.tests/test_audio_analysis.py: two tests had gone stale after the
PcmCapturecrash-safety fix (introduced while packaging for v0.9.5)
that defers wiring libVLC'saudio_set_format/audio_set_callbacks
from__init__to an explicitattach()call. The tests still
asserted the pre-fix, immediate-registration behaviour and were
failing under a real libVLC install. Rewritten to reflect the current,
correct contract (attach()registers the callbacks exactly once,
idempotently, and only once called) instead of reverting the
underlying safety fix.- Minor style debt cleared project-wide (
black,ruff): three
over-length lines reflowed (core/database/dao.py,
ui/views/playlist_panel.py), one unused import removed, import
ordering fixed, and justifiednoqaannotations added for the Qt
model API overrides that cannot follow Python naming/default-argument
conventions (rowCount,columnCount,headerDatain
LibraryTableModel). Zero outstandingblack/rufffindings.
Changed
- Application version bumped to
1.0.0(main.py,pyproject.toml,
installer/winmusic.iss). README.mdrewritten for the stable release: drops the "planned
for v1.0.0" framing, documents the actual installer download/install
steps, and embeds a real interface screenshot instead of the previous
placeholder note.- Full regression pass:
pytest+pytest-qt, 144 tests passing, 1
skipped (a crossfade-start test that requires a live OpenGL context,
unavailable in headless/offscreen environments — unrelated to this
release).
[0.9.5] - 2026-08-01
Added
- PyInstaller
--onedirpackaging (winmusic.spec, project root):
freezessrc/main.pyinto a windowed (console=False)WinMusic.exe
plus its supportingdist/WinMusic/folder, using the app icon
(assets/icons/winmusic.ico). Bundles theshaders/folder and
assets/icons/as data, and explicithiddenimportsfor the runtime
dependencies PyInstaller's static analysis cannot always discover on
its own (vlc,pytesseract,imagehash,acoustid,PIL).
Optionally vendors the native runtimes the app needs at runtime
(libVLC, Tesseract OCR, Chromaprintfpcalc) from a localvendor/
staging folder (seevendor/README.md) so the packaged app stays
Offline-First: any vendor sub-folder missing at build time is skipped
rather than failing the build, and the matching optional feature
degrades gracefully at runtime, exactly as already implemented in
core/cover_recognition/ocr.pyandcore/fingerprinting/. core/app_paths.py: centralizes resolution of bundled-resource
directories (shaders_dir(),icons_dir(),vendor_dir()) so the
same code works both from a source checkout and from a frozen
PyInstaller--onedirbuild (is_frozen()/sys.executable-relative
resolution instead of a__file__-relative one, which is not reliable
once modules run from PyInstaller's frozen archive).
ui/visualizer/shader_registry.pyandmain.pynow resolve the
shaders folder and the app icon through this module instead of ad hoc
Path(__file__).resolve().parents[...]expressions.- Inno Setup installer script (
installer/winmusic.iss): builds
WinMusicSetup-<version>.exefrom the PyInstaller--onediroutput,
with a Start Menu group + uninstaller shortcut, an optional desktop
shortcut, an optional (opt-in, unchecked by default).mp3file
association, a clean uninstall ([UninstallDelete]removes the
install directory only — the local database/logs/settings under
%AppData%\WinMusicare preserved across reinstalls), 64-bit-only
install (ArchitecturesAllowed=x64compatible), and
MinVersion=10.0to enforce the Windows 10/11 target. scripts/build_installer.ps1: orchestrates the full release build
on a Windows machine — dependency install,pyinstaller winmusic.spec --clean --noconfirm, thenISCC.exe installer\winmusic.iss— failing
fast with a clear error if either step doesn't produce its expected
output.vendor/README.md: documents the exact folder layout
(vendor/vlc/,vendor/tesseract/,vendor/chromaprint/fpcalc.exe)
and upstream sources expected before running the packaging build, and
how to verify a produceddist/WinMusic/folder on a clean Windows
10/11 VM with no Python/VLC/Tesseract installed system-wide.- Test suite extended with
test_v095_features.py(frozen vs.
source-checkout path resolution incore.app_paths, PyInstaller spec
content — onedir/console/icon/datas/hidden imports, Inno Setup script
content — required sections, shortcuts, opt-in.mp3association,
clean-uninstall behaviour, build script wiring, and cross-file version
consistency betweenmain.py/pyproject.toml/winmusic.iss) — 14 new
tests, 143 tests cumulated (VLC-backed playback tests and the
OpenGL-backed visualizer tests remain environment-dependent on a real
libVLC install / GPU context, unrelated to this release).
Changed
- Application version bumped to
0.9.5(main.py,pyproject.toml,
installer/winmusic.iss). .gitignore: excludes vendored native runtime binaries
(vendor/*, keepingvendor/README.mdtracked) in addition to the
existing*.spec/build//dist/exclusions — none of this packaging
build output or third-party binary is published to the source-free
public repository (§9 ofROADMAP.md).
[0.9.0] - 2026-08-01
Added
- About tab (
ui/about_tab.py—AboutTabwidget +AboutDialog):
displays application name, version, copyright, e-mail and website (as
clickable links), and a short license summary. Reachable from a new
Help > About WinMusic… menu entry. - Application and installer icons (
assets/icons/winmusic.ico,
assets/icons/installer.ico): multi-resolution.icofiles (16, 24,
32, 48, 64, 128, 256 px) generated from a single dark-themed,
accent-purple design (scripts/generate_icons.py, Pillow-based, fully
offline/reproducible). The app icon is applied to theQApplication
window icon at startup. - Persisted user settings (
core/database/dao.py—SettingsDAO
backed by the existingapp_settingstable): generic
get/set/get_int/set_int/delete/all key-value accessors with
INSERT ... ON CONFLICT DO UPDATEupsert semantics. Three settings are
now persisted and restored across sessions: last scanned library
folder (library_folder, pre-fills the folder picker), last shader
used in the visualizer (last_shader_id, restored on startup), and
playback volume (volume). - Local error logging (
core/logging_setup.py): rotating local log
file (<app-data>/logs/winmusic.log, 2 MB × 3 backups, UTF-8),
offline-only (no network handler is ever attached), plus a
sys.excepthookthat logs any unhandled exception before the default
handler runs. Initialized at startup inmain.py. Previously-silent
except: passblocks inMainWindow(stored cover read failures,
perceptual-hash computation failures) now log a warning instead of
failing silently, and scan/tag-write failures are logged with
logger.exception/logger.errorin addition to the existing UI
message boxes. - Test suite extended with
test_v090_features.py(SettingsDAO
CRUD/upsert/int-coercion,configure_logging/log file creation and
content, About tab constants) — 11 new tests, 129 tests cumulated
(VLC-backed playback tests remain environment-dependent on a real
libVLC install, unrelated to this release).
Changed
MainWindow: restores persisted settings after the initial UI/library
load (_restore_settings), and now persists the shader selection and
volume as they change, and the scanned folder each timeFile > Scan Folder…is used.main.py: application version bumped to0.9.0; local logging is
configured before the Qt application starts.
[0.8.0] - 2026-08-01
Added
- Online enrichment (
core/online_enrichment/), manual/on-demand only,
never called automatically in the background (Offline-First):MusicBrainzClient: recording search (artist + title) and free-text
release search (used to search from OCR output), both against the
public MusicBrainz web service (JSON API, properUser-Agent).CoverArtClient: downloads the front cover for a MusicBrainz release
id from the Cover Art Archive; raises a dedicatedCoverArtError
(including a clean 404 → "no cover available" case) instead of
crashing the UI.genre_fallback.py: full ID3v1 genre code table (0-79) used to keep
genre resolution functional even fully offline / when MusicBrainz has
no usable genre relation.
- Cover recognition (
core/cover_recognition/), fully offline:image_utils.py: normalizes a pasted/imported screenshot (RGB
conversion, max-dimension downscaling) before OCR/hashing.ocr.py: local text extraction viapytesseract, degrading
gracefully (OcrUnavailableError) instead of crashing when the local
Tesseract engine is missing/broken.perceptual_hash.py: pHash computation and Hamming-distance matching
(imagehash) with a configurable confidence threshold.recognizer.py(CoverRecognizer): orchestrates OCR + perceptual
matching against the covers already known in the local library —
entirely offline, no network call is ever made from this module.storage.py: local on-disk storage for recognized/downloaded covers
(<app-data>/covers/album_<id>.<ext>).
- Cover Recognition screen (
ui/dialogs/cover_recognition_dialog.py):
import a screenshot from a file or the clipboard, see the local OCR
text (editable), review ranked offline matches against the library,
optionally trigger an online MusicBrainz search (explicit button, never
automatic) with a live cover-art preview from the Cover Art Archive,
and confirm via a dedicated "Use This Match" step — nothing is written
to the database or to ID3 tags until the user explicitly confirms.
Reachable from the library's right-click menu ("Identify Cover Art…"). AlbumDAO:set_cover_hash,set_musicbrainz_id,get_by_id,
list_with_cover_hash,list_all.ArtistDAO.set_musicbrainz_id.
TrackDAO.get()(raw entity lookup, used to preserve the existing
genre when cover recognition updates a track's artist/album/year).id3_writer.embed_cover(): writes/replaces the front-coverAPIC
frame on a track's ID3 tags (used after a confirmed cover match).- Database migration
v2: addsalbums.cover_hash(+ index) so
perceptual-hash matching against the library works fully offline. - Test suite extended with
test_online_enrichment.py(MusicBrainz /
Cover Art Archive clients, mocked HTTP, no real network calls) and
test_cover_recognition.py/test_v080_features.py(OCR, pHash,
recognizer, DAO roundtrips,embed_cover) — 109 tests passing (VLC
playback tests remain environment-dependent on a real libVLC install,
unrelated to this release).
Changed
LibraryViewgained acover_recognition_requestedsignal and an
"Identify Cover Art…" entry in the track context menu.MainWindownow ownsapp_data_dir(derived from the SQLite database
path) to resolve where recognized/downloaded covers are stored locally.
[0.7.0] - 2026-08-01
Added
- Final 5 audio-reactive GLSL shaders (
src/shaders/*.frag, GLSL 330 core,
same "Image"-pass-only convention as the first 5), completing the full
set of 10 required by the roadmap: Liquid Metal Bass Ripple (bump-mapped
specular ripple driven by bass), Starfield Treble Sparkle (warp-speed
starfield with treble-driven twinkle), Voronoi Audio Morph (animated
Voronoi cells reacting to mid/onset), Fractal Julia Audio Zoom (Julia set
fractal whosecparameter and zoom are modulated by bass/mid/onset,
capped at 48 iterations for performance), Waveform Ribbon 3D Illusion
(5 layered sine ribbons with a pseudo-3D depth/perspective illusion). ShaderVisualizerWidget(ui/visualizer/shader_widget.py) now renders
through an off-screenQOpenGLFramebufferObjectsized at a configurable
render_scale(default0.75, clamped0.5–1.0via
set_render_scale()) before presenting the result upscaled to the
widget's full resolution through a dedicated present pass. This cuts the
number of fragment-shader invocations roughly quadratically with the
scale factor, targeted at integrated/laptop GPUs, while keeping visual
density (bar counts, cell counts, etc.) unaffected since none of the
shaders hard-code pixel counts.- Smooth crossfade transitions between shaders:
set_shader()now keeps
the outgoing shader's program alive and renders both the outgoing and
incoming shader into separate off-screen framebuffers for
TRANSITION_DURATION_MS(550 ms), blending them via a small present
shader (u_tex_from/u_tex_to/u_mix) instead of hard-cutting on
shader switch, whether triggered from the panel selector or
programmatically. - Test suite extended (shader registry/GLSL validation tests now cover
all 10 shaders instead of 5, plus new coverage forrender_scale
clamping and for the crossfade transition state machine) — 79 tests
cumulated, 100% passing (the crossfade test is skipped, rather than
failed, in environments without a real OpenGL context, e.g. headless
CI without a GPU/X server).
Changed
shader_registry.SHADER_REGISTRYnow lists all 10 shaders; the shader
selector inVisualizerPaneland the full-screen visualizer therefore
expose all 10 without any UI code changes, since both already iterate
list_shaders().
Added
ShaderVisualizerWidget(ui/visualizer/shader_widget.py): a
QOpenGLWidget-based renderer targeting OpenGL 3.3 core. Compiles a
shared full-screen-triangle vertex shader against a loadable GLSL
fragment shader (.frag), driven by a dedicated 60 FPS (~16 ms)
QTimerrender loop that pauses automatically while the widget is
hidden. Falls back to a safe solid-color program and emits
shader_compile_failedif a shader fails to compile/link, instead of
crashing the UI.- Shader registry (
ui/visualizer/shader_registry.py): a Qt-independent
module listing the available shaders, resolving their file paths under
src/shaders/, and validating/loading their GLSL source. - First 5 audio-reactive GLSL shaders (
src/shaders/*.frag, GLSL 330
core, Shadertoy-style "Image" pass only, noiChannel), all driven by
theu_bass/u_mid/u_treble/u_amplitude/u_onset/
u_onset_strength/u_time/u_resolutionuniforms fed from the
v0.5.0UniformBus: Spectrum Bars Radial, Plasma Waves, Tunnel Warp,
Particle Field Pulse, Kaleidoscope Frequency. VisualizerPanel(ui/views/visualizer_panel.py): shader selector
(QComboBox) plus a live embedded preview, added toMainWindow
between the library/playlist splitter and the player bar.- Full-screen visualizer mode (
FullscreenVisualizerWindow): opened via
the panel's "Fullscreen" button or the new Visualizer → Fullscreen
menu action (F11), closes onEsc. - Test suite extended (15 new tests: shader registry listing/lookup,
GLSL source validation for all 5 shaders — required uniforms present,
valid#version/main()— widget shader switching, panel selector
wiring) — 71 tests cumulated, 100% passing.
Fixed
PcmCapture.__init__(core/audio_analysis/pcm_capture.py): current
python-vlcreleases require theopaqueparameter to be passed
explicitly toMediaPlayer.audio_set_callbacks()(it has no default
value); the call was missing it, raisingTypeError: ... missing 1 required positional argument: 'opaque'at startup on real Windows
installs.PcmCapture: the play callback given toaudio_set_callbacks()must be
a ctypes function pointer (CFUNCTYPE(None, c_void_p, c_void_p, c_uint, c_int64)), not a plain bound method; libVLC rejected it with
ctypes.ArgumentError: ... 'method' object cannot be interpreted as ctypes.c_void_p. The callback is now wrapped accordingly and the
reference kept alive on the instance.PcmCapture._play_callback:samples(the pointer to the raw audio
buffer) andcount(the per-channel sample count) were swapped when
computing the byte length and reading from memory, which would have
crashed or read garbage as soon as real playback reached the callback
(previously untested, since prior tests only exercised the callback
indirectly viafeed_raw()).- Test stub (
FakeVlcPlayerintests/test_audio_analysis.py) updated
to match the real 6-argumentaudio_set_callbackssignature, and a new
test now drives the callback through the real ctypes function pointer
(as libVLC would) to cover this class of regression going forward.
[0.5.0] - 2026-08-01
Added
- Real-time PCM capture (
core/audio_analysis/pcm_capture.py): hooks into
libVLC's audio callback API (audio_set_format/audio_set_callbacks)
to tap the currently-playing PCM stream directly — no microphone or
WASAPI loopback device required.AudioPlayer.enable_pcm_capture()
wires this transparently onto the existing playback engine. - FFT band extraction (
core/audio_analysis/fft_analyzer.py): windowed
real FFT (scipy.fft.rfft) extracting bass (20–250 Hz), mid
(250–4000 Hz) and treble (4000–20000 Hz) band energies plus overall RMS
amplitude from each analysis frame. - Onset (beat) detection with temporal smoothing
(core/audio_analysis/onset_detector.py): spectral-flux based detector
with an adaptive threshold (rolling mean + sensitivity × standard
deviation) and a minimum inter-onset interval to avoid duplicate
triggers on sustained transients. - Uniforms bus (
core/audio_analysis/uniform_bus.py): thread-safe,
EMA-smoothed, clamped-to-[0,1] snapshot (u_bass,u_mid,u_treble,
u_amplitude,u_onset,u_onset_strength,u_time) ready for
consumption by the shader rendering engine (v0.6.0+). AudioAnalysisEngine(core/audio_analysis/engine.py): orchestrates
capture → FFT → onset detection → uniforms bus on eachtick(); wired
intoMainWindowon a dedicated 60 FPS (~16 ms) timer, active only
while a track is playing.- Test suite extended (18 new tests: PCM capture/downmix, FFT band
discrimination, onset detection/suppression, uniform bus
smoothing/clamping, end-to-end engine tick) — 56 tests cumulated, 100%
passing.
[0.4.0] - 2026-08-01
Added
- Manual metadata editing (
EditMetadataDialog): form to edit title, artist,
album, genre, track number and year; writes both the SQLite record
(TrackDAO.update_metadata) and the file's ID3 tags via the new
core/metadata/id3_writer.py(mutagen-based, TIT2/TPE1/TALB/TCON/TRCK/TYER). - Custom genre management (
GenreManagerDialog+GenreDAO.rename/delete):
create, rename and delete genres from the Library menu; deleting a genre
unlinks it from tracks instead of deleting them. - Playlists (
PlaylistDAO,PlaylistPanel): create, rename, delete;
add/remove tracks; drag-and-drop reordering (reorder); export to and
import from M3U files (core/library/playlist_io.py). - Rating system (1–5 stars) via right-click context menu on the library
table (LibraryViewcontext menu: Play, Edit Metadata, Add to Playlist,
Set Rating, Delete from Library); ratings persisted through
TrackDAO.set_rating(clamped to 0–5). Play count already incremented by
PlayerControllersince v0.2.0 and now surfaced/edit-able through the
same table. TrackDAO.get_with_details,TrackDAO.delete,
PlaylistDAO.tracks_with_detailsand related joined-query helpers.- Main window now uses a splitter: library table on the left, playlist
panel on the right. - Test suite extended (10 new tests: ID3 write, genre CRUD, track metadata
update/rating, playlist CRUD/reorder, M3U export/import) — 38 tests
cumulated, 100% passing.
[0.3.0] - 2026-08-01
Added
- Main application window (
MainWindow, PySide6QMainWindow): library view
on top, persistent player bar at the bottom,Filemenu (Scan Folder,
Refresh Library, Exit), status bar for scan feedback. - Single frozen dark theme (
ui/themes/dark_theme.py): fixed color palette,
QPalette+ global stylesheet applied viaFusionstyle — no light/dark
switch, per project conventions (§8 of ROADMAP). - Library table view (
LibraryView,LibraryTableModel,
LibraryFilterProxyModel): sortable columns (title, artist, album, genre,
year, duration, bitrate, rating, play count) via clickable headers, backed
byTrackDAO.list_with_details()(SQL join across artists/albums/genres). - Instant search bar filtering across title/artist/album/genre as the user
types, combined with dropdown filters for genre, artist and year
(LibraryFilterProxyModel). - Player bar (
PlayerBar): transport controls (previous/play-pause/next),
volume slider, seek slider with live position/duration labels, wired to
PlayerController. TrackDAO.list_with_details(): joined query returning display-ready rows
for the library table.- Folder scanning triggered from the UI (
File > Scan Folder…), reusing
LibraryScanner, with result/error feedback in the status bar. - Test suite extended (8 new tests) covering the table model and the
filter/sort proxy (search, genre/artist/year filters, combined filters) —
37 tests cumulated, 100% passing.
[0.2.0] - 2026-08-01
Added
AudioPlayer: low-latency playback engine wrappingpython-vlc
(load, play, pause, resume, toggle_pause, stop, seek by ms or ratio,
volume get/set, state, end/error event hooks).PlaybackQueue: playback queue with add/remove/clear and next/previous
navigation.PlaybackHistory: bounded in-memory playback history.PlayerController: orchestrates the player, the queue and the history;
incrementsplay_countin the database on playback and automatically
advances to the next queued track when one ends.- Test suite extended to 29 tests, including real headless playback tests
against libVLC (dummy audio/video output) and controller integration
tests.
Fixed
- Potential libVLC deadlock: control calls (
play,set_media, ...)
triggered from within aMediaPlayerEndReached/MediaPlayerEncounteredError
callback are now dispatched on a dedicated thread instead of running
directly on libVLC's internal event thread. Databaseconnections are now safe to use from the audio callback
thread (check_same_thread=False+ internal lock serializing all
statements).
[0.1.0] - 2026-08-01
Added
- Project scaffolding (
src/,pyproject.toml,black/ruffconfiguration). - SQLite database layer with a versioned migration system (
core/database). - Initial schema:
artists,albums,genres,tracks,playlists,
playlist_tracks,app_settings. - Data models:
Artist,Album,Genre,Track,Playlist. - Data-access layer (DAO) for artists, albums, genres and tracks with
idempotent get-or-create semantics. - ID3 tag reader based on
mutagen(title, artist, album, genre, track
number, year, duration, bitrate). - Recursive local-folder scanner that indexes
.mp3files into the
database, updates existing entries, and removes entries for files that
no longer exist on disk. - Unit test suite (
pytest) covering the database, the DAO layer and the
scanner (11 tests).