Import existing LinTunes project

Snapshot of the existing codebase before working through the TASKS.md
backlog. Real library data (data/) and the iTunes import fixture
(itunes-test-library/) are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 21:12:01 -04:00
commit f6d35fa594
70 changed files with 8992 additions and 0 deletions

164
tests/test_round9.py Normal file
View File

@ -0,0 +1,164 @@
"""Round 9: three 'Open / future' items.
1. New playlist is selected and drops straight into inline rename
(PlaylistTree.create_playlist_interactive).
2. EQ visualizer cycles on / dim / off on click (off goes blank).
3. Album-art recovery plumbing: the .itc cache decoder (lintunes.itc).
"""
from PyQt6.QtCore import QObject, pyqtSignal
from PyQt6.QtWidgets import QAbstractItemView
from lintunes.models import Library, Track
from lintunes.library_manager import LibraryManager
from lintunes.gui.sidebar import PlaylistTree
from lintunes.gui.visualizer import VisualizerWidget, BANDS
from lintunes.itc import decode_itc
def _manager(tmp_path):
library = Library(tracks={1: Track(track_id=1, name="One")})
return LibraryManager(library, tmp_path)
# --------------------------------------------------------------------------
# 1. New playlist: instant select + live inline rename
# --------------------------------------------------------------------------
class TestCreatePlaylistInteractive:
def test_new_playlist_is_selected_and_editing(self, qapp, tmp_path):
manager = _manager(tmp_path)
tree = PlaylistTree(manager)
tree.create_playlist_interactive()
playlists = list(manager.library.playlists.values())
assert len(playlists) == 1
new = playlists[0]
assert new.name == "untitled playlist"
# Selected...
assert tree.current_playlist_id() == new.persistent_id
# ...and the inline editor is open so typing renames it immediately.
assert tree.state() == QAbstractItemView.State.EditingState
def test_committing_the_edit_renames_via_manager(self, qapp, tmp_path):
manager = _manager(tmp_path)
tree = PlaylistTree(manager)
tree.create_playlist_interactive()
pid = tree.current_playlist_id()
# Simulate the user typing a name and the editor committing: the
# itemChanged -> _on_item_renamed -> rename_playlist wiring fires.
item = tree._find_item(pid)
item.setText(0, "Road Trip")
assert manager.library.playlists[pid].name == "Road Trip"
# --------------------------------------------------------------------------
# 2. EQ visualizer blank-when-off
# --------------------------------------------------------------------------
class _FakePlayer(QObject):
audio_buffer = pyqtSignal(object)
playing_changed = pyqtSignal(bool)
track_changed = pyqtSignal(object)
def __init__(self):
super().__init__()
self._playing = False
def is_playing(self):
return self._playing
class TestVisualizerToggle:
def test_click_cycles_on_dim_off(self, qapp):
from lintunes.gui.visualizer import MODE_ON, MODE_DIM, MODE_OFF
player = _FakePlayer()
player._playing = True
vis = VisualizerWidget(player)
assert vis._mode == MODE_ON
vis.mousePressEvent(None) # on -> dim
assert vis._mode == MODE_DIM
vis.mousePressEvent(None) # dim -> off
assert vis._mode == MODE_OFF
vis.mousePressEvent(None) # off -> on (wraps)
assert vis._mode == MODE_ON
def test_off_goes_blank_and_stops(self, qapp):
from lintunes.gui.visualizer import MODE_OFF
player = _FakePlayer()
player._playing = True
vis = VisualizerWidget(player)
vis._bars = [7] * BANDS # pretend mid-animation
vis.mousePressEvent(None) # on -> dim
vis.mousePressEvent(None) # dim -> off
assert vis._mode == MODE_OFF
assert vis._bars == [0] * BANDS # blank, not the held frame
assert not vis._levels.any()
assert not vis._timer.isActive() # stopped
def test_dim_keeps_animating_when_playing(self, qapp):
from lintunes.gui.visualizer import MODE_DIM
player = _FakePlayer()
player._playing = True
vis = VisualizerWidget(player)
vis.mousePressEvent(None) # on -> dim
assert vis._mode == MODE_DIM
assert vis._timer.isActive() # dim animates while playing
def test_cycling_back_on_resumes_when_playing(self, qapp):
from lintunes.gui.visualizer import MODE_ON
player = _FakePlayer()
player._playing = True
vis = VisualizerWidget(player)
vis.mousePressEvent(None) # on -> dim
vis.mousePressEvent(None) # dim -> off
vis.mousePressEvent(None) # off -> on again
assert vis._mode == MODE_ON
assert vis._timer.isActive()
# --------------------------------------------------------------------------
# 3. .itc artwork-cache decoder
# --------------------------------------------------------------------------
def _itc_header():
# A minimal iTunes .itc-style header preamble; decode_itc only cares that
# the real image magic appears after it.
return bytes([0, 0, 1, 0x1c]) + b"itch" + b"\x00" * 8 + b"artw" + \
b"\x00" * 8 + b"item" + b"\x00" * 8
class TestItcDecoder:
def test_extracts_jpeg(self):
image = b"\xff\xd8\xff\xe0" + b"JFIFpayload" + b"\xff\xd9"
blob = _itc_header() + image + b"\x00\x00trailing"
out = decode_itc(blob)
assert out is not None
data, mime = out
assert mime == "image/jpeg"
assert data == image # trimmed to EOI, trailing dropped
def test_extracts_png(self):
sig = b"\x89PNG\r\n\x1a\n"
image = sig + b"IHDRfakechunks" + b"IEND\xae\x42\x60\x82"
blob = _itc_header() + image + b"junk"
out = decode_itc(blob)
assert out is not None
data, mime = out
assert mime == "image/png"
assert data == image
def test_raw_bitmap_is_unsupported(self):
# ARGb/PNGf raw payloads have no JPEG/PNG magic -> can't recover.
blob = _itc_header() + b"ARGb" + bytes(range(64))
assert decode_itc(blob) is None