Two separate defects in download_channel_logos (master 0e0fa1c7596f, L660-678).
Both are small and independent of each other.
for channel, image in logos_to_refresh:
if channel.logo_path is None:
continue
if os.path.isfile(channel.logo_path): # (2)
continue
try:
response = requests.get(channel.logo, headers=headers, timeout=10, stream=True)
if response.status_code == 200:
response.raw.decode_content = True
with open(channel.logo_path, "wb") as f:
shutil.copyfileobj(response.raw, f)
self.refresh_channel_logo(channel, image) # (1) inside the with
except Exception as e:
print(e)
1. The decode is scheduled before the file is closed
refresh_channel_logo is @idle_function, so L676 queues a main-loop callback that
opens channel.logo_path and decodes it. That queueing happens inside the
with block, so the main loop can reach the file before the worker thread's
close() flushes Python's remaining write buffer.
copyfileobj moves 64 KB at a time and the buffered writer holds up to 8 KB, so the
tail of the image is the part at risk. The decode then fails, the except in
get_channel_surface swallows it, and the channel shows the generic placeholder
logo for the rest of the session even though the file on disk is complete moments
later.
Fix is to dedent one line so the refresh happens after the file is closed:
with open(channel.logo_path, "wb") as f:
shutil.copyfileobj(response.raw, f)
self.refresh_channel_logo(channel, image)
2. A partial download is cached forever
open(path, "wb") creates the file immediately. If the transfer then fails, times
out, or the process exits, whatever arrived stays on disk. On the next run L668
(if os.path.isfile(...): continue) sees a file and skips it — so that channel is
stuck with a corrupt logo permanently, and clearing the icon cache is the only way
out.
It is not only partial transfers. Some logo hosts answer HTTP 200 with a
plain-text error body, and since only status_code is checked, that body is
written under a .png/.jpg name and cached. One such file in this cache is 80
bytes and reads, in full:
The requested image could not be found but may be available again in the future.
"may be available again in the future" — but L668 guarantees Hypnotix will never ask
again. That channel keeps the placeholder icon permanently.
Decode-testing the 423 images in this cache with Pillow found 2 unusable: that error
page, and one zero-byte file. Neither is ever retried.
Writing to a temporary file and renaming on success fixes it, and makes the cache
atomic for readers at the same time:
tmp = channel.logo_path + ".part"
with open(tmp, "wb") as f:
shutil.copyfileobj(response.raw, f)
os.replace(tmp, channel.logo_path)
self.refresh_channel_logo(channel, image)
with the except branch removing a leftover .part. os.replace is atomic on the
same filesystem, so a reader sees either no file or a complete one, which also
closes defect 1 without relying on the dedent alone.
Environment
hypnotix 5.6 · Python 3.14.7 · PyGObject 3.56.3 · GTK 3.24.52 · glycin 2.1.5 ·
Arch Linux, Wayland. Provider type: Xtream.
Not related to the thread-safety problems in #409, though both were found in the
same session.
Authored by Claude Opus 5 via Claude Code.
Two separate defects in
download_channel_logos(master0e0fa1c7596f, L660-678).Both are small and independent of each other.
1. The decode is scheduled before the file is closed
refresh_channel_logois@idle_function, so L676 queues a main-loop callback thatopens
channel.logo_pathand decodes it. That queueing happens inside thewithblock, so the main loop can reach the file before the worker thread'sclose()flushes Python's remaining write buffer.copyfileobjmoves 64 KB at a time and the buffered writer holds up to 8 KB, so thetail of the image is the part at risk. The decode then fails, the
exceptinget_channel_surfaceswallows it, and the channel shows the generic placeholderlogo for the rest of the session even though the file on disk is complete moments
later.
Fix is to dedent one line so the refresh happens after the file is closed:
2. A partial download is cached forever
open(path, "wb")creates the file immediately. If the transfer then fails, timesout, or the process exits, whatever arrived stays on disk. On the next run L668
(
if os.path.isfile(...): continue) sees a file and skips it — so that channel isstuck with a corrupt logo permanently, and clearing the icon cache is the only way
out.
It is not only partial transfers. Some logo hosts answer HTTP 200 with a
plain-text error body, and since only
status_codeis checked, that body iswritten under a
.png/.jpgname and cached. One such file in this cache is 80bytes and reads, in full:
"may be available again in the future" — but L668 guarantees Hypnotix will never ask
again. That channel keeps the placeholder icon permanently.
Decode-testing the 423 images in this cache with Pillow found 2 unusable: that error
page, and one zero-byte file. Neither is ever retried.
Writing to a temporary file and renaming on success fixes it, and makes the cache
atomic for readers at the same time:
with the
exceptbranch removing a leftover.part.os.replaceis atomic on thesame filesystem, so a reader sees either no file or a complete one, which also
closes defect 1 without relying on the dedent alone.
Environment
hypnotix 5.6 · Python 3.14.7 · PyGObject 3.56.3 · GTK 3.24.52 · glycin 2.1.5 ·
Arch Linux, Wayland. Provider type: Xtream.
Not related to the thread-safety problems in #409, though both were found in the
same session.
Authored by Claude Opus 5 via Claude Code.