Contributing to Downtify

June 2, 2026 · View on GitHub

Thanks for your interest in improving Downtify! This document describes how to set up the project locally, the coding and formatting standards every contribution must follow, and the workflow for submitting changes.

By submitting a pull request you agree that your contribution is licensed under the project's GPL-3.0 license.


Table of contents

  1. Code of conduct
  2. Ways to contribute
  3. Project overview
  4. Prerequisites
  5. Local setup
  6. Running the app
  7. Coding standards
  8. Formatting and linting
  9. Testing
  10. Commits
  11. Pull requests
  12. Branching and releases
  13. Reporting bugs and requesting features
  14. Translations
  15. Security issues

Code of conduct

Be respectful, constructive, and patient. Reviews focus on code, not on the contributor. Harassment, personal attacks, or discriminatory language are not tolerated in issues, PRs, discussions, or commit messages.


Ways to contribute

  • Fixing a bug from the issues page.
  • Adding a feature that has been discussed and accepted in an issue or discussion.
  • Improving documentation (README.md, in-repo docs, or the zensical site under docs/).
  • Adding or improving tests, especially around the Spotify embed scraping, yt-dlp pipeline, and tag embedding.
  • Adding a UI translation (see Translations).
  • Reviewing open pull requests.

For non-trivial changes, open an issue first to align on scope before writing code. This avoids wasted work if the change conflicts with the project's direction.


Project overview

Downtify is a self-hosted Spotify downloader. It resolves track/album/playlist metadata from the public open.spotify.com/embed endpoints (no Spotify Premium / Web API key required), pulls audio from YouTube via yt-dlp, transcodes with ffmpeg, and embeds cover art plus ID3 / Vorbis / MP4 metadata via mutagen. The backend is FastAPI, the frontend is a Vue 3 SPA, and everything ships as a Docker image.

Repository layout:

main.py                # FastAPI boot, logging, CLI args, static SPA serving
downtify/
  api.py               # FastAPI router
  downloader.py        # yt-dlp wrapper, file naming, sanitization
  spotify.py           # open.spotify.com/embed scraping + playlist pagination
  providers.py         # YouTube / yt-music search and match scoring
  lyrics.py            # lrclib lookup + tag embedding + .lrc sidecar
  m3u.py               # M3U / M3U8 playlist generation
  monitor.py           # Playlist watcher (sqlite-backed)
  telemetry.py         # Optional anonymous metrics
frontend/              # Vue 3 + Vite SPA (built into frontend/dist)
tests/                 # pytest suite (Python) + Vitest under frontend/
docker/                # Compose volumes

See CLAUDE.md for the deeper architectural notes and known domain gotchas (Spotify embed schema quirks, yt-dlp anti-bot knobs, tag round-tripping across container formats, etc.).


Prerequisites

You need the following installed locally:

ToolMinimum versionPurpose
Python3.10 (3.13 rec)Backend runtime (Docker pins 3.13)
uvrecentPython project + dependency manager
Node.js20.x +Frontend build (Vue 3 + Vite)
npm10.x +Frontend package manager
ffmpegrecentAudio transcoding (required at runtime)
gitrecentVersion control
Docker + ComposeoptionalOnly needed if you work on the container side

The Python version is pinned in .python-version. uv will pick it up automatically.


Local setup

# 1. Fork the repo on GitHub, then clone your fork
git clone https://github.com/<your-username>/downtify.git
cd downtify

# 2. Add the upstream remote so you can keep your fork up to date
git remote add upstream https://github.com/henriquesebastiao/downtify.git

# 3. Install Python dependencies (creates .venv via uv)
uv sync

# 4. Install frontend dependencies
npm --prefix frontend install

# 5. Copy the example environment file
cp .env.example .env

Edit .env to point DOWNLOAD_DIR at a directory you actually have on disk if you don't want downloads to land in /downloads.


Running the app

Backend (development)

make run
# equivalent to: uv run python main.py web

This starts FastAPI + Uvicorn on DOWNTIFY_PORT (default 8000). In production the backend serves frontend/dist. During development you typically want the Vite dev server side-by-side:

Frontend (development)

npm --prefix frontend run dev

Vite hot-reloads the SPA and proxies API calls to the backend.

Docker

make up      # docker compose up --build -d
make down    # docker compose down

The Docker image is Alpine-based with ffmpeg, tini, and su-exec. UID / GID / UMASK are env-controlled — check Dockerfile and entrypoint.sh.

Docs site

make doc     # uv run zensical serve

Coding standards

All contributions must follow the project's coding and formatting standards. PRs that do not pass lint or format checks will not be merged.

Python

  • Ruff is the single source of truth for style and lint. Configuration lives in pyproject.toml under [tool.ruff] and [tool.ruff.lint].
    • line-length = 79
    • quote-style = 'single'
    • preview = true
    • Lint rule sets: I, F, E, W, PL, PT
  • Type hints are required on all public functions and on any new code. Use from __future__ import annotations (existing convention across the repo).
  • Logging uses loguru (from loguru import logger). Stdlib logging is intercepted in main.py:_InterceptHandler — do not reconfigure it.
  • Do not widen per-file extend-ignore rules in pyproject.toml to silence lint warnings. Fix the code instead.
  • Do not bypass formatting with inline # ruff: noqa / # noqa directives unless there is a documented, justified reason in a comment on the same line.
  • Keep functions small, pure where possible, and free of premature abstraction. Three similar lines are better than a speculative helper.

API stability

The Vue frontend depends on the exact endpoint paths and response shapes documented in downtify/api.py's module docstring. Do not rename or change existing endpoints in a breaking way. Add new endpoints instead, and update the frontend in the same PR.

Frontend (Vue / JS)

  • Formatting handled by Prettier. Config is in .prettierrc.js. Run make format before committing.
  • Tailwind + daisyUI are already wired. Prefer composing existing utility classes over adding new global CSS.
  • Keep components small and colocate their state. New routes go through vue-router.
  • All user-facing strings must live in the i18n locale files (frontend/src/i18n/locales/). Never hard-code English strings into components.

Dependencies

  • Do not add a new top-level dependency without justification. The existing dependencies (fastapi, yt-dlp, mutagen, ytmusicapi, loguru, requests) cover almost every use case. Open an issue if you believe a new dependency is genuinely needed.

  • Python dependencies are managed via uv. Edit pyproject.toml, then run:

    uv lock
    make export      # regenerates requirements.txt from uv.lock
    

    requirements.txt is generated, not hand-edited — the Docker build consumes it.

  • Frontend dependencies are managed via npm. Always commit the updated package-lock.json.

Things to never do

  • Reintroduce spotdl / spotipy / Spotify Web API credentials. They were intentionally removed.
  • Commit frontend/dist, downloads/, data/, or any .mp3 / .m4a artefacts. .gitignore covers these — keep it that way.
  • Hand-edit requirements.txt. Regenerate it via make export.
  • Skip git hooks with --no-verify.
  • Add network calls to tests without recorded fixtures.

Formatting and linting

Run these before pushing. CI runs the same checks and will fail the PR if they don't pass.

make format    # ruff format + ruff --fix + prettier --write
make lint      # ruff check + prettier --check

Behind the scenes:

uv run ruff format .
uv run ruff check . --fix
prettier --write frontend/src/.

If make lint fails, run make format and re-run make lint. Commit the formatting changes in the same commit as the code change, never as a separate "format" commit.


Testing

Both test suites must pass before submitting a PR.

make test

This runs:

npm run test --prefix frontend     # Vitest (frontend)
uv run pytest -x -s -v             # pytest (backend)

