Files
lintunes/lintunes/gui/art_window.py
T
trav d335e9144b v0.18.0: paste a link, get the song
File > Import from URL... runs trav's `song` helper (yt-dlp --extract-audio
--audio-format mp3) from inside the app. Everything at the link downloads to
a private temp dir and is imported as each song lands. "and add to current
playlist?" puts the batch above the selected song, else at the end of the
playing playlist, else at the end of the shown one, in the link's order.
Each song goes straight into Identify Track. With no AcoustID key, the
proposal comes from the filename alone.

Album art now opens on a single click, in a window shaped like the cover
and as big as the screen allows.

Also fixed the dev fixture's play journal, which crashed startup (wrong
shape) and would have been ignored anyway (wrong keys). The loader now
skips a journal it can't read instead of aborting.

Claude-Session: https://claude.ai/code/session_01R5m9mXFPHNro78BdD69mG2
2026-09-10 23:44:45 -05:00

86 lines
3.3 KiB
Python

from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QPainter, QPixmap
# Height left free for the window's own title bar. On Wayland the frame size
# isn't known until the window is mapped, so this is a fixed allowance.
DECORATION_ALLOWANCE = 48
def fit_size(art: QSize, available: QSize,
decorations: int = DECORATION_ALLOWANCE) -> QSize:
"""The largest size with the art's own shape that fits on the screen.
Scales *up* as well as down: a 300 px cover still opens screen-tall. The
window takes the art's shape rather than the screen's, so there are no
black bars.
"""
room = QSize(max(available.width(), 1),
max(available.height() - decorations, 1))
if art.isEmpty():
return room
return art.scaled(room, Qt.AspectRatioMode.KeepAspectRatio)
def _aspect(pixmap: QPixmap | None) -> float | None:
if pixmap is None or pixmap.isNull() or pixmap.height() == 0:
return None
return pixmap.width() / pixmap.height()
class ArtWindow(QWidget):
"""Non-modal window showing the current track's album art as large as
possible. Just an album art window — closing it has no side effects."""
def __init__(self, parent=None):
super().__init__(parent, Qt.WindowType.Window)
self._pixmap: QPixmap | None = None
self.setMinimumSize(120, 120)
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
def show_artwork(self, pixmap: QPixmap | None, title: str, screen=None):
"""Open (or re-raise) the window, sized to the art on ``screen``."""
self.set_artwork(pixmap, title)
self.fit_to_screen(screen)
self.show()
self.raise_()
self.activateWindow()
def set_artwork(self, pixmap: QPixmap | None, title: str):
self.setWindowTitle(title or "Album Art")
reshape = (self.isVisible() and _aspect(pixmap) is not None
and _aspect(self._pixmap) is not None
and abs(_aspect(pixmap) - _aspect(self._pixmap)) > 0.01)
self._pixmap = pixmap
if reshape:
# A track change brought a different shape of cover: re-fit, or
# the new art would sit between black bars.
self.fit_to_screen()
self.update()
def fit_to_screen(self, screen=None):
if self._pixmap is None or self._pixmap.isNull():
return
screen = screen or self.screen() or QApplication.primaryScreen()
if screen is None:
return
geo = screen.availableGeometry()
size = fit_size(self._pixmap.size(), geo.size())
self.resize(size)
# Wayland ignores move() (the compositor places the window); X11
# honors it.
self.move(geo.x() + (geo.width() - size.width()) // 2,
geo.y() + (geo.height() - size.height()) // 2)
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.GlobalColor.black)
if self._pixmap is None or self._pixmap.isNull():
return
scaled = self._pixmap.scaled(
self.size(), Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
x = (self.width() - scaled.width()) // 2
y = (self.height() - scaled.height()) // 2
painter.drawPixmap(x, y, scaled)