Imported from har4s/my-telegram-apple-music-downloader (
AGENTS.md). Install upstream withnpx skills add har4s/my-telegram-apple-music-downloader. Copyright stays with the author.
AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Project Overview
A Telegram bot that downloads Apple Music content (songs, albums, playlists) and sends them back to authorized users. The bot uses gamdl (a Python Apple Music downloader) under the hood and processes metadata, lyrics, and cover art before uploading tracks to Telegram.
Tech Stack
- Language: Python 3.12
- Main Dependencies:
python-telegram-bot(v22+) - Telegram bot framework with async supportgamdl(v2.6+) - Apple Music downloader (wraps yt-dlp, pywidevine)mutagen- MP4 metadata manipulationPillow- Image processing for cover art thumbnailspython-decouple- Environment configuration
Development Commands
Running Locally
# Install dependencies using uv (recommended)
pip install uv
uv pip install -r requirements.txt
# Or using pip directly
pip install -r requirements.txt
# Run the bot
python main.py
Docker Deployment
# Build the Docker image
docker build -t telegram-apple-music-bot .
# Run with docker-compose
cd deployment
docker-compose up -d
# View logs
docker-compose logs -f
# Stop the bot
docker-compose down
Dependency Management
Dependencies are managed in pyproject.toml and compiled to requirements.txt using uv:
# Update requirements.txt after changing pyproject.toml
uv pip compile pyproject.toml -o requirements.txt
Architecture
Core Components
The bot is a single-file application (main.py) with the following key parts:
-
API Initialization (
get_api):- Lazy initialization of global
AppleMusicApiinstance - Loads Apple Music cookies from
./data/cookies.txt - Reuses single API instance across all download requests
- Lazy initialization of global
-
Downloader Creation (
create_downloader):- Creates a new
AppleMusicDownloaderinstance per message - Configures output path specific to the message (
dl-{message_id}) - Sets
save_cover=Trueto save cover art (equivalent to-sflag) - Accepts a codec parameter to configure song quality (ALAC, AAC, AAC_LEGACY)
- Initializes all downloader types (song, music video, uploaded video)
- Creates a new
-
Message Handler (
handle_message):- Filters messages for URLs (text links or entities)
- Validates user authorization against
TELEGRAM_ADMIN_ID - Creates unique download directories per message (
dl-{message_id}) - Delegates to async watcher task with URL list
-
Download Watcher (
watch_download):- Implements codec fallback mechanism (ALAC → AAC → AAC_LEGACY)
- For each codec, creates a downloader instance and attempts all URLs
- If any URL succeeds with a codec, stops trying lower quality codecs
- Cleans up failed attempts before trying next codec
- Sends status updates to user
- On success: processes all
.m4afiles in download directory - Calls
prepare_trackandprepare_thumbnailfor each file - Uploads audio with metadata to Telegram
- Cleans up download directory
-
URL Download (
download_url):- Uses gamdl Python API to download a single URL
- Gets URL info and download queue from API
- Downloads each item in the queue
- Returns success/failure status
-
Track Preparation (
prepare_track):- Loads and parses LRC lyrics files (if present)
- Embeds plain text lyrics in
©lyrtag - Embeds synchronized lyrics in custom
SYLTatom (JSON format) - Adds comment tag with bot signature
- Returns title and artist for upload
-
Thumbnail Processing (
prepare_thumbnail):- Reads
Cover.jpgfrom download directory - Resizes to 320x320px using Pillow with LANCZOS resampling
- Returns JPEG BytesIO buffer for Telegram upload
- Reads
-
Lyrics Parsing (
parse_lrc_file):- Parses LRC format timestamps:
[mm:ss.fff]text - Converts to milliseconds for MP4 SYLT atom
- Returns both timestamped entries and plain text
- Parses LRC format timestamps:
Configuration
Environment variables are loaded via python-decouple in config.py:
TELEGRAM_TOKEN(required): Bot token from @BotFatherTELEGRAM_ADMIN_ID(required): Comma-separated list of authorized user IDsREDIS(unused): Legacy config, not currently used by the bot
File Structure
.
├── main.py # Single-file bot with all handlers and logic
├── config.py # Environment variable parsing
├── pyproject.toml # Python package definition
├── requirements.txt # Compiled dependencies (generated by uv)
├── Dockerfile # Container build with ffmpeg installation
└── deployment/
├── docker-compose.yml # Service definition
├── example.env # Environment template
└── _data/
└── cookies.txt # Apple Music session cookies (required)
Important Implementation Details
gamdl Integration
The bot uses the gamdl Python API directly instead of spawning subprocesses:
- Initializes
AppleMusicApifrom Netscape cookies at./data/cookies.txt(global, lazy initialization) - Creates a new
AppleMusicDownloaderinstance per message with message-specific configuration - Each downloader is configured with its own output path (
dl-{message_id}) andsave_cover=True - Downloads are handled through
get_url_info()→get_download_queue()→download()flow
The cookies file must contain a valid Apple Music session with an active subscription. Users can extract cookies using browser extensions (Export Cookies for Firefox, Open Cookies.txt for Chrome).
Codec Fallback System
The bot implements automatic codec fallback to ensure successful downloads:
- Priority order: ALAC (lossless) → AAC (high quality) → AAC_LEGACY (legacy compatibility)
- Fallback logic: Tries each codec in order until at least one URL succeeds
- Cleanup between attempts: Removes failed downloads before trying next codec
- Benefits: Handles cases where higher quality codecs aren't available for certain tracks
The codec priority list is defined in CODEC_PRIORITY at the top of main.py.
MP4 Metadata Tags
The bot uses iTunes-style MP4 atoms (see TAGS dict at top of main.py):
- Standard tags:
©nam(title),©ART(artist),©alb(album), etc. - Custom SYLT atom:
----:com.apple.iTunes:SYLTstores synced lyrics as JSON array - Comment tag: Adds
t.me/myfuckinglifetimeswatermark
Lyrics Format
Synchronized lyrics are stored in two formats:
- Plain text (
©lyrtag): Newline-separated lyrics without timestamps - Synced JSON (
SYLTatom): Array of{"time_ms": int, "text": str}objects
The LRC parser supports multiple timestamps per line (karaoke-style) and handles fractional seconds (1-3 digits).
Async Task Management
The bot uses python-telegram-bot's task system:
context.application.create_task()spawns background download watchers- Multiple downloads can run concurrently (one per message)
- Each task is isolated with its own download directory and downloader instance
- Single global
AppleMusicApiinstance is reused across all tasks - Cleanup happens in
finallyblock to ensure directories are removed
Error Handling
- Download failures are caught per-URL in
download_url()and logged - If all URLs fail, user receives "Download failed" message
- Missing
.m4afiles after download triggers "Nothing to upload" message - Thumbnail processing failures are logged but don't block upload
- All exceptions in
watch_downloadare caught to send error message to user
Common Workflows
Changing Codec Priority
To modify which codecs are tried and in what order:
- Edit the
CODEC_PRIORITYlist at the top ofmain.py - Available codecs:
SongCodec.ALAC,SongCodec.AAC,SongCodec.AAC_LEGACY - Codecs are tried in list order (first to last)
- To disable fallback: use only one codec in the list
Adding New Metadata Fields
- Add the atom name to
TAGSdict (usemutagendocs for atom names) - Extract value from
gamdloutput or LRC file - Set in
track.tags[TAGS["field_name"]]inprepare_track - Call
track.save()before returning
Modifying Lyrics Processing
The lyrics pipeline is:
load_lyricschecks for.lrcfile next to.m4aparse_lrc_fileextracts timestamps and textprepare_trackembeds both plain and synced formats
To change format, modify parse_lrc_file or add a new parser function.
Changing Image Processing
Thumbnail generation in prepare_thumbnail:
- Current: 320x320px JPEG, LANCZOS resampling
- To change dimensions: modify
ImageOps.fit()size tuple - To change format: modify
fitted.save(buffer, format="...") - Return
Noneto skip thumbnail (Telegram will use default)
Docker Container Details
The Dockerfile installs:
- Static ffmpeg binary (required by
gamdlfor audio processing) - Python 3.12 slim base image
- All requirements from
requirements.txt
The container expects:
- Volume mount:
./deployment/_data:/app/data(forcookies.txt) - Environment variables:
TELEGRAM_TOKEN,TELEGRAM_ADMIN_ID
Security Considerations
- User authorization is checked on every message via
TELEGRAM_ADMIN_ID - Cookies file contains Apple Music session - treat as sensitive
- Bot signature is added to
commenttag to identify downloaded tracks - No rate limiting or queue management - handle with care in production