Skip to content

Instantly share code, notes, and snippets.

@nirjxr26
Last active April 16, 2026 11:07
Show Gist options
  • Select an option

  • Save nirjxr26/c17a2a37c02277d12829c7e18ab4e319 to your computer and use it in GitHub Desktop.

Select an option

Save nirjxr26/c17a2a37c02277d12829c7e18ab4e319 to your computer and use it in GitHub Desktop.
Fetches, caches, and displays brand logos with a fallback to initials when unavailable.

VaultLock Logo Fetching

Patterns from building an offline password manager in Python/PyQt6. The problem sounds simple: show a logo next to each credential. The implementation is less simple.


Treat the network as optional, always

Logo sources fail. They time out, return 404, or come back with HTML instead of an image. If your UI waits on any of that, the app feels broken.

All fetching runs in a QThread. The main thread never touches the network. QML renders initials immediately and updates when a logo arrives — not the other way around.

class LogoFetchWorker(QThread):
    logo_ready = pyqtSignal(str, str)  # domain, cache_path
    fetch_failed = pyqtSignal(str)     # domain

Watch out for: if you emit signals from a worker thread directly into QML without going through a controller slot, you'll get cross-thread warnings and occasional crashes. Route everything through the controller.


Multi-source fallback with response validation

No single logo API is reliable for every domain. Clearbit is great until it isn't. Google favicons exist for almost everything but are sometimes tiny. DuckDuckGo fills gaps.

Try them in order, but don't trust a 200 response. Validate the content before caching it.

sources = [
    f"https://logo.clearbit.com/{self.domain}?size=128",
    f"https://www.google.com/s2/favicons?sz=128&domain={self.domain}",
    f"https://icons.duckduckgo.com/ip3/{self.domain}.ico"
]

# Don't trust Content-Type alone — check magic bytes
magic = content[:12].lower()
is_image = (
    magic.startswith(b'\x89png') or
    magic.startswith(b'\xff\xd8\xff') or  # JPEG
    magic.startswith(b'\x00\x00\x01\x00') or  # ICO
    b'webp' in magic or
    b'<svg' in magic
)

if len(content) < 500 or not is_image:
    continue

Watch out for: servers that return a valid-looking image content type with an HTML error page body. The < 500 bytes check catches most of these before the magic byte check even runs.


Atomic cache writes

Writing directly to the cache file means a crash or interruption mid-write produces a corrupt file that looks valid on the next read. Every subsequent launch tries to load it, fails silently, and refetches — or worse, renders garbage.

Write to a temp file first, then rename.

temp_file = cache_file + ".tmp"
with open(temp_file, 'wb') as f:
    f.write(content)

if os.path.exists(cache_file):
    os.remove(cache_file)
os.rename(temp_file, cache_file)

Watch out for: os.rename is atomic on Unix but not always on Windows across drives. If your cache and temp directories are on different volumes, use shutil.move instead.


Three operational guards in LogoManager

Without these, a real session gets noisy fast.

Memory cache — disk reads on every lookup add up. Cache resolved paths in memory for the session.

Pending fetch deduplication — if two credential tiles for github.com both miss cache at the same time, you don't want two concurrent fetches for the same domain. Track in-flight requests and skip if one is already running.

Failed domain tracking — if Clearbit, Google, and DuckDuckGo all fail for a domain, mark it as failed and don't retry for the rest of the session. Initials are fine. Hammering three APIs repeatedly is not.

if domain in self.failed_domains:
    return None

if domain in self.pending_fetches:
    return None  # already in flight

self.pending_fetches.add(domain)

Watch out for: failed_domains should be session-only, not persisted. A domain that fails today might have a logo tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment