Back to Blog

Building FetchIt: A minimal and fast mobile media downloader

Emmanouil Athanasopoulos6 min read1,214 words
React NativeExpoMobileDownloader

The Motivation Behind the Project

FetchIt was built to make downloading media on mobile devices as straightforward as possible. The main goal was to remove the usual friction found in downloader apps — intrusive ads, complex UI, and slow background tasks — and replace it with a clean, focused experience. I had been using a handful of downloader apps on my phone for years, and every single one shared the same problems: they were either slow, covered in ads, or required you to navigate through a maze of menus to find the download button. I wanted to build something that just worked the moment you pasted a link. The idea behind FetchIt was simple — open the app, paste the URL, tap download. No login, no account creation, no premium tier needed to access basic features. The challenge was that building something this simple on the surface required a surprisingly complex technical foundation underneath.

Core Features and Design Goals

  • Paste & Fetch: auto-detects sources and fetches metadata instantly from any URL. The app monitors the clipboard on focus and pre-fills the input field, so the user can go from copying a link to seeing video metadata in under a second.
  • Format Negotiation: users can pick video quality presets (up to 1080p) or extract audio (MP3/M4A/FLAC). The format picker adapts to what the source actually offers — if 4K is not available, it does not show up in the list.
  • Playlist Support: detect and download entire playlists with select/deselect controls. Users can preview titles before downloading, skip items they do not want, and track progress for each item independently.
  • On-device processing: uses embedded Python and FFmpeg to process downloads locally without relying on external APIs. This is the most important architectural decision in the app — it means the app does not depend on any third-party server to function.

Deep Dive: How It Works Under the Hood

The biggest technical challenge in FetchIt was embedding a full Python runtime and FFmpeg binary on an Android device. Most downloader apps solve this by routing traffic through a proxy server, which means the server does the heavy lifting and the app just receives the final file. I did not want to take that approach because it introduces a single point of failure — if the server goes down, every user is affected. Instead, FetchIt uses the youtubedl-android library, which packages yt-dlp (a Python script) and FFmpeg into native Android binaries that run directly on the device. The Kotlin native module I built acts as a bridge: it receives a URL from the JavaScript layer, passes it to yt-dlp for metadata extraction, then hands off the download task to FFmpeg for format conversion. All of this happens in a background thread so the UI stays responsive. One of the trickiest parts was implementing real-time progress tracking. The yt-dlp process outputs progress data to stdout, so I had to capture that stream in Kotlin, parse the percentage, speed, and ETA values using regex, and emit them as events back to the React Native layer. On the JS side, Reanimated picks up those events and drives smooth progress bar animations at 60fps.

Technical Implementation

  • Built a custom Expo Native Module in Kotlin that acts as a bridge between React Native and the `youtubedl-android` library. The module exposes methods for URL metadata extraction, format listing, download initiation, and cancellation — all running in coroutine-backed background threads to prevent UI blocking.
  • Embedded yt-dlp, Python, and FFmpeg directly onto the Android device using `youtubedl-android`, allowing the app to fetch and process video natively, avoiding the need for an external proxy server. The total binary footprint is approximately 30MB, which is preloaded on first launch and cached in the app's internal storage.
  • Implemented a progress tracking bridge that emits real-time events from Kotlin to JS for animated download progress, speed, and ETA rendering via Reanimated. The bridge uses NativeEventEmitter with throttled updates (every 200ms) to avoid flooding the JS thread with events.
  • Built an internal updater mechanism to download the latest yt-dlp Python script directly within the app, ensuring compatibility as site extractors change over time. This was critical because video hosting sites frequently change their page structure, and yt-dlp releases patches within hours.

Challenges I Faced Along the Way

The hardest part of this project was not the downloading itself — it was error handling. When you are dealing with thousands of potential video sources, each with different page structures, authentication flows, and rate limiting policies, you need robust fallback behavior. I spent significant time building a retry system that detects transient failures (network timeouts, rate limits) versus permanent ones (geo-blocked content, removed videos) and gives the user appropriate feedback. Another challenge was managing the binary size. Embedding Python and FFmpeg adds about 30MB to the APK. I experimented with on-demand downloading of these binaries to reduce the initial install size, but the user experience suffered — people expected the app to work immediately after installation. In the end, I kept the binaries bundled and focused on optimising the rest of the APK to compensate.

Technology Choices and Alternatives

I evaluated three approaches before settling on the embedded yt-dlp architecture. The first was a server-side proxy model, which is what most competing apps use. It is simpler to implement but creates an operational burden — you need to pay for and maintain servers that handle potentially gigabytes of video traffic. The second approach was using platform-native download APIs, but these are limited to direct file URLs and cannot handle the metadata extraction and format negotiation that yt-dlp provides. The third approach — the one I chose — was embedding the entire processing pipeline on-device. It has a higher upfront complexity cost, but it means the app works offline-first, does not depend on my infrastructure, and gives users full privacy since no data passes through my servers.

The Technology Stack

Built with Expo SDK 56, React Native, and TypeScript. Relies heavily on the Expo Modules API to wrap `youtubedl-android` into a custom Kotlin module. The UI is built with Expo Router and NativeTabs, leveraging Reanimated for smooth progress bar animations. The app uses a modular architecture where the download engine, format picker, playlist manager, and progress tracker are all separate concerns that communicate through typed event interfaces.

What I Would Do Differently Next Time

If I were to rebuild FetchIt from scratch, I would invest more time upfront in building a comprehensive test harness for the download engine. Right now, testing requires manually pasting URLs from different sources and checking the output. A better approach would be a set of integration tests that automatically verify metadata extraction and download completion across a curated list of sources. I would also abstract the native module interface more aggressively so that adding iOS support later (using a different backend than youtubedl-android) would not require rewriting the JavaScript layer.

Final Reflections

A utility app does not need to be complex; it just needs to be reliable. FetchIt demonstrates how prioritizing user experience over feature bloat results in a better tool. The most important lesson from this project is that the simplest-looking apps often have the most complex internals — and that is exactly how it should be. The complexity should live in the code, not in the interface.