A self-hosted web app that turns two "vibe recommendation" subreddits — r/MoviesThatFeelLike and r/SongsThatFeelLikeThis — into a browsable, exportable discovery tool.
Both subreddits work the same way: someone posts an image (or just a feeling) and asks "what feels like this?", and the comments fill in with recommendations. That's a great source of curated, vibe-matched discovery — but it's stuck in Reddit's UI, mixed in with noise, and impossible to act on directly.
MediaThatFeelsLike pulls posts from both subs and presents them as two sections,
Movies and Music, each a mood-board grid of the post images — hover a tile with
more than one image and prev/next arrows let you flick through the gallery right there.
Clicking a tile opens the vibe page: the image(s) at the top, and a cleaned-up list
of recommendations parsed out of the comments — with the source comment one click away,
tick-boxes to curate the list, inline editing, and manual additions. Hover a recommendation
for a one-click ▶ Trailer (movies) or ▶ Play (music) button that opens a YouTube
search for it in a new tab — no setup, no API key, just a well-formed search query; YouTube's
own ranking reliably puts the actual trailer or song first. Music recommendations also get
a Spotify button alongside it, straight to a search on open.spotify.com. A movie
recommendation also gets a ▶ watch trailer here toggle when TheMovieDB is configured
on the Settings page: it expands inline to the official trailer, embedded right there on
the page, with a direct "open on YouTube" link underneath for the trailers whose owner has
disabled embedding (common for official studio uploads).
Every recommendation also has its own page (click its title). For a film that's everything TheMovieDB knows: backdrop and poster, tagline, synopsis, rating and votes, runtime, age rating, genres, the embedded trailer plus every other clip TMDB lists, cast with photos, director/writers/composer, budget and box office, keywords, and a "more like this" strip. For a track it's MusicBrainz + Last.fm + the Cover Art Archive: album art, which album it's from, length, listener and play counts, tags, a track blurb, the artist's bio, active years, external links (website, Wikipedia, Discogs, Spotify, Bandcamp…), top tracks, similar artists, and every release it appears on. Down the side, "In your library" checks Radarr / Lidarr / Jellyfin / Navidrome live — is it there, downloaded, what quality, with a link straight to it in that app — and offers the add / playlist actions if not; a Soulseek panel searches slskd and lists every plausible copy (format, bitrate, size, who has it, free slot or queue) so you can pick one by hand instead of trusting the automatic best guess.
From there:
- Export the list as CSV, plain text, or M3U/M3U8 (with real library paths when Navidrome or Jellyfin knows the item)
- Add to Radarr (movies) or Lidarr (music) — looked up, added, monitored and searched, with a per-item result line
- Create a playlist in Jellyfin or Navidrome from everything the library already has
- Grab via slskd (music) — searches Soulseek and queues the best matching file straight into your slskd downloads, a few tracks at a time
Radarr/Lidarr/Jellyfin/Navidrome/slskd are configured once on the Settings page; credentials are encrypted at rest and never shown again.
Reddit's official API is effectively closed to new personal apps (since Nov 2025), and
unauthenticated reddit.com JSON gets an IP blocked for hours once it notices you. So the
sync reads the Arctic Shift public Reddit archive by default — full posts including
galleries, full comment trees with real scores — and downloads images from Reddit's CDN
into a local cache so the grid never hotlinks. The other two backends (direct reddit.com,
PRAW) are still there behind REDDIT_BACKEND. docs/ARCHITECTURE.md has the evidence
trail and the pacing rules.
Recommendation parsing is heuristic (links, Artist - Title, Title by Artist,
Title (Year), quoted titles, list items, short whole comments, title-case runs), each
candidate carrying a confidence score. Anything at 0.6+ is included by default; the rest
sits in a collapsed "low confidence" section to promote by hand. Curation is the point,
not an afterthought — the parser is a candidate generator.
Music has one extra wrinkle: commenters write "Title - Artist" about as often as
"Artist - Title". The parser keeps a table of artists it can trust (KnownArtist, seeded
from your Lidarr library and grown by every MusicBrainz confirmation, editable in the
admin) to orient each line, applies one orientation per comment, and MusicBrainz gets the
final say — opening a recommendation's page, or the sync sidecar's background pass,
looks the pair up both ways and quietly swaps a reversed one (the dedupe key ignores
orientation, so nothing is duplicated). docs/ARCHITECTURE.md → "Which half is the
artist?".
cp .env.example .env # fill in DJANGO_SECRET_KEY, CREDENTIAL_ENCRYPTION_KEY,
# DJANGO_ALLOWED_HOSTS and (optionally) the service URLs/keys
docker compose up -d --buildThe app is on port 8095; the sync sidecar runs a sync every 30 minutes
(SYNC_INTERVAL, SYNC_MAX_COMMENTS, SYNC_VERIFY_LIMIT in compose.yaml). Data (sqlite + cached images)
lives in ./data. Press Sync now on the Settings page for the first fill, then let
the sidecar catch up over a few runs — comment threads are capped per run on purpose.
python -m venv .venv && .venv\Scripts\activate # source .venv/bin/activate on Linux
pip install -r requirements-dev.txt
cp .env.example .env # fill in as above
python manage.py migrate && python manage.py bootstrap
python manage.py sync_reddit --limit 20 --max-comments 12
python manage.py runserverThen http://127.0.0.1:8000/. On Windows set PYTHONIOENCODING=utf-8 first — the sync
log uses arrows. pytest -q runs the tests; ruff check . lints.
| command | what it does |
|---|---|
sync_reddit [--source SUB] [--kind movies|music] [--limit N] [--max-comments N] [--refresh] [--no-images] [--force] |
fetch listings, comment threads, images; parse recommendations |
reparse_recommendations [--post ID] [--kind ...] |
re-run the parser over stored comments (no network) |
verify_recommendations [--limit N] [--recheck] [--seed-only] |
refresh KnownArtist from Lidarr, then confirm unverified music recs against MusicBrainz, swapping artist/title where the comment had them reversed (the sync sidecar runs this after every sync; SYNC_VERIFY_LIMIT, default 100) |
bootstrap |
create the two default sources, seed service settings from .env (idempotent) |
Everything is in .env (see .env.example for every key):
DJANGO_SECRET_KEY,DJANGO_DEBUG,DJANGO_ALLOWED_HOSTS(comma-separated; must include the IP/hostname you browse to)CREDENTIAL_ENCRYPTION_KEY— Fernet key for the stored service credentialsREDDIT_BACKEND(auto/archive/direct/praw),ARCHIVE_USER_AGENT,REDDIT_FETCH_USER_AGENT, optionalREDDIT_CLIENT_ID/SECRETRADARR_URL/RADARR_API_KEY,LIDARR_URL/LIDARR_API_KEY,JELLYFIN_URL/JELLYFIN_API_KEY,NAVIDROME_URL/NAVIDROME_USERNAME/NAVIDROME_PASSWORD,SLSKD_URL/SLSKD_API_KEY,TMDB_API_KEY,LASTFM_API_KEY— only used to seed the Settings page on first run; the Settings page is the source of truth afterwards. (MusicBrainz and the Cover Art Archive need no key at all.)MEDIA_DATA_DIR— where cached post images live. Leave unset for amediasubfolder next to the sqlite DB. Images are by far the largest and fastest-growing thing this app stores, so point this at different (e.g. larger, network-attached) storage if that suits your setup better; in Docker, setMEDIA_HOST_PATHin.envinstead (seecompose.yaml's comments) — that's the host-side path, bind-mounted into both containers, that this variable then points at from inside themMIN_FREE_DISK_GB(default 2.0) — image caching pauses (with a warning in the sync log) once free space underMEDIA_DATA_DIR(or wherever it defaults to) drops below this; everything else keeps working
Subreddits are managed on the Settings page too (add more sources to either section).
--backfill N (CLI or the Settings page's Sync form) pages further back into a
source's history — these subs turned out to be high-volume enough that even a
60-page backfill (100 posts/page) only reached a few months back for one and fully
exhausted six years of archive for the other, so "as far back as possible" ends up
bounded by disk space, not by how deep the archive actually goes. MIN_FREE_DISK_GB
and MEDIA_DATA_DIR above are the two knobs for that.
Every button — the bulk ones at the top of a vibe page and the "⊕" dropdown on each individual recommendation row — is a direct, explicit API call triggered by that click. None of this uses Radarr/Lidarr's own "Import List" feature (where Radarr/Lidarr itself polls an external URL and auto-imports whatever's there): the point here is picking specific recommendations out of a specific post, not following a blanket list.
The per-recommendation dropdown works independently of the row's include/exclude checkbox (an explicit click is its own instruction) and independently of the bulk buttons: for Radarr/Lidarr/slskd it's the same per-item action the bulk button already does one at a time; for Jellyfin/Navidrome it adds to an existing playlist of the name you give (creating it only if there isn't one yet) rather than always starting a new one, so picking tracks one at a time naturally builds a single running collection — leave the name prompt blank and it defaults to "MediaThatFeelsLike Picks".
- Radarr: lookup by title (+year when known), add monitored with the chosen root folder / quality profile, search immediately (toggle). Already-present films are reported, not duplicated. Either way the row's "radarr: added"/"radarr: exists" chip links straight to that film's own page in Radarr, opened in a new tab.
- Lidarr has no per-track concept, so a track is pushed as the album/single that
carries it (album lookup by
Artist Title, swapped order tried too); if none can be identified the artist is added unmonitored for hand-picking. Lidarr leaves a freshly added artist unmonitored after an album add and would skip it in RSS sync — the client waits for Lidarr's refresh and re-monitors it. - Jellyfin / Navidrome: each recommendation is fuzzy-matched against the library; the ones found go into a new playlist, the rest are listed as "not in the library".
- slskd: searches Soulseek (waits up to a configurable timeout, default 15s, before cancelling the search early to collect whatever's arrived so far), picks the best audio file by title match + format/bitrate, weighted toward peers with a free upload slot and a short queue (a "better" file behind 100 people in line may never actually arrive). Downloads the winner automatically unless "auto-download" is turned off on the Settings page, in which case it just reports what it found. A push handles up to 6 recommendations at a time by default (each search takes several seconds) — fewer if you raise the search timeout, so a batch can never outrun the server's request timeout. Press the button again to work through a longer list; anything already queued is skipped automatically. Soulseek is a P2P network, so results genuinely vary run to run — some tracks return hundreds of candidates in seconds, others none at all.
Deployed and running on the services LXC (http://192.168.1.110:8095), synced against
both subreddits and tested end-to-end against the real Radarr/Lidarr/Jellyfin/Navidrome
instances. Also on Docker Hub: wb20244/mediathatfeelslike.
There is no login — keep it on the LAN (like Glance/Homepage).
MIT.