Share it from YouTube
The app is a share target: tap Share in the YouTube app, pick it, and the link is pulled out of whatever text came with it. Or paste with one tap.
A whole playlist, offline, tagged properly.
An Android app I built for my girlfriend. Share a YouTube playlist into it and every song lands on her phone — skipping the ones she already has, and tagged with its title, artist and cover so Samsung Music shows them properly.
01Why it exists
My girlfriend loves saving things from YouTube — music, mostly. She had a playlist she wanted on her phone so she could listen offline, and downloading it one video at a time was such a hassle.
So I made a playlist downloader. Paste the link, or share it straight from the YouTube app, and it fetches the whole thing.
It also checks what we’ve already downloaded, so nothing comes down twice, and it writes each song’s title, artist and cover into the file. It plays straight away in Samsung Music, looking like it belongs there. The short version: she doesn’t have to pay for Spotify.
The first version, in June, was one screen that dropped files into Downloads. At the end of August I rebuilt it as a proper app — downloads that carry on in the background, a library, and playlists.
02Try it
Ten songs in the playlist, six already on her phone. Press Download on the phone and follow each song through the app — then press it again. Delete a song in between and see what comes back.
Save as
Music is saved to your Music folder, so it shows up in Samsung Music. Videos go to your Movies folder. Everything is listed in Library.
Playlists
1 playlist
Library
6 downloads
What the app sees
Late Night Drives · 10 videos
Waiting for a link.Samsung Music · Albums
Illustration rebuilt in HTML from the app’s own colours, layout and wording, with a mostly invented playlist — not a screenshot. The order of steps, the skip rule and the messages on the phone are the app’s; the line above the headline and the download speeds are made up. The progress text only changes every 400 ms, because that’s how often the real app reads it.
03What it does
The app is a share target: tap Share in the YouTube app, pick it, and the link is pulled out of whatever text came with it. Or paste with one tap.
A foreground service carries on with the screen off. A notification shows progress with a Stop button, then a summary when it’s done.
Every download is remembered by video and format. A song she deleted no longer counts as saved, so it comes back on the next run.
Title, artist, album and cover go into every song — and a playlist stays together as one album instead of one per singer.
Music is YouTube’s own AAC audio, kept as .m4a with no re-encoding. Video works when YouTube offers sound and picture in one stream; when it doesn’t, the app says so and suggests Music.
Search, filter by music or video, five ways to sort, play with any app, delete with a confirm. Each playlist gets a 2×2 cover made from its songs.
04How it’s built
The app is Kotlin and Jetpack Compose, but the downloading is done by yt-dlp — a Python program — running on the phone itself. About 2,370 lines of Kotlin and 300 of Python.
audio["\xa9nam"] = [title]
if artist:
audio["\xa9ART"] = [artist]
# Album artist is what music apps group an album by. Setting it per
# track splits one playlist into one album per singer, so for a
# playlist every track must share the same value.
if album_artist:
audio["aART"] = [album_artist]
# Marks it a compilation, which helps players keep it together.
audio["cpil"] = True
elif artist:
audio["aART"] = [artist]
if album:
audio["\xa9alb"] = [album]
if cover_data:
audio["covr"] = [MP4Cover(
cover_data,
imageformat=MP4Cover.FORMAT_PNG if cover_is_png
else MP4Cover.FORMAT_JPEG,
)]
05Build notes
yt-dlp holds Python’s interpreter lock for long stretches, so calling back into Kotlin on every chunk wasn’t an option. Instead the progress hook writes a small JSON file — atomically, at most every 0.4 s — and Kotlin polls it every 400 ms. Stop works in reverse: the hook raises on the next chunk.
Android’s media index takes the artist from the file’s own tags, so setting the MediaStore column alone wasn’t enough. mutagen writes real MP4 atoms, and every song from a playlist shares one album artist so the playlist stays a single album.
Songs go into MediaStore under Music/YT Downloader, staged behind IS_PENDING=1 until the copy finishes — a crash can’t leave a half-written track that looks playable. If the insert is refused, it falls back to Download/. Filenames are capped at 150 bytes.
A Room table with a unique index on (videoId, format) remembers every download. Before skipping one, the app checks the file is still in MediaStore — so if she deletes a song, it simply comes back next time.