MediaFilez downloads public media into Discord through one /media command. It tries ordered engines, validates every result, fits oversized video, audio, or images to the current interaction limit, and streams one confirmed attachment without buffering the whole file in memory.
/media supports server installs and user installs. It can run in a server channel, a bot DM, or a private channel.
| Option | Required | Default | Purpose |
|---|---|---|---|
url |
yes | none | Public media page or direct media URL |
output |
yes | none | auto, video, image, or audio |
fit_to_limit |
no | true |
Process oversized media to fit the current Discord limit |
private |
no | false |
Make progress and the final result visible only to the requester |
Each output value changes validation and processing:
| Output | Result |
|---|---|
| Auto (detect media) | Accepts the first validated video, audio, or image and uses the matching processing path |
| Video | Returns playable video |
| Image / video frame | Returns a source image, page thumbnail, or a frame extracted from video |
| Audio | Returns source audio or extracts an MP3 audio track from video |
Private results use Discord's ephemeral interaction reply, so they do not depend on the user's DM settings. An operator can make every guild result private with PUBLIC_REPLIES_IN_GUILDS=false.
Thumbnail no longer appears as a separate choice. Old interactions using its stored value remain valid while Discord propagates the updated command.
The original interaction shows queue position, the active engine, transfer progress when an engine reports it, and the processing or upload stage. A successful reply contains the attachment, its final size, and a Nerd Info button.
Nerd Info opens an ephemeral message with the filename, selected engine, download time, processing time, upload target, final size, and any processing or recovery note. Its small payload lives in the button ID, so the button still works after a bot restart. It does not contain the source URL, cookies, or credentials.
/mediaincludes anAutooutput that accepts validated video, audio, or image media and chooses the matching processing path.- Local installs include FFmpeg and FFprobe packages.
pnpm installalso fetches a SHA-256 verified gallery-dl build into.toolswhen no operator path is supplied. - Instagram has a direct embed-proxy fallback. The default converts
instagram.comtokkkinstagram.com, then downloads the returned media through the same redirect, SSRF, size, and signature checks as any other URL. - Unknown pages gain a metadata extractor for Open Graph, Twitter card, and HTML media tags.
- Share-link wrappers and JSON-LD
contentUrlfields feed the guarded direct downloader. Reddit posts first use the first-party embed path, then a configurable public embed relay when Reddit blocks the bot server's address. - Multiple Cobalt endpoints are tried in order. A failed endpoint enters a short cooldown so queued jobs can skip the dead host.
- Cobalt now covers its full documented host set in the planner and requests a target-aware video quality instead of always requesting the largest source.
- External downloaders run only for recognized public platforms. Unknown pages and direct links stay inside the DNS-, redirect-, and byte-guarded HTTP engines, and gallery-dl enforces the byte ceiling during transfer.
- The queue accepts four jobs by default and two jobs per user. Both values remain configurable.
- Discord's
attachmentSizeLimitis now the normal upload target. The old 7 MiB ceiling is gone. - Final Discord uploads use a bounded-memory multipart stream. Nitro-sized attachments no longer expand into several in-memory copies before delivery.
fit_to_limitnow handles oversized audio and images as well as video. Audio is re-encoded to MP3; images step down JPEG quality and resolution only as far as needed.- An engine-local timeout falls through to the next engine. Only the whole-job abort stops fallback.
- yt-dlp receives a private writable cookie copy for each attempt, so the configured source can remain mounted read-only and concurrent jobs cannot rewrite one shared jar.
- Process-lifetime ownership locks let startup remove abandoned MediaFilez temp directories without touching work owned by another running instance.
Engine order depends on the host and requested output.
Auto starts with the extractor best suited to the host, then falls back through the remaining engines. Direct file URLs use direct HTTP first. Page metadata checks video, audio, and image fields in that order. File signatures and FFprobe streams determine the final media type whenever available, and validation rejects unknown media.
| Source | Default order |
|---|---|
| Direct media URL | direct HTTP |
| YouTube | yt-dlp, YouTube.js, Cobalt, page metadata |
| Instagram auto or image | gallery-dl, yt-dlp, Instagram proxy, Cobalt, page metadata |
| Instagram video or audio | yt-dlp, Instagram proxy, Cobalt, gallery-dl, page metadata |
| Pinterest, Flickr, Imgur | gallery-dl, yt-dlp, Cobalt, page metadata |
| Reddit auto or image | Reddit embed, Reddit proxy, gallery-dl, yt-dlp, Cobalt, page metadata |
| Reddit video or audio | Cobalt, yt-dlp, Reddit embed, Reddit proxy, gallery-dl, page metadata |
| Other Cobalt services | Cobalt, yt-dlp, gallery-dl, page metadata |
| Unknown page | page metadata, direct HTTP |
Each engine writes into its own attempt directory. MediaFilez checks file signatures and FFprobe streams before committing a result. A fallback starts only after the prior attempt stops and leaves no valid file. A process error does not discard a complete file left behind.
For auto and video, MediaFilez asks yt-dlp for an audio-bearing alternative when the first valid video is silent and yt-dlp remains in the plan. It uses the second result if validation passes. If that attempt fails, MediaFilez keeps the original silent video instead of turning a usable download into an error.
Unknown pages can still expose media through standard page metadata or direct HTTP. They are not passed blindly to yt-dlp or gallery-dl: keeping them in the redirect-, DNS-, and byte-guarded HTTP path prevents an arbitrary page from expanding the subprocess network boundary. Add a host to an explicit platform route only after its extractor and security behavior are known.
Recognized platform routes include TikTok short links (vm.tiktok.com and vt.tiktok.com), X/Twitter, Facebook, Tumblr, Bluesky, SoundCloud, Vimeo, Snapchat, Streamable, VK, Bilibili, Dailymotion, Loom, Newgrounds, OK.ru, Rutube, and Twitch. Recognition selects an engine plan; it does not guarantee that the source site will allow a download.
No downloader can guarantee every website: sites change markup, expire media URLs, block data-center addresses, require fresh cookies, or remove extractor access. See the current yt-dlp supported sites and gallery-dl supported sites.
- Node.js 22.13 or newer
- pnpm 11.25
- A Discord application token
Local users do not need to install FFmpeg, FFprobe, yt-dlp, or gallery-dl by hand. The package install supplies them. Run pnpm run preflight to see the exact binary version and path status.
Docker uses Debian FFmpeg plus pinned Python builds of gallery-dl and yt-dlp. Its yt-dlp installation includes curl-cffi for optional browser impersonation.
pnpm installCopy .env.example to .env, then set BOT_TOKEN and CLIENT_ID.
pnpm run check
pnpm run format:check
pnpm run preflight
pnpm run deploy
pnpm startGlobal command updates can take time to appear in every Discord client. pnpm run deploy must run after changing command choices. Deployment upserts /media without deleting Discord's Activity Entry Point or unrelated commands.
The included PM2 file runs prod:start, which publishes the current command definition before starting the bot:
pm2 start ecosystem.config.cjsUseful diagnostics:
pnpm run diagnose:engines -- https://www.instagram.com/reels/DcV3RyRz0sq/
pnpm run smoke:download
pnpm run smoke:process
pnpm run smoke:fit -- <public-video-url>If the gallery-dl download was skipped or interrupted, run:
pnpm run tools:installSet GALLERY_DL_AUTO_INSTALL=false in the shell that runs pnpm install to skip the automatic tool download. GALLERY_DL_PATH can point to an operator-managed executable.
The Compose stack starts MediaFilez and two private Cobalt v11 APIs on an internal network. The container starts the bot but does not publish slash-command changes on its own.
docker compose build mediafilez
docker compose run --rm mediafilez pnpm run deploy
docker compose up -d
docker compose logs -f --tail=100
docker compose run --rm mediafilez pnpm run preflightSet YTDLP_IMPERSONATE=chrome on Docker hosts only after preflight confirms chrome impersonation ready. Leave it disabled for installations whose yt-dlp build does not include curl-cffi.
Cobalt's maintainers state that hosted instances are not intended for unrelated projects without permission. Self-hosting is the safe default. Compose starts the official 11.7.1 image first, then a digest-pinned zImPatrick compatibility build as a second local fallback. Both remain private to the Compose network.
Add more operator-owned endpoints as a comma-separated list:
COBALT_API_ENDPOINTS=https://cobalt-a.example,https://cobalt-b.example
COBALT_MAX_ENDPOINTS=5
COBALT_FAILURE_COOLDOWN_MS=60000MediaFilez tries configured endpoints in order, falls through failures, and cools down dead hosts. Directory results are filtered to the requested service and exclude Turnstile-protected instances. COBALT_API_KEY is sent only to operator-configured endpoints, never endpoints learned from the directory. Directory connections repeat the public-address check when opening the socket, and Cobalt JSON responses have a fixed size limit. COBALT_DIRECTORY_ENABLED remains off because directory entries are third-party services with separate privacy, availability, and authorization rules. The request and response format follows the Cobalt API documentation.
Some public posts still require an authenticated browser session. The hosted bot may use a dedicated operator-controlled source-platform account for this purpose. Users never sign in through MediaFilez and must not provide their own passwords or cookies. Export a fresh Netscape-format cookie file for the dedicated account and set:
MEDIA_COOKIES_FILE=C:\path\to\cookies.txtyt-dlp and gallery-dl share this source. Mount it read-only on a server and never commit it. MediaFilez copies it with private permissions into the current yt-dlp attempt because yt-dlp updates its cookie jar on exit; the mounted source remains unchanged.
services:
mediafilez:
environment:
MEDIA_COOKIES_FILE: /run/secrets/media-cookies.txt
volumes:
- ./secrets/media-cookies.txt:/run/secrets/media-cookies.txt:roYTDLP_COOKIES_FROM_BROWSER is useful for local diagnosis. A cookie file works better on servers because browsers may lock their databases and Windows DPAPI ties decryption to a user session.
Public YouTube links use an anonymous yt-dlp session by default. This avoids sending an operator's general browser session to YouTube and is often faster from data-center addresses. Set YTDLP_COOKIES_FOR_YOUTUBE=true only when restricted YouTube media requires the dedicated account session.
The source platform may associate requests made with these cookies with the dedicated account. MediaFilez does not bypass private-account permissions, paywalls, DRM, or removed content. Download only media you have permission to access and save.
Start with .env.example. Size values accept b, kb, kib, mb, mib, gb, or gib. Timeout values are milliseconds.
| Variable | Default | Purpose |
|---|---|---|
BOT_TOKEN |
required | Discord bot token |
CLIENT_ID |
required | Discord application ID used to publish /media |
PUBLIC_REPLIES_IN_GUILDS |
true |
Guild reply visibility; false forces ephemeral replies |
MAX_DOWNLOAD_SIZE |
500mb |
Maximum source artifact before processing |
MIN_FREE_DISK_SPACE |
1gb |
Free temp-storage floor checked before a job starts |
MAX_CONCURRENT_JOBS |
4 |
Jobs running at once |
MAX_QUEUE_SIZE |
50 |
Waiting jobs accepted before backpressure rejects work |
MAX_CONCURRENT_JOBS_PER_USER |
2 |
Per-user running or queued job cap |
DISCORD_UPLOAD_TARGET_SIZE |
500mb |
Operator ceiling; the interaction limit can lower it |
DISCORD_UPLOAD_ATTEMPTS |
3 |
Verified attachment upload attempts |
DISCORD_UPLOAD_RETRY_DELAY_MS |
1500 |
Base delay between verified upload retries |
STATUS_UPDATE_INTERVAL_MS |
2500 |
Minimum delay between progress-message edits |
| Variable | Default | Purpose |
|---|---|---|
HTTP_RESPONSE_TIMEOUT_MS |
45000 |
Time allowed to receive an HTTP response |
HTTP_IDLE_TIMEOUT_MS |
60000 |
Maximum pause between downloaded chunks |
YTDLP_TIMEOUT_MS |
480000 |
yt-dlp and gallery-dl process timeout |
FFMPEG_TIMEOUT_MS |
600000 |
FFmpeg and FFprobe process timeout |
JOB_TIMEOUT_MS |
840000 |
Whole-job deadline |
DISCORD_REST_TIMEOUT_MS |
300000 |
Discord request and streamed upload timeout |
DISCORD_REST_RETRIES |
0 |
Retries inside discord.js REST calls |
| Variable | Default | Purpose |
|---|---|---|
MEDIA_COOKIES_FILE |
empty | Netscape cookie source shared by yt-dlp and gallery-dl |
YTDLP_COOKIES_FROM_BROWSER |
empty | Local browser-cookie extraction |
YTDLP_COOKIES_FOR_YOUTUBE |
false |
Send the configured cookie source to YouTube; normally unnecessary for public videos |
YTDLP_PATH |
automatic | Operator-managed yt-dlp executable |
YTDLP_CONCURRENT_FRAGMENTS |
4 |
Fragment transfers inside one yt-dlp attempt |
YTDLP_IMPERSONATE |
disabled | Impersonation target; enable only when yt-dlp --list-impersonate-targets reports it as available |
FFMPEG_PATH |
automatic | Operator-managed FFmpeg executable |
FFPROBE_PATH |
automatic | Operator-managed FFprobe executable |
FFMPEG_THREADS |
2 |
Encoder threads per fitting job |
YOUTUBE_JS_ENABLED |
true |
Enables the YouTube.js fallback |
GALLERY_DL_ENABLED |
true |
Enables gallery and image extraction |
GALLERY_DL_PATH |
automatic | Operator-managed gallery-dl executable |
PAGE_METADATA_ENABLED |
true |
Enables generic page metadata extraction |
PAGE_METADATA_MAX_SIZE |
1mb |
Maximum HTML read by the metadata engine |
INSTAGRAM_PROXY_HOSTS |
www.kkkinstagram.com |
Ordered Instagram relay hosts; use none to disable |
INSTAGRAM_PROXY_FIRST |
false |
Try the configured relay before other engines for Instagram video/auto; the relay receives the URL |
REDDIT_PROXY_HOSTS |
redditez.com |
Ordered Reddit embed relay hosts; use none to disable |
DISABLED_ENGINES |
empty | Engine names removed from every plan |
| Variable | Default | Purpose |
|---|---|---|
COBALT_API_ENDPOINTS |
empty | Operator-authorized instances; Compose supplies two internal URLs |
COBALT_DIRECTORY_ENABLED |
false |
Opt in to tested, Turnstile-free entries from cobalt.directory |
COBALT_DIRECTORY_URL |
https://cobalt.directory/api/working?type=api&turnstile=0 |
Directory API used when discovery is enabled |
COBALT_ENDPOINT_TIMEOUT_MS |
12000 |
Cobalt endpoint request timeout |
COBALT_MAX_ENDPOINTS |
5 |
Maximum configured and discovered endpoints |
COBALT_FAILURE_COOLDOWN_MS |
60000 |
Local cooldown after an endpoint failure |
COBALT_API_KEY |
empty | Credential for operator-configured Cobalt endpoints |
COBALT_AUTH_SCHEME |
Api-Key |
Authorization scheme paired with COBALT_API_KEY |
TEMP_PREFIX |
mediafilez- |
Temp-directory prefix; it must be a plain name of at least 8 characters |
HTTP_USER_AGENT |
MediaFilez/2.1 (Discord media downloader) |
User-Agent for HTTP, Cobalt, and Discord upload requests |
DEBUG |
false |
Enables debug log lines |
Discord sends attachment_size_limit with each interaction. MediaFilez uses the smaller value between that limit, DISCORD_UPLOAD_TARGET_SIZE, and its 500 MiB hard ceiling. Discord documents this field as the effective per-attachment limit for the invoking user or guild: Discord interaction and upload reference.
When fit_to_limit is enabled, MediaFilez keeps media unchanged if it fits. Oversized video goes through remux, audio-only reduction, then H.264 fitting when needed. Oversized audio is re-encoded at a target-aware bitrate. Oversized images become JPEG and step down through bounded quality and resolution attempts. Files that cannot fit at usable settings return a measured size error.
The final multipart request streams from disk and remains under the whole-job abort signal. Upload retries are verification-first. If Discord closes a connection, the bot fetches the original reply and checks its attachment before another upload starts. An unknown delivery state never clears a file that Discord may have accepted.
src/
commands/ Discord command schema
jobs/ queue, limits, and job lifetime
download/ planning, validation, and engine ownership
engines/ one adapter per downloader or resolver
media/ image, audio, and video preparation
platform/discord/ reply and upload state machine
utils/ shared process, file, security, and format code
test/
unit/ deterministic behavior tests
smoke-*.js live local proofs
docs/architecture.md invariants and ownership boundaries
CONTRIBUTING.md code and test rules
Keep new engines beside existing engines. Do not move validation, fallback ownership, or reply state into adapters. See CONTRIBUTING.md before opening a change.
MediaFilez is licensed under AGPL-3.0-only. See LICENSE.
- Terms of Service for the official hosted bot
- Privacy Policy, including data use, recipients, retention, and deletion requests
- Security Policy for private vulnerability reporting
Self-hosters are independent operators and must publish policies that match their actual deployment.