Skip to content

Architecture

EmilDeuOfficial edited this page Aug 22, 2026 · 1 revision

Architecture

This page is aimed at developers working on or extending the project.


Modules

DeuMediaDownloader/
├── main.py          Entry point, dependency check, launcher loop
├── ui.py            Launcher, three downloader windows, dialogs, widgets
├── downloader.py    Spotify API, yt-dlp logic, task managers for all three sources
├── converter.py     FFmpeg discovery, metadata and cover embedding via mutagen
├── config.py        Version, formats, colors, translations, configuration I/O
├── assets.py        SVG to icon rendering for the platform logos
├── build.py         Fully automated build: PyInstaller plus Inno Setup
├── installer.iss    Inno Setup script
├── img/
│   ├── app.ico      Windows icon
│   └── app-icon.svg Source icon
└── tools/
    └── svg_to_ico.py  Convert SVG to ICO

Dependency direction:

main.py  ->  ui.py  ->  downloader.py  ->  converter.py
               |             |                  |
               +-------------+------------------+---> config.py
               |
               +---> assets.py

config.py has no internal project dependencies and is therefore the bottom layer.


main.py

Three jobs:

  1. Dependency check: uses importlib.util.find_spec to verify that customtkinter, spotipy, yt_dlp, mutagen and requests are available. If something is missing, a Tkinter dialog shows the matching pip install command. The check is skipped when sys.frozen is set, that is, inside the compiled EXE.
  2. FFmpeg check: the result is passed to every window as ffmpeg_available.
  3. Launcher loop:
while True:
    choice = LauncherApp().run()
    if not choice:
        break
    went_back = <MatchingDownloader>(...).run()
    if not went_back:
        break

Each downloader returns whether it was left through the back arrow. Only then does the launcher appear again.


downloader.py

The core. Three parallel and deliberately separate paths for Spotify, YouTube and TikTok.

Shared building blocks

Element Purpose
DownloadStatus Enum: QUEUED, SEARCHING, DOWNLOADING, CONVERTING, EMBEDDING, DONE, ERROR
_sanitize() Windows-safe filenames, pipe and colon become Unicode lookalikes
_apply_template() Applies the naming scheme, removes empty brackets and separators
_ffmpeg_opts() Sets ffmpeg_location for yt-dlp when FFmpeg was found
_RATE_MAP Maps the limit labels (1M, 5M, 10M, 50M) to byte values

Task data classes

Class Fields (excerpt)
TrackInfo track_id, title, artist, album, cover_url, duration_ms, year
DownloadTask task_id, track, output_dir, format_name, status, progress, callbacks
YouTubeTask task_id, url, title, output_dir, format_name, status, progress, callbacks
TikTokTask same as YouTubeTask

Every task carries three optional callbacks: on_progress, on_status and on_done. The UI hooks into them and forwards the calls into the Tk thread via root.after(0, ...).

The manager

All three managers (SpotifyDownloadManager, YouTubeDownloadManager, TikTokDownloadManager) follow the same pattern:

class XDownloadManager:
    def __init__(self, max_workers=2, ffmpeg_ok=True):
        self._pending = queue.Queue()
        threading.Thread(target=self._dispatch_loop, daemon=True).start()

    def submit(self, task):
        self._pending.put(task)

    def _dispatch_loop(self):
        while True:
            task = self._pending.get()
            # wait until a slot is free
            # then start a dedicated thread for the task

A dispatcher thread pulls tasks from a queue and starts one daemon thread per task as soon as a slot frees up. A counter under a lock caps the concurrency, the wait loop polls every 200 milliseconds.

DownloadManager is an alias for SpotifyDownloadManager and exists only for backwards compatibility.

Spotify path

SpotifyClient wraps spotipy. Worth noting:

  • parse_url() recognizes six patterns: the three web URLs for track, playlist and album plus the corresponding spotify: URIs.
  • get_playlist_tracks() pages through with sp.next() until the end and skips podcast episodes as well as entries without an ID.
  • get_album_tracks() fetches cover art and year once from the album and attaches them to every track, because album_tracks() does not include those fields.

_find_best_youtube_match() is the heart of match quality: query five search results and return the one with the smallest duration difference from the Spotify duration. If the duration is missing, the first result is taken. If the search returns nothing, there is a fallback to a simple single-result search.

