- Youtube / Twitch /
Crunchyrolletc. to STRM files - Watch Youtube through Jellyfin or Emby
- Watch Twitch through Jellyfin or Emby
Watch Crunchyroll through Jellyfin or Emby#52
- Python 3 https://www.python.org/downloads/
- Tested with Python 3.10 and 3.11. Very new major/minor releases occasionally ship breaking changes against some of the Python dependencies; if you hit unexplained import or runtime errors, try Python 3.11.
- On Windows make sure "Add Python to PATH" is checked during install.
- FFmpeg https://ffmpeg.org/ — must be available on the system
PATH(ffmpeg -versionfrom a terminal should work). - yt-dlp https://github.com/yt-dlp/yt-dlp — must be available on the system
PATHas a binary (yt-dlp --versionmust work from a terminal), because ytdlp2STRM calls it as an external process.pip install -r requierments.txtinstalls the Python package, but on some systems the entry-point binary is not picked up onPATH. If that happens, install it as a binary instead:- Windows (PowerShell):
winget install yt-dlp.yt-dlp - Linux (Debian/Ubuntu):
sudo apt install yt-dlpor grab the release binary from the yt-dlp repo. - macOS:
brew install yt-dlp.
- Windows (PowerShell):
- To allocate ytdlp2STRM, I suggest using /opt/ in Linux or C:\ProgramData in Windows.
- The next steps have been tested on Ubuntu 22.04 LTS. In terminal:
cd /opt && git clone https://github.com/fe80Grau/ytdlp2STRM.git- Install requierments.txt
cd /opt/ytdlp2STRM && pip install -r requierments.txt- Copy service file to system services folder
sudo cp config/ytdlp2strm.service /etc/systemd/system/ytdlp2strm.service- Enable service
sudo systemctl enable ytdlp2strm.service- Start service
sudo systemctl start ytdlp2strm.service- Check it
sudo systemctl status ytdlp2strm.service- Check GUI in browser
http://localhost:5000/- The next steps have been tested on Windows 11 Pro 22H2. Using Powershell or Windows Terminal with Administrator privileges:
cd C:\ProgramData; git clone https://github.com/fe80Grau/ytdlp2STRM.git;- Install requierments.txt
cd C:\ProgramData\ytdlp2STRM; pip install -r requierments.txt- Create a task that is scheduled to run main.py at startup. If you plan to install ytdlp2STRM in a different folder than C:\ProgramData\ytdlp2STRM, edit ./config/MS-TASK-ytdlp2STRM.xml
schtasks.exe /create /tn "ytdlp2STRM" /xml C:\ProgramData\ytdlp2STRM\config\MS-TASK-ytdlp2STRM.xml- Run task
schtasks.exe /run /tn "ytdlp2STRM"- In case everything is working, these commands will return "Running"
(Get-ScheduledTask | Where TaskName -eq ytdlp2STRM ).State- Check the GUI in the browser.
http://localhost:5000/To deploy this as a Docker container, follow these steps in the ytdlp2STRM root folder.
- Build Docker image. The default host port is 5005, to change it edit Dockerfile and change the env value of DOCKER_PORT with the same port that you will configure in the docker run command.
docker build . -t "ytdlp2strm" - Create a volume to preserve data.
docker create --name ytdlp2strm-data ytdlp2strm - Run the container with volume and mount D:\media (host folder for accessing strm files, change it as you prefer) in /media (container folder).
docker run -p 5005:5000 --restart=always -d -v D:\media:/media --volumes-from ytdlp2strm-data --name ytdlp2STRM ytdlp2strm- Check the GUI in the browser
http://localhost:5005/-
- Run container. To persist strm files, you must configure a volume, that is, a directory on the host that points to the /media directory in the container. Also, the default env value of DOCKER_PORT is 5005, make sure to put 5005 as host port or re-declare the env value of DOCKER_PORT like I do in the following screenshot.

