PWA Wikimedia Commons Uploader
A small, fast, framework-free PWA that uploads photos and videos to Wikimedia Commons. Works offline, installs to the home screen (full screen), survives app/device restarts mid-upload, and runs fully in the browser for Commons-supported formats. Unsupported image and video formats use the companion Rust conversion service on Wikimedia Toolforge.
App: https://vitaly-zdanevich.github.io/PWAWikimediaCommonsUploader/
You can usually upload only media you created yourself (own work) under a free license. Photos of buildings, monuments and artworks may be restricted in some countries (no or limited Freedom of Panorama) — check before uploading.
Features
- OAuth 2.0 login (PKCE, no server), multiple accounts with switching
- Select many images/videos, or take a photo/video with the camera (iOS and Android)
- JPEG filenames use the shorter
.jpgextension in the UI and on Commons - Receive photos shared from other apps: on Android the installed PWA appears in the system share sheet; iOS never lets PWAs into its share sheet, so there use Photos → Copy → 📋 Paste (also Ctrl/Cmd+V on desktop)
- Per-file optional name, description, license and categories, with a full-width photo preview before the edit controls; global categories and file-name prefix
- Category autocompletion (your saved categories + Commons prefix search); a click on an added category chip opens it on Commons
- 📍 Nearby: finds the closest Commons categories via Wikidata (geolocation), with direction arrow and distance in meters, ordered by distance
- When photos are added, EXIF GPS from the first photo alone proposes the closest available categories within 25 km as one-tap chips; 📍 Nearby uses that same location when available
- ✨ Namify: renames all GPS-tagged files to
2026july_46_54_76_to_26_55_56_iphone7plus(date, coordinates and camera from EXIF), adds "Feel free to rename to something more descriptive." to the description, and files them into the hidden Category:Files with coordinate-based names uploaded by PWA so they stay findable for future renaming - Default license CC BY 4.0 (changeable); prefixes and categories are saved for reuse
- Generic file names that Commons rejects (
IMG_*,DSC*,PXL_*, UUID-style05998DD7-…,20230101_123456, digits-only, …) are highlighted in orange until they are renamed or given a prefix - Chunked, resumable uploads: continues after switching apps, going offline, or a device restart (queue and file bytes persist in IndexedDB)
- Clear red errors from Commons, including links when the file name is taken or an identical file (same SHA1) already exists — after a rename, retry republishes instantly without re-uploading
- Text-only list with ✅ when uploaded (thumbnails can be enabled in Preferences)
- Uploaded files stay editable: change description/categories/license and push with "Update on Commons" — refused if anyone else edited the page since, to never overwrite their work (renaming is excluded: it needs the filemover right)
- After uploading: copy the list of direct file URLs or Commons page URLs (one per line)
- HEIC/HEIF, AVIF, BMP, camera RAW and other unsupported images are converted to WebP; MP4/MOV and other unsupported videos are remuxed when their codecs are already compatible, or converted to WebM with AV1 video and Opus audio
- Dark mode with pure
#000background (prefers-color-scheme) - Every upload gets the hidden tracking category
Uploaded by PWA from Vitaly Zdanevichon the last line of the wikitext
Setup (one-time)
-
Register an OAuth 2.0 client: go to Special:OAuthConsumerRegistration/propose/oauth2 on Meta-Wiki and propose a consumer with:
-
OAuth protocol: OAuth 2.0
-
This consumer is for use only by : leave unchecked if other accounts should log in (note: until an OAuth admin approves the consumer, only the proposing account can use it)
-
Callback URL:
https://vitaly-zdanevich.github.io/PWAWikimediaCommonsUploader/(exactly, with the trailing slash) -
Applicable project: restricting the consumer to
commons.wikimedia.orgis fine and is the least-privilege choice — the app talks only to Commons (login still happens via Meta, which works regardless of this restriction) -
Client is confidential: leave unchecked. This PWA is a "web app without a server component": it runs entirely in your browser from GitHub Pages, so there is nowhere to store a client secret — any secret shipped in the JavaScript would be public anyway. Instead of a secret the app uses PKCE (
code_challengeat login,code_verifierat the token exchange). If you check it by mistake, the token endpoint will require aclient_secretthe app never sends, login will fail with an "invalid client" error, and the setting cannot be changed afterwards — you would have to register a new consumer. After submitting, only the client ID matters — Meta-Wiki shows it using OAuth 1.0a wording as the "consumer key" (or "client application key"). The "consumer secret" displayed next to it is the client secret; ignore it. -
Allowed OAuth2 grant types — check:
- Authorization code
- Refresh token
Authorization code is the login flow itself (with PKCE). Refresh token is needed because access tokens expire after about 4 hours and the app refreshes them silently — otherwise you would sign in again every 4 hours and interrupted uploads could not resume unattended. Client credentials stays unchecked: it is a machine-to-machine flow where the app itself authenticates with a client secret (confidential clients only); this app always acts as the signed-in user.
-
Applicable grants — check exactly these three, nothing more:
- Basic rights
- Create, edit, and move pages
- Upload new files
"Create, edit, and move pages" is required because publishing an upload creates the file description page (
createpage/edit). "Upload, replace, and move files" is not needed: the app never overwrites or moves existing files — a taken name or duplicate is reported as an error instead. -
-
Open the app → ⚙ Preferences → paste the client ID (or set
DEFAULT_OAUTH_CLIENT_IDinsrc/config.ts). -
Sign in.
Conversion service
The implemented companion service is a Rust/Axum binary in converter/. It streams
incoming files to bounded temporary storage, limits concurrent conversions, and uses
libvips with an ImageMagick fallback for still images. Its FFmpeg decision logic is
adapted from
bot_telegram_wikimedia_commons_uploader:
compatible streams are copied into a Commons container, and only incompatible streams
are transcoded. AV1 encoding tries SVT-AV1 first and falls back to libaom.
The PWA POSTs multipart/form-data to
https://pwa-commons-uploader-converter.toolforge.org/convert. Fields must be in this order so
the service can authenticate before accepting a large body; file is last:
| field | value |
|---|---|
token | the user's OAuth 2 access token (Bearer) |
filename | desired Commons file name (extension may change) |
text | ready wikitext for the file page |
comment | upload comment |
file | the original file (HEIC/MP4/MOV/…) |
The service validates the token against Commons, converts into a supported format,
uploads in 16 MiB chunks with the supplied wikitext unchanged, and immediately removes
its temporary files. It does not persist or log OAuth tokens. A successful response is
{"fileName":"Photo.webp","pageUrl":"...","fileUrl":"..."}; errors are
{"error":"message"}. The final filename matters because conversion replaces the
original extension with .webp, .webm, .ogv, or an accepted audio extension.
Configuration variables:
| variable | default | purpose |
|---|---|---|
PORT | 8000 | HTTP listen port (Toolforge sets this) |
MAX_UPLOAD_MB | 120 | maximum incoming file size (below Toolforge's 128 MiB proxy ceiling) |
MAX_IMAGE_PIXELS | 200000000 | decoded image safety limit |
CONVERSION_TIMEOUT_SECONDS | 3600 | timeout for each converter process |
CONVERSION_CONCURRENCY | 1 | simultaneous buffered conversions/uploads |
IMAGE_QUALITY | 92 | lossy WebP quality |
COMMONS_API_TIMEOUT_SECONDS | 300 | timeout for each Commons API request |
Development
npm install
npm run dev # local dev server
npm run lint # eslint
npm run typecheck # tsc --noEmit
npm test # vitest
npm run build # vite build + HTML minification + service worker generation
npm run icons # regenerate PNG/ICO icons from public/icons/icon.svg
cargo test --locked # Rust unit tests
cargo run --locked # converter at http://localhost:8000
The frontend has no runtime dependencies; TypeScript is built with Vite targeting
Safari 14+ (works on iOS 15). Running the converter locally also requires ffmpeg,
libvips-tools, imagemagick, libraw-bin, and HEIF support. Health check:
curl http://localhost:8000/healthz.
Toolforge deployment
Create or join the Toolforge tool account, then deploy the latest pushed main branch:
./scripts/toolforge-deploy.sh
The script uploads toolforge/service.template, starts a Toolforge Build Service build
from GitHub, starts or restarts the webservice, and waits for /healthz. project.toml
declares FFmpeg, libvips, ImageMagick, LibRaw and HEIF runtime support; Aptfile
provides the same packages for Toolforge's custom Rust buildpack group.
The service listens on 0.0.0.0:$PORT, as required by Toolforge.
Read recent logs or follow them from the local checkout:
./scripts/toolforge-logs.sh
./scripts/toolforge-logs.sh --since 30m --errors
./scripts/toolforge-logs.sh --follow
Set TOOLFORGE_LOGIN, TOOLFORGE_HOST, TOOLFORGE_TOOL, TOOLFORGE_SSH, or
TOOLFORGE_SSH_CONFIG when the defaults do not match the deployment.
Versioning and deployment
- Every commit bumps
versioninpackage.json: minor for a new feature, patch for a fix. - CI (GitHub Actions) runs lint, typecheck, tests and build on every push; it deploys to GitHub Pages only when the commit changed the version.
Adding it to Telegram
Yes — this PWA can be attached to an existing Telegram bot as a
Mini App: in @BotFather use
Bot Settings → Menu Button (or web_app inline buttons) and point it to the
GitHub Pages URL. Caveats: inside Telegram's webview the OAuth redirect works as a
normal navigation, but storage may be isolated from your regular browser, so you
sign in once inside Telegram too; iOS Telegram may not keep long uploads running in
the background as reliably as the installed PWA.
My other Wikimedia-related projects
GitHub
- wikimedia_commons_pwa_viewer — minimal PWA for browsing Wikimedia Commons images by feed, category, search, or location (open the app)
- bot_telegram_wikimedia_commons_uploader — Telegram bot that uploads images and media to Wikimedia Commons under each user's own account
- bot_telegram_wikimedia_commons — Telegram and CLI bot for searching Wikimedia Commons media
- bot_telegram_wikipedia — Telegram bot for Wikipedia search
- gthumb-copy-wikimedia-commons-link — gThumb extension that copies the Wikimedia Commons link for a local file
- wikipedia_diffs_to_evernote — daily synchronization of a Wikipedia user's edits to Evernote
- wikipedia-userstyle-dark-minimum — dark, minimal Wikipedia userstyle that does not require a browser extension
- youta — low-resource terminal audio player with Wikidata metadata integration
GitLab
- wiki2man_on_rust — converts official Wikipedia XML dumps into roff man pages for offline reading in a terminal
- gthumb-wikimedia-commons-extension — gThumb extension for viewing Wikimedia Commons images
- commons-fuse — read-only FUSE filesystem for Wikimedia Commons
- upload_to_commons_with_categories_from_iptc — Python script for uploading images from gThumb with IPTC categories
- pwb_wrapper_for_simpler_uploading_to_commons — stateless CLI wrapper around Pywikibot for single-file and batch uploads
- web-extension-uploading-to-wikimedia-commons — browser extension for uploading images to Wikimedia Commons
- commons-wikimedia-find-by-hash — CLI tool that finds a Wikimedia Commons file with the same SHA-1 as a local file
- webextension_find_by_hash — browser extension for finding Wikimedia Commons files by hash
- video-to-webm-av1-opus — file-manager script that converts video to Commons-compatible AV1/Opus WebM
Rate limits
Commons allows regular users 380 upload API requests per 72 minutes (each 16 MB
chunk and each publish counts as one). When throttled (HTTP 429), the app pauses the
queue and retries automatically; large batches may take a while. Users in the
autopatrolled group on Commons are effectively unlimited.
Notes on iOS
- Install via Safari → Share → Add to Home Screen for full-screen mode.
- iOS suspends web apps when the screen locks or you switch away — there is no background-upload API in iOS Safari. While uploading, the app keeps the screen awake: via the Wake Lock API where available (Android, iOS 16.4+), or on older iOS by playing a hidden video with a silent audio track (muted playback does not prevent sleep there — a side effect is that starting an upload may pause your background music). The lock button still suspends the app; reopen it and the upload continues automatically from the last uploaded chunk.
Category: https://commons.wikimedia.org/wiki/Category:Uploaded_by_PWA_from_Vitaly_Zdanevich
Wikidata: https://www.wikidata.org/wiki/Q140522959
See also https://commons.wikimedia.org/wiki/Commons:Upload_tools