Progress model

All three paths use the same split:

Phase Share
Download 0 to 80 percent
Conversion 85 percent
Metadata 90 percent
Done 100 percent

The download share comes from the yt-dlp progress hook, the rest is set at the phase transitions.

Error handling

Every download_* function wraps the whole flow in a try/except. On failure the status is set to ERROR, the message is stored in error_msg, and on_done is still called so the UI does not treat the entry as hanging. An error in one task does not affect other tasks.

If the expected target file is not found after the download, a glob for the filename with any extension runs before a FileNotFoundError is raised. That covers cases where the postprocessor produced a different extension.


ui.py

By far the largest file. Structure:

Area Classes
Helper functions _fix_scroll_ghosting, _bring_to_front, _center_geometry, _apply_win11_rounded, _apply_taskbar_button
Shared widgets CustomDropdown
Spotify SettingsDialog, QueueItemWidget, DeuMediaDownloaderApp
YouTube YouTubeSettingsDialog, YouTubeQueueItemWidget, YouTubeDownloaderApp
TikTok TikTokSettingsDialog, TikTokQueueItemWidget, TikTokDownloaderApp
Launcher LauncherApp

Frameless windows

All windows use overrideredirect(True) and draw their own title bar. That requires several Windows-specific interventions through ctypes:

  • _apply_win11_rounded() sets the DWM attribute for rounded corners.
  • _apply_taskbar_button() makes sure the window still gets a taskbar entry despite overrideredirect.
  • _bring_to_front() pulls the window to the foreground on startup.
  • Dragging, minimizing and maximizing are handled by custom title bar handlers.

Windows start at alpha 0, get built, and are only then made visible. That way no widget assembly is ever visible to the user.

Scroll ghosting

_fix_scroll_ghosting() works around a CustomTkinter problem where widget remnants stay on screen while scrolling. The function hooks into yscrollcommand and forces a repaint via InvalidateRect and UpdateWindow. The history of this fix spans several versions between v1.4.7 and v1.5.3.

Threading rule

Network and file access always run in background threads. Every message back to the UI goes through self._root.after(0, lambda: ...). This rule applies project-wide and must not be broken, otherwise Tk freezes or crashes.


converter.py

Two areas of responsibility.

Finding FFmpeg: find_ffmpeg() searches the PATH, the winget package directory and three common installation paths. It returns the full path to ffmpeg.exe or None.

Writing metadata: embed_metadata() dispatches by file extension to five specialized functions for MP3, FLAC, OGG, M4A and WAV. The format differences are substantial, ID3 frames versus Vorbis comments versus iTunes atoms, hence the separate implementations. fetch_cover() downloads the cover image via requests with a 10 second timeout and returns None on any error.


config.py

Pure data layer without logic:

Area Contents
APP_VERSION The single place for the version number
AUDIO_FORMATS, VIDEO_FORMATS Format definitions including yt-dlp selectors
COLORS GitHub dark style palette with Spotify green as the accent
STRINGS Translations for en and de
DEFAULT_CONFIG Defaults for every key
load_config(), save_config() JSON I/O with a merge over the defaults
T(key) Translation function with a fallback to English

A new configuration key is only ever added to DEFAULT_CONFIG, the merge in load_config() handles migration of existing installations by itself.


assets.py

Renders img/app-icon.svg at runtime into CTkImage objects for the three platform logos. It contains a custom SVG path parser that resolves translate transforms and scales the path commands, plus aggdraw for antialiased output. warmup_icons() is called in a background thread when the launcher starts so the lru_cached icons are already available when a downloader opens.


Adding a new platform

The existing pattern can be reused for a fourth source like this:

  1. In downloader.py: add is_x_url(), XTask, extract_x_entries(), download_x_task() and XDownloadManager following the TikTok example.
  2. In config.py: add keys with their own prefix to DEFAULT_CONFIG and translations to STRINGS for both languages.
  3. In ui.py: add XQueueItemWidget, XSettingsDialog and XDownloaderApp, most easily modeled on the TikTok variants.
  4. In assets.py: add an icon function.
  5. In ui.py: hook a fourth card into LauncherApp._build_ui().
  6. In main.py: add a new branch to the launcher loop.

Then run the build process from Build and Release.

Clone this wiki locally