- Run container. To persist strm files, you must configure a volume, that is, a directory on the host that points to the /media directory in the container. Also, the default env value of DOCKER_PORT is 5005, make sure to put 5005 as host port or re-declare the env value of DOCKER_PORT like I do in the following screenshot.
-
Check the GUI in the browser
http://localhost:5001/- Thank you INPoppoRTUNE
- INPoppoRTUNE was here
---
services:
ytdlp2strm:
image: fe80grau/ytdlp2strm
container_name: ytdlp2STRM
environment:
- AM_I_IN_A_DOCKER_CONTAINER=Yes
- DOCKER_PORT=5005
volumes:
- /local/path/to/media:/media
- /local/path/to/channel_list.json:/opt/ytdlp2STRM/plugins/youtube/channel_list.json
- /local/path/to/yt_config.json:/opt/ytdlp2STRM/plugins/youtube/config.json
- /local/path/to/config.json:/opt/ytdlp2STRM/config/config.json
- /local/path/to/crons.json:/opt/ytdlp2STRM/config/crons.json
- ytdlp2strm-data:/opt/ytdlp2STRM
ports:
- 5005:5000
restart: always
volumes:
ytdlp2strm-data:Where:
/local/path/to/mediais the local folder where the.strmfile will be created/local/path/to/config.jsonis optional and will set your ytdlp2STRM general settings; formatted as config.example.json/local/path/to/crons.jsonis optional and will set your ytdlp2STRM cronjob settings; formatted as crons.example.json/local/path/to/channel_list.jsonis optional and will set your Youtube channel list; formatted as channel_list.example.json/local/path/to/yt_config.jsonis optional and will set your Youtube plugin settings; formatted as config.example.json
- The default env value of DOCKER_PORT is 5005, make sure to put 5005 as host port or re-declare the env value of DOCKER_PORT
- After that you can view all channels folders within /media/Youtube and their strm files. If you are using Jellyfin/Emby, add /media/Youtube, /media/Twitch
and /media/Crunchyrollas folders in Library and enjoy it!
- Fix: duplicate companion files in YouTube channels: when a YouTube video was renamed or the episode counter had advanced, every cron run regenerated
.nfo,.png,.vttand.srtfiles under a new episode number with no.strmnext to them, while the real.strmlived at the original number. The existence check now runs before the new title/episode number is computed and reuses the path of the existing.strmfor subtitle and metadata refreshes. The lookup also tolerates unreadable/locked files. - Fix #119:
video_qualitychange ignored until cache TTL expired: the direct-mode HLS cache (in memory and in<strm_output_folder>/.direct_cache) was keyed only byyoutube_id+lang, so a previously-resolved low-quality variant kept being served after changingvideo_qualityinplugins/youtube/config.json. The quality is now part of the cache key and filename, so changing the setting invalidates the previous caches immediately. - Cleanup tool:
scripts/clean_youtube_orphans.pyremoves companion files (.nfo,.png/.jpg/.webp,.vtt,.srt,.ass) that no longer have a matching.strmin the same folder. Runs in dry-run by default; pass--deleteto actually remove files.python scripts/clean_youtube_orphans.py "/media/Youtube" # dry-run python scripts/clean_youtube_orphans.py "/media/Youtube" --delete # actually delete
- Centralized version: the application version is now read from
version.py(__version__) and injected into every UI template asapp_version, instead of being hardcoded in each HTML page and incli.py. To cut a release just bumpversion.py. - YouTube
iframemode: new generation method for the YouTube plugin. The.strmfile is written with the public YouTube watch URL (https://www.youtube.com/watch?v=<id>) instead of a ytdlp2STRM endpoint, so players/apps that can resolve YouTube URLs natively don't need to go through this service. Use it from cron or CLI as--params iframe.
- Season folders by year: YouTube and Twitch videos are now organized in year-based Season folders
- Structure:
Channel/Season {year}/S{year}E{XX} - video.strm - Example:
Channel [ID]/Season 2025/S2025E01 - Video Title.strm
- Structure:
- UI improvements:
- New UI
- Authentication improvements:
- All yt-dlp commands now properly use cookie authentication
- Better handling of subscriber-only content on Twitch
- SponsorBlock doesn't work in redirect mode.
- Local NFO for each video
- audio- prefix
- Season folders by year: Videos are organized in
Season {year}folders (e.g., Season 2025, Season 2026) - Episode numbering: Files are named
S{year}E{XX}format, resetting to E01 each year - Cookie authentication: Supports both browser cookies and cookie files for age-restricted content
- Language configuration: Configurable audio language via
langparameter in config.json - Video quality limit:
video_qualitycan limit playback quality. Usebestfor the current behavior or values like1080,720,480to avoid selecting higher resolutions. - Subtitles: When
download_subtitlesis enabled, YouTube.vttsubtitles are downloaded next to each.strmfile. WebVTT files are post-processed to improve Jellyfin/Emby rendering:- Force centered subtitle alignment.
- Remove YouTube karaoke inline timestamps.
- Collapse repeated rollup/persiana cues.
- The same cleanup is also applied when subtitles are served through
/youtube/subtitles/<id>.vtt. convert_subtitles_to_srtcan also create.srtsidecar files from the cleaned VTT subtitles for better compatibility with Android/TV clients.keep_vtt_subtitleskeeps the original cleaned.vttfiles when SRT conversion is enabled.
- Direct playback cache: YouTube direct mode resolves the best HLS stream and stores the processed manifest in
<strm_output_folder>/.direct_cache.direct_stream_cache_hourscontrols how long cached direct manifests are considered valid.- Cached manifests avoid repeating the slow
yt-dlpresolution on repeated playback.
- Optimized direct HLS: With
direct_serve_media_playlistenabled, ytdlp2STRM serves the selected media playlist directly instead of the full master playlist, reducing startup work for Jellyfin/Emby while keeping the selected highest-quality stream.- The generated VOD playlist is cacheable for
direct_stream_cache_hours, which can reduce extra playlist reloads during seek operations. - Seeking still depends on Jellyfin/Emby fetching the target YouTube HLS segment from
googlevideo.com. For near-instant seeking like YouTube, the only fully reliable options are local/download playback or a future segment proxy/cache mode, both with higher disk/bandwidth cost.
- The generated VOD playlist is cacheable for
- Direct prewarm:
direct_prewarm_latest_per_channelprecaches the latest detected videos per channel during the YouTube scan.direct_prewarm_neighborsprecaches the previous and next video in the same folder when a video is played.- Prewarm runs in background and is limited to avoid launching too many
yt-dlpresolutions at once.
- If a live video is on air the !000-live-channel.strm will be created. The script will download the strm for each video in the /videos channel tab in any manner. Take a look at the limits and daterange values for videos in ./plugins/twitch/config.json.
- SponsorBlock doesn't work in redirect mode, Twitch only works in direct mode at the moment.
- Season folders by year: Videos are organized in
Season {year}folders (e.g., Season 2025, Season 2026) - Episode numbering: Files are named
S{year}E{XX}format, resetting to E01 each year - Cookie authentication: Supports browser cookies for subscriber-only content
- Plugin for 3cat, content in Catalan.
Requieres a cookie file from Premium user login (you can extract the cookie file from Crunchyroll with browser extension like https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) or load a fresh cookie from browser (check discusion in yt-dlp/yt-dlp#7442 (comment)).Requires yt-dlp nightly build to work.yt-dlp >=2024.05.22.232749.dev0Only works with login auth.I'm using a filter language your crunchyroll_audio_language config value and extractor crunchyrollbeta:hardsub=your crunchyroll_subtitle_language config value to get a version with one language and subs embeddedTo avoid constant rewriting of the strm files, a file called last_episode.txt is generated in the series directory, it contains the playlist position of the last strm downloaded, this will only generate strm for new episodes.Patch yt-dlp if Crunchyroll not works yt-dlp/yt-dlp#7442 (comment)mutate_values.jsonA particular file for this plugin. Overwrites the value of a specified field. For example, in cases where the season given by yt-dlp - season_number does not correspond to the actual season of the series. The available fields are as follows: season_number, season, episode_number, and episode.- ~~direct mode. On Crunchyroll, the direct mode inherits the functionality of the download mode. ~~
bridge mode. Given the latest updates, it is necessary to obtain the audio and video streams separately, redirect their output, and remux both tracks to finally serve them over HTTP. Experimental, sometimes it may fail to start playback and you need to try playing it again. There is no timestamp and it is not possible to navigate through the video's timeline.With download mode, the audio and video streams will be downloaded separately, and after downloading, they will be remuxed to finally serve a final video/mp4 file. The files will be downloaded to./temp/and their lifespan will be 24 hours. This is configurable inconfig.json->ytdlp2strm_temp_file_duration. The Crunchyroll plugin in download mode will automatically download the latest discovered episode of each series declared inchannel_list.json.In theconfig.jsonfile, and specifically for the Crunchyroll plugin, there are 4 parameters:jellyfin_preload_last_episode,jellyfin_base_url,jellyfin_user_id, andjellyfin_api_key. When configured with their correct values, they allow detecting if an episode is being played and pre-downloading the next one to achieve a seamless playback flow without interruptions.
A little script to serve yt-dlp video/audio as HTTP data throught Flask and dynamic URLs. We can use this dynamic URLs with youtube id video in url like http://127.0.0.1:5000/youtube/direct/FxCqhXVc9iY and open it with VLC or save it in .strm file (works in Jellyfin)
- Controller that loads plugins functions, used in crons to manage strm files
- Build strms manually:
cd /opt/ytdlp2STRM/ && python3 cli.py --media youtube --params directYou can change --media value for another plugin
- ytdlp2strm_host
- ytdlp2strm_port
- ytdlp2strm_keep_old_strm
- ytdlp2strm_temp_file_duration
-
Working with Schedule library (https://schedule.readthedocs.io/en/stable/examples.html)
-
Do attribute needs a list with commands ["--media", "youtube", "--params", "direct"], replace youtube with your plugin name and direct with your prefered mode.
-
Custom timezone for each cron
-
direct : A simple redirect to final stream URL. (faster, no disk usage, sponsorblock not works)
-
bridge : Remuxing on fly. (fast, no disk usage)
-
download : First download full video then it's served. (slow, temp disk usage)
-
iframe : The
.strmstores the public YouTube watch URL (https://www.youtube.com/watch?v=<id>) instead of a ytdlp2STRM URL. Useful for players/apps that can play YouTube URLs natively without going through this service. -
With download mode, the files in the temp folder older than 24h will be deleted.
- strm_output_folder
- channels_list_file
- days_dateafter
- videos_limit
- [YOUTUBE] sponsorblock
- [YOUTUBE] sponsorblock_cats
- [YOUTUBE] cookies *Required to obtain the manifest for age-protected videos. It can be (cookies-from-browser or cookies)
- [YOUTUBE] cookie_value *If you set cookies as browser cookies you must indicate the browser (i recommend firefox). In the case of cookies, you must indicate the cookie file path stored in text format
- [YOUTUBE] lang *Language for yt-dlp extractor
- [YOUTUBE] episode_format *Episode naming mode.
sequentialuses incremental episode numbers;mmdduses month/day style numbering. - [YOUTUBE] video_quality *Maximum video height for yt-dlp playback selection. Use
bestfor no limit, or values like1080,720,480. - [YOUTUBE] download_subtitles *Download YouTube subtitles/auto-subtitles as
.vttsidecar files and fix alignment/rollup issues for Jellyfin/Emby. - [YOUTUBE] convert_subtitles_to_srt *Generate
.srtsidecar files from cleaned.vttsubtitles for better Android/TV compatibility. - [YOUTUBE] keep_vtt_subtitles *Keep
.vttfiles after SRT conversion. SetFalseto leave only.srtfiles. - [YOUTUBE] direct_stream_cache_hours *TTL in hours for cached direct playback manifests stored in
<strm_output_folder>/.direct_cache. - [YOUTUBE] direct_serve_media_playlist *Serve the selected HLS media playlist directly instead of the master playlist to improve Jellyfin/Emby startup time.
- [YOUTUBE] direct_prewarm_latest_per_channel *Number of latest videos per channel to prewarm during YouTube scans. Set
0to disable. - [YOUTUBE] direct_prewarm_neighbors *When a direct video is played, prewarm previous/next videos in background.
- [YOUTUBE] jellyfin_integration *Trigger a Jellyfin/Emby library scan after new YouTube STRM files are generated.
- [YOUTUBE] jellyfin_base_url *Base URL of your Jellyfin/Emby server.
- [YOUTUBE] jellyfin_api_key *API key used to trigger library scans.
- [YOUTUBE] jellyfin_library_name *Library name to refresh when new YouTube content is added.
[CRUNCHYROLL] crunchyroll_auth (browser, cookies orlogin), browser option in addition with background task opening firefox is the best way to keep unatended workflow.[CRUNCHYROLL] crunchyroll_browser (set if your choice in curnchyroll_auth is browser) You can read more about this searching --cookies-from-browser in https://github.com/yt-dlp/yt-dlp[CRUNCHYROLL] crunchyroll_useragent (set if your choice in curnchyroll_auth is browser) Needs the same user agent that your browser. If you search current user-agent in Google you can see your user-agent, copy it.[CRUNCHYROLL] crunchyroll_username (set if your choice in curnchyroll_auth is login)[CRUNCHYROLL] crunchyroll_password (set if your choice in curnchyroll_auth is login)[CRUNCHYROLL] crunchyroll_cookies_file (set if your choice in curnchyroll_auth is cookies)[CRUNCHYROLL] crunchyroll_audio_language[CRUNCHYROLL] crunchyroll_subtitle_language <- embedded in video[CRUNCHYROLL] jellyfin_preload (False by default, set True to preload the next episode while the current is playing in Jellyfin)[CRUNCHYROLL] jellyfin_preload_last_episode (An @Floflo10 idea. False by default, set True to preloads the last episode at the time its strm is generated. Remember in 24h will be deleted from temp folder)[CRUNCHYROLL] jellyfin_base_url (Your Jellyfin URL, without final slash)[CRUNCHYROLL] jellyfin_user_id (Your Jellyfin user_id)[CRUNCHYROLL] jellyfin_api_key (Your Jellyfin api_key)
- [YOUTUBE] With "keyword-" prefix you can search for a keyword and this script will create the folders of channels founds dinamically and put inside them the strm files for each video. See an exaple in channel_list.example.json
- [YOUTUBE] Playlist needs "list-" prefix before playlist id, you can see an exaple in channel_list.example.json
- [YOUTUBE] If you want to get livestream from /streams youtube channel tab you need to add a new channel in channel_list with /streams (Check an example in ./plugins/youtube/channel_list.example.json)
- [TWITCH] This script makes a NFO file (tvshow.nfo) for each youtube or twitch channel (to get name, description and images). *Description only works in Linux systems at the moment
[CRUNCHYROLL] Only support URL series (not episodes), the script will create a folder for each serie, and subfolders for each season, inside season folder the strm episodes files will be created
- LINUX: ytdlp2strm.service example service to run main.py with systemctl.
- WINDOWS: MS-TASK-ytdlp2STRM.xml example scheduled task with schtasks.
ytdlp2STRM generates .strm files under the strm_output_folder configured in each plugin (plugins/<media>/config.json). Each .strm just contains a URL that points back to this service, so Jellyfin/Emby must have access both to the STRM files and to the ytdlp2STRM HTTP server (http://<host>:5000 by default).
Typical setup:
- Decide where the STRM files will live, e.g.
/media/Youtubeon Linux orD:\media\Youtubeon Windows. - Make sure that same path is the
strm_output_foldervalue inplugins/youtube/config.json(and equivalent for Twitch/etc.). - In Jellyfin/Emby create a Library of type "Shows" and point it to that folder (
/media/Youtube). Do the same for Twitch if you use that plugin. - Each channel becomes a Show, each year a Season, and each video an Episode (
Channel [ID]/Season {year}/S{year}E{XX} - Video title.strm). - In the ytdlp2STRM Youtube plugin config you can enable
jellyfin_integrationso the service triggers a scan of the configuredjellyfin_library_nameafter new STRM files are written. Populatejellyfin_base_urlandjellyfin_api_keyaccordingly.
If Jellyfin runs on a different host/container than ytdlp2STRM, the URL stored inside the
.strmfiles (http://127.0.0.1:5000/...by default) must be reachable from that host. Changeytdlp2strm_hostinconfig/config.jsonto the IP/DNS name the Jellyfin server can reach.
FileNotFoundError: [WinError 2] The system cannot find the file specified(or the equivalent on Linux/macOS) when opening adirectURL or running the plugin from the Dashboard: the service tried to runyt-dlpas a subprocess but could not find the binary onPATH. Install yt-dlp as a binary (see the Prerequisite section) and restart the ytdlp2STRM service.- Cron runs but nothing happens / it always starts on the first channel: usually a symptom of the previous point. Check
ytdlp2strm.logforFileNotFoundErrororcommand not found: yt-dlp. - Dashboard › Run says
only python cli.py command can be executed from here: fixed in recent versions by resolving the interpreter server-side. If you still hit it, update to the latest version. - Audio is in the wrong language even when
langis set: YouTube videos with dubbed tracks default to the original. Recent versions add-S lang:<lang>automatically so the configured language is preferred. Update and restart.

