linuxmint / linuxmint/hypnotix

Channel logos: idle callback fires before the file is closed, and failed downloads are cached permanently

Open
#431 1 comment 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.