Guidelines:

  • Add or extend tests for any change to spotify.py, providers.py, downloader.py, m3u.py, lyrics.py, or monitor.py. The existing tests/test_spotify_embed.py and tests/test_spotify_url.py files cover the embed schema quirks — extend them rather than mocking around them.
  • Tests must run offline. Use fixtures or monkeypatch to stub HTTP. Do not hit open.spotify.com, api.spotify.com, YouTube, or lrclib from a test.
  • Keep tests focused: one behavior per test, descriptive names, no shared mutable state.
  • If you cannot test a code path because it depends on a real download, say so explicitly in the PR description and describe the manual verification you performed.

Commits

  • One logical change per commit. Rebase / squash noisy "fix lint", "wip", "typo" commits before opening the PR.
  • Write commit messages in the imperative mood: "Add playlist pagination", not "Added" or "Adds".
  • Subject line ≤ 72 characters. Add a body when the why is non-obvious.
  • Reference issues with Fixes #123 / Closes #123 in the body when applicable.
  • Do not use --no-verify to skip hooks. If a hook fails, investigate and fix the underlying issue.
  • Do not force-push to main under any circumstances. Force-pushing your own PR branch is fine when rebasing on upstream/main.

Pull requests

Before opening a PR:

  1. Rebase your branch on the latest upstream/main.
  2. make format clean — no diff after running.
  3. make lint passes.
  4. make test passes (both pytest and Vitest).
  5. New behavior is covered by a test, or the PR description explains why not.
  6. If the change touches download behavior, manually run make run, pull one Spotify track and one playlist, and confirm metadata + cover art + lyrics embed correctly. State this verification explicitly in the PR description.
  7. If the change affects the SPA, run npm --prefix frontend run build and confirm the resulting frontend/dist is served correctly by the backend.
  8. If Python dependencies changed, make export was run and the updated requirements.txt is committed.
  9. If the change is user-facing, the README / docs are updated in the same PR.

PR description should include:

  • What changed and why.
  • Link to the issue it fixes / closes, if any.
  • Screenshots or short clips for UI changes.
  • A test plan: what you ran, what you verified manually.
  • Any follow-up work intentionally left out of this PR.

Reviews are an exchange. Expect to iterate. Keep responses to review comments focused on the code, and resolve threads only after the reviewer has agreed the change is good.


Branching and releases

  • All work targets the main branch via PR. There is no long-lived develop branch.
  • Release versioning follows Semantic Versioning. Versions live in pyproject.toml, downtify/__init__.py, frontend/package.json, Makefile, and the Dockerfile labels — keep them in sync.
  • Maintainers bump the version with make version X.Y.Z, which runs version.sh, rebuilds the frontend, and re-formats the tree. Contributors should not bump the version in their PRs.
  • The changelog is generated with make changelog (uses github_changelog_generator).

Reporting bugs and requesting features

  • Use the issues page and pick the appropriate template.
  • For bugs, include: Downtify version, deployment method (Docker vs. local), OS, exact reproduction steps, expected vs. actual behavior, and relevant log output. If a specific Spotify URL triggers the bug, include it — the project deliberately uses only public endpoints, so the URL is not sensitive.
  • For feature requests, describe the problem first, then the proposed solution. Features that require Spotify Premium credentials or the Spotify Web API will not be accepted.

Translations

Adding a UI translation is a small, three-step change documented in the README. In short:

  1. Copy frontend/src/i18n/locales/en.js to a new file named after the IETF language tag (fr.js, de.js, ja.js, …).
  2. Translate the values; leave keys and placeholder tokens ({count}, {file}, …) unchanged.
  3. Register the locale in frontend/src/i18n/index.js.

Missing keys fall back to English, so partial translations are welcome.


Security issues

Do not open a public GitHub issue for security vulnerabilities. Email the maintainer privately at contato@henriquesebastiao.com with a description of the issue and a reproduction. You will receive an acknowledgement within a reasonable time, and a fix coordinated before public disclosure.


Thanks for contributing! ⭐ Stars and good PRs both keep the project alive.