All notable changes to Downl.one are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and since v26.10.22 this project uses [Date Versioning](https://datevers.ing) (YY.MM.DD, the UTC release date, with -1, -2… for later releases the same day); earlier releases used Semantic Versioning. This is Downl.one's third rebuild — see Earlier history at the bottom for the full story, and Before this changelog below for this codebase's own early development.
v26.10.25-2
- Video thumbnails now come through Downl.one (
api/thumb.php) instead of loading in the visitor's browser straight from YouTube, X and the rest, so those sites never see the visitor. Only images Downl.one signed, from public addresses, up to 5 MB, are passed on, and nothing is stored - Ordinary visitors get no cookies at all: a PHP session (and its cookie) is now only started for the admin preview it exists for
- The privacy and cookies policies now match what the site does: a new Cloudflare section (the site runs behind Cloudflare, the download form uses Turnstile, and YouTube and X requests leave through Cloudflare's WARP network), how links needing a signed-in viewer are fetched as Downl.one's own accounts, that thumbnails come through Downl.one, that error reports go through an email provider, that server logs normally show Cloudflare's address rather than the visitor's, and the strictly necessary admin-session and Cloudflare security cookies
- The download proxy, the merger and the new thumbnail proxy only fetch the public internet. A host must resolve to public addresses only (loopback, private, link-local, carrier-grade NAT, IPv6-local,
localhostand.internalnames are refused), each request is pinned to the address that was checked, only http(s) on the standard ports is allowed, and redirects are followed by hand with every hop checked again (includes/NetGuard.php) - Media, merge and thumbnail links are signed with a random secret made once per server (
storage/proxy.key), instead of a key derived from the server's name, with longer signatures, and each kind of link has its own - yt-dlp's generic extractor, which reads whatever page it's given, is switched off for lookups and playlists, except for the four sites that need it (Kwai, Streamff, fb.watch and v.redd.it links). A redirect from an allowed site, such as a t.co link, can no longer lead yt-dlp to an arbitrary page. A redd.it link's redirect is read rather than followed, and only a Reddit address is used
- The rate limits now go by the visitor's real address only when the request really comes from Cloudflare, and never by
X-Forwarded-For, which visitors can set themselves; the admin sign-in limit could be sidestepped that way
v26.10.25-1
- Error reports by email: when a lookup fails because of a fault on Downl.one's side, the real error is emailed to the alert address set on Admin → Config, with the link that failed, where in the code it happened, the version and the time, but nothing about the visitor (no IP address or browser details). The same fault is emailed at most once an hour, saying how many more times it happened in between, and no more than 10 error emails go out an hour (
includes/ErrorReport.php) - Plain-English messages for more problems that are about the link rather than Downl.one: private, deleted or unavailable videos, links that aren't to a video (profiles, channels, searches), videos blocked in the server's country, and live streams or premieres that haven't started
- Faults on Downl.one's side no longer show the raw technical error (such as
python3: can't open file '/usr/local/bin/yt-dlp': [Errno 13] Permission denied). The visitor now sees "Sorry, something went wrong on our side while fetching this link. We've been told about it and will look into it, so please try again a little later." Problems with the link itself still get their own explanation, and YouTube blocking the server as a bot is both explained to the visitor and reported - The privacy policy now says that a link which fails because of a fault on Downl.one's side is included in the error report to the team, and what else the report does and doesn't contain
- Portrait videos are labelled by their shorter side, as video resolutions normally are: a 1080×1920 X video is now 1080p rather than 1920p (and 720p, 480p, 320p rather than 1280p, 852p, 568p)
- On phones, the "Last used" badge squeezed the format it marked: it sat between the format's details and the Save button, on a row that stays on one line, so the size broke in two ("5.6 / MB") and the badge overlapped the MP4 tag. It now sits next to the resolution, and the size never breaks across lines
- X video titles ended with the post's own
https://t.co/…link, which cluttered them and could stretch a narrow screen. Those links are now left out of the title tests/test_php_pages.shused the same default port as Stuxs.Tools' copy (8098), so running both at once sent one project's requests to the other's server and failed. Downl.one's now uses 8097
v26.10.25
- Downloads failed with "python3: can't open file '/usr/local/bin/yt-dlp': Permission denied" after
bin/update-ytdlp.shinstalled a new yt-dlp. The script downloads to amktempfile (mode 600) and marked it executable withchmod +x, which gives 711: runnable, but readable only by root, and yt-dlp is a Python zip app that has to be read to run. Its own check of the new version runs as root, so it passed, and every earlier weekly run had found nothing new to install, so this only showed when yt-dlp 2026.10.21 came out. The new binary is now installed with mode 755 bin/update-ytdlp.shis now executable in the repo itself, as the cron job runs it directly; the server's checkout had been carrying that as a local change, which would have blocked a deploy that changed the script
v26.10.23-1
CONTRIBUTING.mdsaid the project has no automated test suite, right after the line describing it. It now says to run PHPUnit andtests/test_php_pages.shbefore committing, and to also try the download paths you touched on a local server, since the tests don't cover real downloads
v26.10.23
CONTRIBUTING.md's manual smoke-test advice now just says to check the pages you touched on a local PHP server
v26.10.22-8
- Discord component embeds, as on Stuxs.Tools: links to Downl.one pasted into Discord show a custom preview in Downl.one green instead of the standard card. The homepage shows what Downl.one does and every highlighted platform, with Download something, FAQ and Stux.Dev buttons; the old per-platform pages (
/tiktok,/twitterand so on) show "Download from …" with the platform's note; other pages show their name and description. It's built server-side (discord_*helpers inincludes/helpers.php, set per view as$discordEmbed), left out if it would ever pass Discord's 3,000-byte limit, and the Open Graph tags remain as the fallback. Page names and descriptions are escaped so Discord shows them as plain text
v26.10.22-7
- A full favicon set, built from the icon by
bin/make-icons.php:/favicon.ico(16, 32 and 48 px) at the root, 32 and 192 px PNGs, a 180 pxapple-touch-icon.pngon the site's dark background (iOS fills a transparent one with black), a maskable 512 px icon for Android home screens, and a web manifest (/site.webmanifest). Every page, the admin pages included, now carries the same icon tags (icon_tags()inincludes/helpers.php)
- The favicon was missing in places, and on mobile. Only a single 512 px PNG was declared, with no sizes; there was no
/favicon.ico, which browsers, bookmarks and search engines ask for whatever the page says (it was a 404); the admin pages had no icon at all; and there was no web manifest for Android to take its icons from
v26.10.22-6
- A second click while a sign-in session was starting (e.g. *Sign in* straight after *Add account*) restarted the session with a new password, so noVNC answered the link already opened with "password check failed". Asking for the session that's already open now leaves it alone (in the admin page and in the root request handler), and *Sign in* and *Add account* stay disabled until a starting session is ready
v26.10.22-5
- Removing a browser account while its sign-in session was open deleted the profile under Chrome's feet, and Chrome wrote it back as it closed, leaving an orphaned profile. A profile in use is now left until the session closes, and profiles no account owns are cleaned up by the daily check and the Automated accounts page
- The Automated accounts page could show the sign-in link a moment before the remote browser was reachable. The session is now announced only once noVNC is listening, and the VNC server's and noVNC's own messages go to the service's journal (
journalctl -u 'downlone-signin@*') instead of being discarded
v26.10.22-4
- Automated accounts for YouTube as well as X, on a new Admin → Automated accounts page that replaces Admin → X accounts (the old address redirects). YouTube accounts are a last resort, used only when YouTube still blocks an anonymous lookup as a bot or hides an age-restricted video; X accounts are used for posts X hides from logged-out visitors, as before
- Sign in once, in a browser on the server. Each account can be its own Chrome profile on the server, signed into through a remote browser session (a virtual display with noVNC, listening on localhost only and reached through an SSH tunnel, with a password made up for each session and a 30-minute limit). yt-dlp then reads the session straight from the profile, so there are no cookies to copy. The sign-in service is in
deploy/signin/with an installer; pasting cookies remains available as a fallback - The daily check (
bin/check-accounts.php, replacingbin/check-x-accounts.php, which still works) opens each browser account's profile so the site can refresh the login, reads whether it's still signed in and when the login expires, and tests every account. Alert emails cover YouTube accounts too, including a warning 14 days before a login expires OUTBOUND_PROXYsetting: YouTube and X lookups and YouTube media downloads can leave through a proxy, such as Cloudflare WARP in proxy mode, instead of the server's own datacenter IP, which YouTube increasingly blocks as a bot. Lookups and downloads always use the same exit, since YouTube's media links only work from the IP that asked for themapi/health.phpreports YouTube and X accounts separately (youtube_accounts,x_accounts) and whether the outbound proxy is on- INSTALL.md covers setting up the proxy, a JavaScript runtime and the bgutil PO token provider for yt-dlp, and the sign-in browser
- When YouTube blocks the server as a bot, the error now says so, instead of yt-dlp's raw message or "This video is age-restricted or private", which the native lookup used to show for YouTube's bot check
- Account ids made only of digits (about one in 15) became integer array keys, which broke comparisons with them; ids now always contain a letter
deploy/is blocked from the web likebin/andstorage/(in.htaccess, the dev server and the nginx example). The sign-in request handler that runs as root is installed as a root-owned copy rather than run from the site's directory, which the site's user can write to
v26.10.22-3
- Admin alert emails are now a branded HTML email (the Downl.one logo, an "Admin alert" label, the message with clickable links, and an *Open the admin panel* button), with the plain-text version still included for mail clients that don't show HTML
v26.10.22-2
- Production runs PHP 8.3, so that's now the baseline everywhere instead of 7.4: INSTALL.md, README.md, CONTRIBUTING.md, DEV_GUIDE.md,
composer.json(php >=8.3), the nginx example's PHP-FPM socket, andapi/health.php's PHP check (now 8.3+) - CI lints, runs PHPUnit and requests every page on PHP 8.3 (it ran the main jobs on 8.1 and a separate production job on 7.4)
dev-server.sh/dev-server.batusePHP_BINor thephpon your PATH, and only warn when it's older than 8.3, instead of looking for a PHP 7.4 install and warning about anything else
composer.lockhad been out of date since v3.12.0, whencomposer.json's PHP requirement changed without the lock being refreshed, socomposer installwarned about it on every CI rundev-server.batshowed a blank PHP version (and so always warned) whenPHP_BINwas a full path tophp.exe
includes/compat.php, the PHP 7.4 polyfills forstr_contains(),str_starts_with()andstr_ends_with(), which are built into PHP 8- CI's separate "PHP 7.4 (production)" job
v26.10.22-1
- Switching on Cloudflare Turnstile (now a tickbox on the admin Config page) would have left the download form and admin sign-in without a challenge, failing every check: the site's Content-Security-Policy blocked Cloudflare's Turnstile script and frame. It now allows
challenges.cloudflare.comfor both. If you run nginx, copy the updated header into yourserverblock too (INSTALL.md section 6)
v26.10.22
- X (Twitter) posts marked as sensitive or age-restricted can now be downloaded, the same way cobalt does it. X hides these from logged-out visitors: both its guest API and its embed API return an empty placeholder, although the video files themselves are public. When a post comes back hidden, Downl.one now asks again as a logged-in X account, then through X's embed API, which still serves some posts the guest API drops. Without an account, behaviour is unchanged apart from the extra embed API attempt
- Admin → X accounts page for those accounts: paste a logged-in account's cookies (never its password), and it's tested straight away against an age-restricted post. Shows each account's status (working, rate-limited, logged out / expired, can't see sensitive media), when it was last checked and used, and its expiry date; the cookies are stored in
storage/x-accounts.jsonand only a maskedauth_tokenis ever shown back - Every request saves the cookies X refreshes, so accounts stay logged in without being re-added. With several accounts, the least recently used working one is picked, a rate-limited one sits out for 15 minutes, and a logged-out one is skipped
- Alert emails to the admin (
ALERT_EMAIL) when an X account stops working, can't see sensitive media, or is 14 days from itsauth_tokenexpiry date, once per problem. They're sent through any SMTP server (or PHP'smail()) via a small built-in SMTP client, configured on the admin Config page (newALERT_EMAIL,MAIL_FROMandSMTP_*settings), with *Send a test email* buttons there and on the X accounts page bin/check-x-accounts.php, a daily cron job that tests every X account and checks expiry dates, so a dropped login is noticed even on days nobody downloads a hidden post (INSTALL.md section 7)- X account status on the admin dashboard and in
api/health.php(x_accounts, not fatal to the overall status) - The admin Config page now also edits the site URL, the alert email / SMTP settings and Cloudflare Turnstile (switch and keys). The SMTP password and Turnstile secret key are write-only: never shown back, and an empty box keeps the current one. Settings an older
config.phpdoesn't have yet are added to it on save. The yt-dlp and ffmpeg paths stay hand-edited only, since they're run as shell commands
- Downl.one now uses [Date Versioning](https://datevers.ing) instead of Semantic Versioning. Versions are the UTC release date as
YY.MM.DD, with-1,-2… for further releases the same day, so this release, which would have been v3.15.0, is v26.10.22 and follows v3.14.11.commit.sh/commit.batrefuse aVERSION.mdthat isn't a valid Date Version
- Settings entered on the admin Config page can't contain line breaks, which could otherwise have injected extra headers into alert emails
- The local dev server now refuses requests for
storage/,includes/andbin/, as.htaccessalready does in production, sincestorage/now holds X account cookies. It previously served them as plain files (e.g.storage/hits/hits.json)
v3.14.11
- The logos and icons in the Markdown docs (README and the like) follow GitHub's light or dark theme, using each brand's
logo-light/logo-darkandicon-light/icon-darkfiles
v3.14.10
- The logos and icons in the Markdown docs (README and the like) follow GitHub's light or dark theme, using each brand's
logo-light/logo-darkandicon-light/icon-darkfiles
v3.14.9
- X (Twitter) videos were all listed as "Audio only" even though they have sound. yt-dlp gives X's direct MP4s no video or audio codec fields at all, and a missing codec was read as "no video"; a missing codec now means "unknown", so a format with a resolution or a video container counts as video, and as video with sound unless it's explicitly marked as silent. X videos now show as Video & Audio at each resolution (1080p, 720p, 360p…) instead of as audio named
http-10368and the like - YouTube's 360p format with sound (when Downl.one reads YouTube directly) was listed as "Video only", because only an
audio/MIME type counted as having sound. A video MIME type that lists an audio codec too (avc1…, mp4a…) is now Video & Audio - X posts that X hides from logged-out visitors (marked as sensitive or age-restricted, from protected accounts, or deleted) failed with yt-dlp's raw "No video could be found in this tweet". X returns an empty placeholder for these instead of the post, so Downl.one can't fetch them without an X login; it now says so in plain English, and also covers posts that genuinely have no video
v3.14.8
- The footer's Created-with icons (heart, code, coffee) no longer carry a
vertical-alignnudge, on the main site and on the maintenance and coming-soon pages; they keep their optical sizes
v3.14.7
- The highlighted platforms are laid out for phones: the "35 highlighted platforms" badge and "Plus 1000+ more" line are centred under the heading, each platform shows its note again (cut to two lines, since there's no hover tooltip on a phone), icons sit in a fixed column so the names line up, and very narrow screens (≤340px) get a single column
v3.14.6
- The line between a result's two columns stopped part-way down (where the thumbnail and title ended); it now runs the card's full height
- The thumbnail no longer pulls away from the top of the card while the page scrolls past it
v3.14.5
- A note across the bottom of each result: "Need this in a different format? You can use our partner platform Stuxs.Tools to format it after download.", linking to [Stuxs.Tools](https://stuxs.tools)
- Each format's file type (MP4, WebM, M4A…) is shown as a badge next to its size instead of as ".mp4" text
v3.14.4
- The three download modes are called Video & Audio (the default), Video only and Audio only, matching the labels on the formats themselves (which now read "Video & Audio" instead of "Video + Audio")
v3.14.3
- The download mode has three options: Video (with audio) (the default), Video (no audio) and Audio only, each listing only that kind of format. Video (with audio) used to list video-only formats too
- On phones the three mode buttons are equal tiles with the icon above the label, so no label wraps unevenly
v3.14.2
- Results are laid out in two columns on desktop and tablet: the thumbnail, title and channel on the left, the formats on the right. On phones they stack in one column with the title and channel centred
- The whole thumbnail shows (16:9, portrait thumbnails fitted rather than cropped) instead of a short cropped strip
- On phones each format stays on one line (type and quality on the left, Save on the right) instead of putting Save on a line of its own
- The format type labels share one width, so the resolutions line up down the list
- The results (and error messages) sat right under the "By continuing, you agree to…" line; they now have room above them
v3.14.1
- The Video / Audio only switch and Playlist mode now sit under the URL bar in a collapsed Advanced options section, so the link gets the bar's full width. While it's collapsed, the label names any option that's switched on (e.g. "Advanced options · Audio only")
- Video mode lists only Video + Audio and Video only formats, and Audio only mode only audio formats (whichever there are, if a link has none of the chosen kind)
- The chosen mode is remembered in your browser
- Switching between Video and Audio only did nothing until the link was fetched again; it now re-filters the formats on screen straight away
- The "Last used" star lit up both the Video + Audio and the Video only row of the same resolution, and never appeared for formats whose size has a decimal point (e.g. "80.5 MB")
v3.14.0
- Video + Audio downloads at every YouTube resolution up to 4K. YouTube only serves sound and picture together up to 360p, so each resolution now pairs the best video with the best audio in the same container (MP4 + M4A, or WebM + Opus) and [
api/merge.php](api/merge.php) joins them with ffmpeg as they download: no re-encoding, nothing kept on the server. H.264 is preferred (plays everywhere), then AV1, then VP9. At mostMAX_CONCURRENTmerges run at once; past that, visitors are asked to try again in a minute FFMPEG_PATHinconfig.example.php(default/usr/bin/ffmpeg), and anffmpegentry in/api/health.php
- The built-in YouTube lookup uses YouTube's visionOS client, which returns every format up to 4K as direct links. If it ever stops working, links fall back to yt-dlp as before;
INSTALL.mdexplains how to update it from yt-dlp's client list
- The built-in YouTube lookup failed on every video (its Android client had been retired), so every YouTube link took the slower yt-dlp path
- YouTube downloads ran at roughly 0.7 MB/s, because YouTube throttles one long request. They're now fetched in 10 MB ranges, which YouTube serves at full speed (about 10 MB/s and up), and seeking/resuming still works
- With yt-dlp, YouTube's video-only formats were dropped whenever a combined format existed, leaving only 360p
v3.13.7
- Formats are listed best first (Video + Audio, then Video only, then Audio only, tallest or highest bitrate first within each), and the list keeps the top 12 video and top 5 audio formats instead of the first 15 of everything, so audio is never pushed off the end
- YouTube (and TikTok, SoundCloud, X, LinkedIn) show their proper names on the platform badge instead of "Youtube"
- YouTube results only offered the 15 lowest-quality video-only formats (144p–360p, no audio) when the fallback extractor was used
- Storyboard thumbnails no longer appear as "Audio only · 0kbps" formats, and HLS playlist entries (which downloaded a playlist file rather than the media) are no longer listed when direct files exist
- Quality labels line up on the left instead of drifting with the width of the size text
v3.13.6
- Fetching a URL no longer fails with "getFormatPref is not defined": the helpers that remember your last-used format (and mark it "Last used" on the next result) were called but never defined
v3.13.5
- The site's timezone is one line in
config.php(date_default_timezone_set('Europe/London'), UK time by default), in its own Timezone section at the top; change it there and every date and time the site shows follows
v3.13.4
- The default timezone in
config.example.phpis UK time (Europe/London) instead of UTC
- The admin pages labelled every time "UTC" whatever the configured timezone; they now show the real zone (e.g. BST or GMT)
v3.13.3
- The Content Security Policy's
img-srcallowsblob:images, so previews the browser builds from a file or download (URL.createObjectURL) are no longer blocked
v3.13.2
- Link preview images (
og.php) returned a 500 error on PHP 8:imagedashedline()was called with an extra argument that PHP 7 ignored
v3.13.1
- The copyright line reads Stux.Group instead of Stux Group Ltd
- The footer's Created-with icons are optically sized, so the heart no longer looks bigger than the code and coffee icons
v3.13.0
/sitemap(an HTML page in the site layout listing every public page) and/sitemap.xml(for crawlers), built from one page list inincludes/sitemap_pages.phpwith the fixed site URL and each page's source-file modification date aslastmod; the page test checks both- A Created with love / code / coffee by Stux.Dev line in the footer, on the coming-soon and maintenance pages too
- In dev mode,
?banner=soon,maintenance,sitepreviews the other site banners (production ignores it)
- The footer links to the Sitemap next to Boring Legal Stuff
- The dev-mode and admin-override banners are the shared Stux site banner (
assets/css/site-banner.css+assets/js/site-banner.js): a muted tint of each variant's accent with a faint icon pattern and a label chip. The header and mobile menu move down by the banners' measured height, so nothing is covered or cut off, even when a banner wraps on a phone - The footer's Stux.Dev logo, "A Stux.Dev Service" and the Powered by Stuxedo badge are grey and dimmed until hovered or focused, on every page including coming-soon and maintenance, and the hover animates smoothly
- The footer uses Stuxedo's text badge instead of the rectangle one, and the Stux.Dev logo and the badge are both 28px tall
- The copyright line uses Font Awesome's copyright icon instead of the © sign (screen readers still read "Copyright")
dev-server.sh/.batrun on PHP 7.4, like production (PHP_BIN, thenphp74/php7.4, then%LOCALAPPDATA%\Programs\PHP\7.4), and warn when only another version is available
- Turning on coming-soon or maintenance mode crashed every page with "Call to undefined function asset()": those pages were shown before
includes/helpers.phpwas loaded. It now loads first - The maintenance page's theme button showed its icon markup as text after it was clicked
v3.12.1
/api/health's PHP check reported PHP 7.4 as not OK (it required PHP 8); it now passes on 7.4 and newer, the versions Downl.one supports- The page smoke test required
/api/healthto answer 200, but it rightly answers 503 when yt-dlp isn't installed, as on CI runners, so the new PHP 7.4 CI job failed; it now accepts 200 or 503 there and still fails on any PHP error in the response
v3.12.0
- A PHP 7.4 (production) CI job: production runs PHP 7.4, so CI now lints every file, runs the PHPUnit suite (on PHPUnit 9.6) and requests every page on PHP 7.4, alongside the existing PHP 8.1 jobs
tests/test_php_pages.sh, the page smoke test that job runs: it serves the site with PHP's built-in server (usingconfig.example.phpwhen there's noconfig.php, with DEV_MODE on and coming-soon/maintenance mode off) and fails on an unexpected status or any PHP error, warning, notice or deprecation on every page, including/legaland its six sub-pagesincludes/compat.php, which providesstr_contains(),str_starts_with()andstr_ends_with()on PHP 7.4 (skipped on PHP 8, where they're built in)
composer.json, the README, CONTRIBUTING, INSTALL and DEV_GUIDE now say PHP 7.4+ instead of 8.1+; the INSTALL nginx example uses the PHP 7.4 FPM socket- Tests that run in a separate process carry a
@runInSeparateProcessannotation next to the attribute, so PHPUnit 9.6 (PHP 7.4) honours it too; the test bootstrap only loadsvendor/when PHPUnit isn't already loaded
- The site didn't run on PHP 7.4: 14 calls to PHP 8's
str_contains/str_starts_with/str_ends_with(in the downloader, the proxy, the admin rate-limit page and the router) had no fallback, and the YouTube thumbnail lookup passed an expression toend(), which PHP 7.4 rejects outright
v3.11.9
HelpersTestcoverage for the changelog section ordering
- The
/changelogpage (render_changelog()inincludes/helpers.php) now sorts each release's###sections into a fixed order — Added, Changed, Fixed, Removed, Security, Deprecated — at render time, rather than trusting the orderCHANGELOG.mdlists them in; unknown section types go last - Changelog type badges now use the fixed family palette — Added
#2ecc71, Changed#3ba7ff, Fixed#ffa64d, Removed#ff4d4d, Security#b06bff, Deprecated#8a8a94— as tinted badges (coloured text on a light tint of the same hue), with darker variants of each for the light theme - CHANGELOG sections reordered to Added, Changed, Fixed, Removed, Security, Deprecated
v3.11.8
- Footer/coming-soon credit changed from "A Stux.Dev Project" to "A Stux.Dev Service" (linking to
services.stux.devinstead ofprojects.stux.dev) — Downl.one isn't a StuxieDev, Ridgwell Family, or Stux.Dev Labs project, so it gets the standard service credit like the rest of the family.
v3.11.7
README.md's tagline was wrapped inbold— changed to the### *italic*heading style used for taglines elsewhere in the family.
v3.11.6
- The coming-soon and maintenance splash pages' icon used
fa-user-shieldfor both — changed tofa-rocket(coming soon) andfa-screwdriver-wrench(maintenance) so each page's icon matches its meaning.
v3.11.5
- The remaining Stux.Group mentions missed by v3.11.4:
LICENSE.md's copyright holder now names the actual legal entity,Stux Group Ltd;CONTRIBUTING.md's "is a Stux.Group project" line and every admin-panel footer (admin/*.php) now credit Stux.Dev, linking tohttps://stux.dev.
v3.11.4
- The Imprint page's Operator section incorrectly said Downl.one is "built and maintained by Stux.Group" directly — it's operated by Stux.Dev, which is in turn operated by Stux Group Ltd (company no. 13160574). Corrected the Operator section to name Stux.Dev.
- The legal contact address across
views/legal-imprint.php,legal-cookies.php,legal-opt-out.php, andlegal.phpwas[email protected]— unified to[email protected]to match the other Stux.Dev-operated products.[email protected](general contact) is unchanged. - The "Powered by" footer badge (
index.php) and the About/coming-soon pages named and linked Stux.Group directly — now read "A Stux.Dev Project" (linking tohttps://projects.stux.dev), with the brand logo itself linking tohttps://stux.devand pointing at the newhttps://global.media.stux.dev/logo.png. The About page's creator card was updated the same way (name, avatar icon, handle link, bio text), and its "Powered by Stuxedo" mentions now link as one phrase instead of just the word "Stuxedo". - The coming-soon page's brand credit was text-only, unlike Stuxs.Tools' equivalent — added the same small Stux.Dev logo image and the Stuxedo rectangle badge image alongside the text.
v3.11.3
README.md,index.php's footer, andviews/about.php's creator card Stux.Group logo/icon URLs had a leftover duplicated/global/path segment (global.media.stux.group/global/logo.pngand/icon.png) — corrected tohttps://global.media.stux.group/logo.pngand/icon.png
v3.11.2
HelpersTest::testRenderChangelogH3BecomesH4still asserted the pre-v3.11.0 plain<h4>output for### Added/### Changed/etc. headings, so CI has been failing on every push since the v3.11.0 pill-badge change torender_changelog()landed — updated the test to assert thechangelog-label/changelog-label-suffixbadge markup instead, and split it into separate cases for the badge, the suffix, and non-category headings (which still become<h4>)
v3.11.1
- The self-hosted Gontserrat/Creato Display/JetBrains Mono
@font-faceurl()s inassets/css/app.csswere the one remaining static asset without any cache-busting query — everything else on the site (CSS/JS entry points, logos, icons) already goes throughasset()'s?v=<version>, but a static.cssfile can't have a PHP-computed version injected into it. Added a manual?v=1to each fonturl()instead, to be bumped only when a font file itself is replaced.
v3.11.0
- The changelog page's
### Added/### Changed/### Fixed/### Removed/### Deprecated/### Securitysection headings (parsed byrender_changelog()inincludes/helpers.php) now render as colored pill badges — green/blue/amber/red/red/pink respectively — instead of plain uppercase gray text, matching the badge treatment on TIGHC's changelog page. A trailing parenthetical on a heading (e.g.### Added (Turnstile)) still renders, shown as muted text next to the badge. New.changelog-label/.changelog-label-*rules inassets/css/app.css.
v3.10.6
asset()helper inincludes/helpers.php, modeled on the one already used by Stuxs.Tools — returns/assets/<path>?v=<version>
- Every hardcoded
/assets/...reference (favicons, FontAwesome, the self-hosted Gontserrat/Creato Display@font-facefonts,logo.png,icon.png, the Stuxs.Tools cross-promo icons,app.css,app.js) now goes through the newasset()helper instead of a bare path, so every static asset gets?v=<version>and browser/CDN caches invalidate on every release instead of serving a stale file indefinitely — brings this project in line with Stuxs.Tools' existing convention
v3.10.5
- Footer's "Built & Maintained by Stux.Dev" now links to
https://github.com/StuxDevinstead ofhttps://stux.dev, matching the GitHub-org link convention used for the other brand footers (StuxAPIs, Stux.Cloud, Stuxedo)
v3.10.4
- Footer's version number is now a link to
/changelog, replacing the separate "Changelog" link that used to sit in the footer's link list
v3.10.3
- "Built and maintained by Stux.Group" (intro sentence and Credits section) corrected to Stux.Dev — Downl.one is made by Stux.Dev, which is part of the Stux.Group Brand of Companies, not built directly by Stux.Group itself; the closing brand-of-companies disclaimer now reflects this too
v3.10.2
- A "Downl.one is part of the Stux.Group Brand of Companies" line to
README.md's License section, matching the disclaimer already rolled out across the StuxAPIs repos
v3.10.1
- Changelog intro didn't state the Keep a Changelog format or that versions follow Semantic Versioning, despite
VERSION.md/commit.sh/commit.batalready enforcing MAJOR.MINOR.PATCH and the entries below already following the Keep a Changelog Added/Changed/Fixed structure — added both lines
v3.10.0
DEV_GUIDE.md— a dedicated local-development guide (cloning,config.php, the dev server and its flags, yt-dlp — it's optional, the dev server handles it — and verifying the install viaapi/health.php), split out ofCONTRIBUTING.md's old "Local setup" section- A "Fixing a bare PHP install" section in
DEV_GUIDE.mdfor a manualwinget/scoop-style PHP install that ships with nophp.iniat all (no extensions, no CA bundle for cURL's SSL verification) README.mdnow has a dedicated Contact section ([email protected]/[email protected]); the same addresses were added toCONTRIBUTING.mdand as a "still stuck?" line at the end ofINSTALL.md/DEV_GUIDE.md's troubleshooting tables — the site itself already used these addresses everywhere, the docs just didn't mention them
CONTRIBUTING.md's "Local setup" section is now a short pointer toDEV_GUIDE.md/INSTALL.mdinstead of duplicating setup steps inline (and no longer implies yt-dlp needs installing manually for local dev)README.md/INSTALL.mdnow cross-link toDEV_GUIDE.md
api/proxy.phpcalledurldecode($_GET['url']), but PHP already URL-decodes$_GETonce — this double-decode corrupted both the HMAC signature check and the actual fetch URL whenever the underlying media URL contained a percent-encoded character in its own query string, which is true for effectively every real CDN URL (confirmed via git history: present since v2.7.0, so likely broken in production too, not just locally).gitignorewas missing/storage/hits/(present on Stuxs.Tools' equivalent file but never added here), so the runtime-generated hit-counter file showed up as an untracked change after any local sessionINSTALL.md's version header had been stuck atv3.5.2for the last 8 releases (v3.6.0–v3.8.4) — it's meant to be bumped on every release (and was, consistently, up through v3.5.2) but nothing enforces that; bumped to current
v3.9.0
- Cross-branded Stuxs.Tools assets and colors updated to match its new logo/icon/accent: replaced the stale
stuxs-tools-icon.svgwithstuxs-tools-icon.png, addedstuxs-tools-logo.png, and updated every hardcoded#7C6EFFreference to the new#6B00A2acrossapp.cssandabout.php - About page's "sister project — Stuxs.Tools" card now shows the full Stuxs.Tools logo image, matching the simpler logo-only treatment Stuxs.Tools already uses for its own Downl.one card, instead of a separate icon + name + URL header block
- Cleaned up mojibake-prone em dashes in
commit.sh/commit.bat/bin/update-ytdlp.shcomments and echo/log text, matching the same cleanup applied to Stuxs.Tools' scripts
v3.8.4
- The dev-mode/admin-override banner icon now sits inline with the title text ("Development Mode", "Coming Soon Mode", "Maintenance Mode") instead of floating beside the whole two-line block
- The coming-soon and maintenance pages now use the same icon as the admin-override banner instead of a generic download icon
v3.8.3
- Updated the site logo (
assets/img/logo.png) to the new design commit.sh/commit.batno longer add a commit-authorship trailer
- With
DEV_MODEon, the homepage showed two PHP warnings ("Constant YTDLP_PATH already defined" and a resulting "session_start(): Session cannot be started after headers have already been sent") —config.php'sDEV_MODEerror-display toggle ran before the laterYTDLP_PATHdefine, which dev-server.sh's router also pre-defines; moved the toggle to the end of the file so it can't unmask that harmless collision again
v3.8.2
commit.sh/commit.bathad a hardcoded version and message left over from the v3.5.2 release, so they'd tag any future commit as "v3.5.2" if run — now read the version fromVERSION.mddynamically, so they can't go stale like this again. Also backfilled the missingv3.6.0–v3.8.1git tags this staleness had caused to be skipped.
v3.8.1
dev-server.sh/.batweren't actually forcingDEV_MODEon, so the dev banner and local Stuxs.Tools links required manually flippingDEV_MODEinconfig.phpevery time — they now force it on by default for every run, with--no-dev-modeto opt out and test production behavior locally
v3.8.0
- Dedicated
/faqpage — 10 common questions in an accordion, linked from the header, footer and mobile nav /platformsredirects to the platform grid on the homepage- Legacy per-platform pages from the old site (e.g.
/tiktok,/snapchat) now show a "this has moved to the homepage" page instead of a bare 404 — covers renamed platforms too (/twitter→ X) - "Boring Legal Stuff" added back to the mobile nav (it had been dropped entirely)
- The "Why Downl.one" feature card icons (VPS-powered speed, Zero data stored, Video & audio, platform count) had a full icon-color palette defined in CSS that was never applied to the markup, so they rendered in flat default text color — now a consistent green accent
- Footer's Stux.Group logo and Stuxedo badge were noticeably small (14px/20px) — bumped to 18px/26px
v3.7.0
- Development mode (
DEV_MODEin config.php) now shows a small banner on every page and points Stuxs.Tools links at your local dev server — port set viaSTUXSTOOLS_DEV_PORT, overridable per-run withdev-server.sh/.bat --sister-port <port> ?admin_token=<token>now bypasses coming-soon/maintenance mode for the rest of the browser session, with a banner marking the override while it's active;?admin_preview=offclears itADMIN_TOKENis now a 128-character token that regenerates itself automatically everyADMIN_TOKEN_ROTATE_HOURS(3h floor enforced regardless of what it's set to) — rewritten straight into config.php so the current token is always readable there. NewADMIN_TOKEN_DISABLEDoption fully blocks token-based bypass even if a token leaks
- A full admin panel: dashboard (environment status, admin-token rotate/disable, analytics snapshot), analytics (platform/type/preference breakdowns, request→download conversion rate), config editor, cache clear, a rate-limit viewer, and a yt-dlp health check
- Anonymous, aggregate-only download analytics (
storage/hits/hits.json, fed byapi/info.phpandapi/proxy.php) — no IPs or per-visitor data ever recorded - Optional Cloudflare Turnstile challenge on the admin login form (
TURNSTILE_ENABLEDin config.php) — off by default, fails closed on any verification error - Every admin page now shows a footer with the current version, copyright range, and a Stux.Group credit, plus Changelog/About links
- The homepage download form can also require a Cloudflare Turnstile challenge (same
TURNSTILE_ENABLEDswitch) — the Download button stays disabled until the widget completes, andapi/info.phprejects the request server-side if the token is missing or invalid
- The dev-server router pre-defined its CLI-flag overrides before checking whether the request was for a real file, so any direct request to
admin/*.phporapi/*.phpcollided with config.php's owndefine()calls ("constant already defined" warnings) — the overrides now only run when actually falling through toindex.php
v3.6.0
- A consent line above the homepage paste box: "By continuing, you agree to our Terms and Ethics of Use"
- Dedicated
/changelog,/donateand/settingspages — the changelog is no longer folded into the About page, and Settings holds real quality/codec/bitrate download preferences - Header, footer and mobile nav now link to Changelog, Donate and Settings; the standalone "Boring Legal Stuff" nav link was replaced by these
- Settings page preferences (max quality, YouTube codec, max audio bitrate) are now real — saved to
localStorageand sent with every download request, which filters the returned stream list server-side. A preference is skipped rather than applied if it would leave no streams at all - Selecting "Audio only" now actually filters results down to audio streams
- Consolidated the two separate, independently-wired mobile navigation panels into one — removes a duplicate hamburger button and ~90 lines of dead drawer CSS
- The paste-a-link box now stacks vertically on mobile (input, then Video/Audio toggle, then a full-width Download button) instead of cramming everything onto one row
- Theme toggle button widened from 34px to 38px
- Mobile nav icons for Donate and Settings given their own colors (rose/indigo), matching the existing blue/teal/amber set
- A full-width divider now separates "More free tools at Stuxs.Tools" from the rest of the mobile nav, matching the header divider
.input-mode-toggleno longer stretches full-width or carries an unneeded bottom margin on mobile — it stays sized to its content
- Mobile nav's "About" link read "About Downl.one" instead of just "About"
setMode()and theplaylistModevariable were referenced by the download-mode and playlist-mode UI but never defined, so switching to Audio-only or toggling playlist mode silently did nothing- Between 641–768px, neither the desktop nav links nor the mobile hamburger button was visible — the two breakpoints that hide/show them didn't match
- The Download button collapsed into an isolated circular icon on mobile once the paste box was stacked; it's now a full-width labeled button
- The paste box's mobile padding was asymmetric, left over from a duplicate rule at a different breakpoint
v3.5.2
- A little something for the curious. You'll know it when you find it.
v3.5.1
- More spacing below the homepage's Stuxs.Tools partner card before "Supported Platforms" (32px → 80px)
- The footer's "More tools at Stuxs.Tools" link wasn't rendering in Stuxs.Tools' purple —
.footer-nav a's color rule has higher CSS specificity than the plain.footer-stuxsclass, so it silently won regardless of what was set on.footer-stuxs. Rescoped to.footer-nav .footer-stuxsso the whole link (icon, lead-in text, and bold name) renders in purple with a purple-tinted background/border
v3.5.0
- Header rebuilt to match Stuxs.Tools' layout: centered container, logo left, icon+text nav center (Platforms, About, and a new Boring Legal Stuff link, each with a distinct icon color), theme toggle right — the standalone "More tools at Stuxs.Tools" header button is gone (footer and mobile nav links are unaffected)
- A purple "Partner platform" card promoting Stuxs.Tools on the homepage, mirroring the existing Downl.one card on Stuxs.Tools' site
- The hero heading's accent text ("Fast, free, private.") is now an animated gradient, with a plain-color fallback via
@supportsandanimation: noneunderprefers-reduced-motion - Legal hub page ("Boring Legal Stuff"): its 6 sections now show in a responsive grid (3 columns on desktop, 2 on tablet, 1 on mobile) with each icon colored distinctly, and the icon sits to the right of the text — built on new standalone
.legal-grid/.legal-cardclasses, fully independent of the shared.features-grid/.feature-cardsystem
- The download button's icon is now Downl.one's own icon rendered in white, instead of a generic FontAwesome download glyph
- Added spacing above the playlist mode toggle — it was sitting flush against the input row
- The URL input box showed a "double border" on focus — a box-shadow ring stacked on top of the border-color change, and a separate native
:focus-visibleoutline landing on the<input>itself. Both removed; only a clean border-color change remains
v3.4.1
- About page: the Stuxs.Tools card's icon now sits above its name/URL/description again instead of beside it
- The download button's loading spinner (
#btnSpin) was showing permanently regardless of loading state — its.spinclass setdisplay: inline-blockunconditionally, which overrode the browser's default[hidden] { display: none }rule for the same element. Scoped the rule to.spin:not([hidden])so it only displays while actually loading
v3.4.0
- Self-hosted
Creato Display(headings/logo) andGontserrat(body) fonts — the Google Fonts CDN<link>is gone entirely;JetBrains Monois now self-hosted too - Every platform icon is now colored with that service's real brand color instead of a single flat icon color
- Easter egg: holding Shift swaps the X icon and label for the old Twitter bird and label, reverting when Shift is released
- Kickstarter added as a supported platform (
kickstarter.com,ksr.com), bringing the highlighted platform count to 35 - About page: the Stux.Group and Stuxedo cards are now themed with each company's real brand color (sampled directly from their logos), and links inside those cards — plus the Stuxs.Tools card — now inherit the matching brand color instead of the site's default link color
- About page: the Stuxs.Tools card now shows an icon, name, and URL, matching the Stuxedo card's layout, instead of a bare description paragraph
- The Stuxs.Tools button in the header, footer, and mobile nav now uses Stuxs.Tools' own purple brand color instead of Downl.one's green
- Footer: the About link moved next to "Boring Legal Stuff" instead of sitting next to the Stuxs.Tools button
- Dailymotion, OK.ru, and Odysee now use their real Font Awesome brand icons instead of generic fallback icons
- Headings that directly follow an about-page info card now sit close to it instead of inheriting the same 2rem top margin used for regular prose headings
coming-soon.php's "N+ more platforms" copy is now computed from the actual platform count instead of a hardcoded number- Font Awesome upgraded from 7.2.0 to 7.3.1 (self-hosted)
- The link-box placeholder text on the homepage no longer overflows its box
v3.3.0
dev-server.sh/dev-server.bat— local dev server scripts that start PHP's built-in server with a router shim (so static assets actually load), auto-bootstrapconfig.phpfromconfig.example.phpon first run, and support--no-coming-soon/--no-maintenanceflags to bypass those modes for a single run without touchingconfig.php. They also detect whether a working yt-dlp is onPATHand, if not, download a copy into.dev-ytdlp/(gitignored) and use it for that run — regardless ofYTDLP_PATHinconfig.phpnull_device()helper inincludes/helpers.php— picksNULor/dev/nullbased on the OS
- Every hardcoded
https://stuxs.toolsliteral (8 occurrences acrossindex.phpand four view files) now uses the existingSTUXSTOOLS_URLconstant instead — the constant was already defined but wasn't actually being used anywhere
api/health.phpandapi/info.phpboth shelled out to yt-dlp with2>/dev/null, which silently breaks under Windows'cmd.exe(no/dev/null) — this was causinghealth.phpto report yt-dlp's version as empty even when it was working correctly. Both now usenull_device()
v3.2.3
- The about page's "The name" section now shows the real Downl.one icon instead of a generic FontAwesome download icon
- The "sister project — Stuxs.Tools" card now shows the real Stuxs.Tools icon (was missing entirely)
- The about page's "Who built it" section no longer credits the personal "Stux / StuxieDev" handle — it now credits Stux.Group throughout (name, bio, and creator-card icon), consistent with how the rest of the site attributes the project
- The about page header corrected from "A project by Stuxedo" to "A Stux.Group project · Powered by Stuxedo" — Stuxedo hosts the project, it doesn't own it
- The
coming-soon.phpcredit line changed from "Built by Stux.Group · Hosted by Stuxedo" to "A Stux.Group Project · Powered by Stuxedo", matching the footer's established phrasing
v3.2.2
CONTRIBUTING.md— local setup, project conventions, and how to add platform support, linked from README.md
- Brand name stylization made consistent throughout: "Downl.one" and "Stuxs.Tools" everywhere either is referred to in prose, page titles, meta tags, alt text, and OG image text (URLs, email addresses, User-Agent strings, and CSS/JS identifiers are unaffected — those stay lowercase, since they're literal technical values, not stylized references)
DOWNLONE_NAMEnow defined as'Downl.one'(was'downl.one')
v3.2.1
- The version number itself now lives in
VERSION.md(a bare version string, safe to commit) —version.phpis now just a thin reader (trim(file_get_contents(...))) instead of hardcoding the version config.phpno longer pulls inversion.php— each real entry point (index.php,api/health.php) requires it directly instead, keeping deployment config and release version decoupled
v3.2.0
- Changelog now rendered on the about page via a small dependency-free Markdown renderer
config.example.phpas the committed config template —config.phpitself is now gitignored, never committedversion.phpsplit out ofconfig.phpto hold just the version constant (safe to commit;config.phprequires it)
- All
DOWNL_constants renamed toDOWNLONE_throughout, and theSTUXS_URLcross-reference constant renamed toSTUXSTOOLS_URL - "Hosted & provided by Stuxedo" card now shows Stuxedo's real icon instead of a generic server icon; "Stuxedo by Stux.Cloud" corrected to just "Stuxedo" throughout about.php
og.phpnow composites the reallogo.pngwordmark instead of drawing "DOWNL.ONE" as text- INSTALL.md updated for the new
config.example.php/version.phpsetup step
- The mobile menu was completely non-functional at every screen width — the same missing
@media (min-width: 769px)wrapper bug as Stuxs.Tools, leaving.mobile-drawerand.mobile-overlaypermanentlydisplay: none !important
v3.1.0
- "Boring Legal Stuff" hub at
/legal, linking to six documents: Privacy Policy, Terms and Ethics, Cookies Policy, Imprint, Disclaimer, and Opt-Out Preferences (previously just Privacy and Terms, now moved under/legal/)
- Credited to Stux.Group throughout (previously StuxieDev/Stux); copyright range corrected to 2021–present
- Footer restructured: version number is now followed by a "Boring Legal Stuff" link, and the Stux.Group / Stuxedo attribution reads "A Stux.Group Project" alongside the Powered by Stuxedo badge
- Header, coming-soon, and maintenance pages now use the real logo image instead of constructing a logo from an icon plus coloured text
- Stuxs.Tools now shown with its real icon everywhere it's referenced (header, footer, mobile nav) instead of a generic bolt icon
- The mobile menu's logo was missing a wrapping element the other two logo instances had, causing a stray gap in "DOWNL.ONE"
v3.0.0
- README.md and a new INSTALL.md covering VPS deployment, yt-dlp installation, web server config, and a going-live checklist
bin/update-ytdlp.sh— downloads and verifies the latest yt-dlp release before replacing the pinned binary, intended for a weekly cron; INSTALL.md documents pinning a specific version instead if you'd rather not auto-updateapi/cache-prune.phpcron script for rate-limit file cleanup- Documentation on the YouTube InnerTube client key/version and why it occasionally needs manual rotation
- Every 404 response was returning HTTP 200 instead of 404
- The native YouTube path didn't fall back to yt-dlp when the InnerTube API returned a bad response (the exact failure mode a rotated InnerTube key causes) — it would silently "succeed" with nothing playable instead
- A PHP
Error(not just anException) in the native YouTube/Reddit paths would crash the request instead of falling through to yt-dlp - The stream proxy always reported HTTP 206 whenever a Range request came in, regardless of whether the upstream host actually honoured it — could mismatch and break video seeking
Before this changelog
Development before v3.0.0 wasn't tracked with version numbers in this document. For reference, roughly in order: the initial build (35+ highlighted platforms, feature parity with the well-known "cobalt" downloader, HMAC-signed stream proxy so nothing is written to disk); an audio-only download mode with a live progress bar and a /api/health endpoint; security hardening alongside sitemap.xml/robots.txt; playlist support via yt-dlp's flat-playlist mode with per-item format memory; the official icon and logo; and long-cache, immutable headers for static assets.
Earlier history
Downl.one has existed in three very different forms:
- v1.0.0 — the original Downl.one: a basic standalone download script, not a website
- v2.0.0 — a separate, now-retired WordPress site (frontend theme + backend)
- v3.0.0 onwards — this codebase: a standalone, dependency-free PHP application (no CMS, no Composer, own router), unrelated to either of the above