-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
This page is aimed at developers working on or extending the project.
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.
Three jobs:
-
Dependency check: uses
importlib.util.find_specto verify thatcustomtkinter,spotipy,yt_dlp,mutagenandrequestsare available. If something is missing, a Tkinter dialog shows the matchingpip installcommand. The check is skipped whensys.frozenis set, that is, inside the compiled EXE. -
FFmpeg check: the result is passed to every window as
ffmpeg_available. - Launcher loop:
while True:
choice = LauncherApp().run()
if not choice:
break
went_back = <MatchingDownloader>(...).run()
if not went_back:
breakEach downloader returns whether it was left through the back arrow. Only then does the launcher appear again.
The core. Three parallel and deliberately separate paths for Spotify, YouTube and TikTok.
| 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 |
| 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, ...).
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 taskA 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.
SpotifyClient wraps spotipy. Worth noting:
-
parse_url()recognizes six patterns: the three web URLs for track, playlist and album plus the correspondingspotify:URIs. -
get_playlist_tracks()pages through withsp.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, becausealbum_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.
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.
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.
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 |
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 despiteoverrideredirect. -
_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.
_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.
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.
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.
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.
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.
The existing pattern can be reused for a fourth source like this:
- In
downloader.py: addis_x_url(),XTask,extract_x_entries(),download_x_task()andXDownloadManagerfollowing the TikTok example. - In
config.py: add keys with their own prefix toDEFAULT_CONFIGand translations toSTRINGSfor both languages. - In
ui.py: addXQueueItemWidget,XSettingsDialogandXDownloaderApp, most easily modeled on the TikTok variants. - In
assets.py: add an icon function. - In
ui.py: hook a fourth card intoLauncherApp._build_ui(). - In
main.py: add a new branch to the launcher loop.
Then run the build process from Build and Release.
DeuMediaDownloader - for personal use only, with content you have the right to download. Respect the terms of service of Spotify, YouTube and TikTok as well as applicable copyright law.