Repository navigation
Channel logos: idle callback fires before the file is closed, and failed downloads are cached permanently #431
Description
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.
Activity
jebjarrell1974-debug commentedon Sep 15, 2026
Correcting my own evidence above — I edited the issue body to match.
I originally cited "four JPEGs with no
FFD9end marker" as evidence. That was abad check: I later decode-tested all 423 cached images with Pillow and all four of
those load fine. A missing end marker is not the same as an unusable file, and I
should have tested decodability before citing it.
What actually holds up is smaller but clearer: 2 of 423 cached images are genuinely
unusable, and one of them is the 80-byte "could not be found but may be available
again in the future" error page now quoted in the body. The defect is unchanged —
os.path.isfile()at L668 means neither is ever re-requested — but the numbers inthe first version of this report were wrong and overstated.