linuxmint / linuxmint/hypnotix
Channel logos: idle callback fires before the file is closed, and failed downloads are cached permanently
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.7k
- Forks
- 242
- PR merge metrics
- No merged PRs in 30d
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.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at download_channel_logos around L660-L678 in the reported master revision and trace the download, file-writing, and idle-callback flow. Verify behavior for successful downloads, interrupted or failed transfers, and HTTP 200 error bodies. Done means callbacks only process complete files and failed downloads do not leave cache entries that suppress retries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- desktop, networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100