From b6536e73ffee4e9655001acf598108d0f8b4f1f5 Mon Sep 17 00:00:00 2001 From: Ronit Gupta Date: Sun, 27 Sep 2026 20:33:29 +0530 Subject: [PATCH 1/2] download_channel_logos: make logo writes atomic and avoid caching partial downloads (#431) --- usr/lib/hypnotix/hypnotix.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index e4241ce..b243b9b 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -667,14 +667,21 @@ def download_channel_logos(self, logos_to_refresh): continue if os.path.isfile(channel.logo_path): continue + tmp_path = f"{channel.logo_path}.part" 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: + with open(tmp_path, "wb") as f: shutil.copyfileobj(response.raw, f) - self.refresh_channel_logo(channel, image) + os.replace(tmp_path, channel.logo_path) + self.refresh_channel_logo(channel, image) except Exception as e: + if os.path.exists(tmp_path): + try: + os.remove(tmp_path) + except Exception: + pass print(e) @idle_function From 24b7aa1dd2d4d9240b4452513d5b0903d8dd4282 Mon Sep 17 00:00:00 2001 From: Ronit Gupta Date: Sun, 27 Sep 2026 20:56:23 +0530 Subject: [PATCH 2/2] download_channel_logos: validate decodable image and use unique temp files for concurrent downloads --- usr/lib/hypnotix/hypnotix.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index b243b9b..539a159 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -4,6 +4,7 @@ import os import shutil import sys +import tempfile import time import traceback import warnings @@ -667,22 +668,36 @@ def download_channel_logos(self, logos_to_refresh): continue if os.path.isfile(channel.logo_path): continue - tmp_path = f"{channel.logo_path}.part" + tmp_name = None try: response = requests.get(channel.logo, headers=headers, timeout=10, stream=True) if response.status_code == 200: response.raw.decode_content = True - with open(tmp_path, "wb") as f: - shutil.copyfileobj(response.raw, f) - os.replace(tmp_path, channel.logo_path) + os.makedirs(os.path.dirname(channel.logo_path), exist_ok=True) + with tempfile.NamedTemporaryFile(dir=os.path.dirname(channel.logo_path), + prefix=".logo_", + suffix=".part", + delete=False) as tmp_file: + tmp_name = tmp_file.name + shutil.copyfileobj(response.raw, tmp_file) + + if os.path.getsize(tmp_name) == 0: + raise ValueError("Downloaded logo is empty") + + # Verify downloaded content is a valid decodable image before caching + GdkPixbuf.Pixbuf.new_from_file(tmp_name) + + os.replace(tmp_name, channel.logo_path) + tmp_name = None self.refresh_channel_logo(channel, image) except Exception as e: - if os.path.exists(tmp_path): + print(e) + finally: + if tmp_name and os.path.exists(tmp_name): try: - os.remove(tmp_path) + os.remove(tmp_name) except Exception: pass - print(e) @idle_function def refresh_channel_logo(self, channel, image):