Jellyfin Status
Shows title of playing movie/series with progress bar
About this flow
| System | Node-RED |
|---|---|
| AWTRIX version | AWTRIX NG |
| Topic | Miscellaneous |
| Built by | strusic |
| File | FyvOggtfrhVC.json · 8.5 KB |
| Icons | 2 |
| Published | 29 Aug 2026 · updated 30 Aug 2026 |
| Downloads | 43 |
Is this your flow?
If you created this flow, open your saved edit link to add it to your account. You can then manage it whenever you sign in. Adding it to your account replaces the edit link.
Flow description
Jellyfin to Awtrix NG (Node-RED Flow)
This simple Node-RED flow connects to your Jellyfin API and displays what is currently playing (with a smooth progress bar) on your Awtrix NG matrix.
How to use:
- Import the JSON flow into your Node-RED.
- Double-click the HTTP Request node and update the URL with your details:
- Replace
JELLYFIN_IPwith your Jellyfin server IP. - Replace
YOUR_API_KEYwith your API key generated in Jellyfin (Dashboard->Advanced->API Keys).
- Double-click the MQTT Out node and select your own MQTT Broker.
- Note: Make sure the topic matches your Awtrix prefix (default is
awtrix-ng/cmd/apps/pushed/Jellyfin).
- Click Deploy and you're good to go!
UPDATE 1. 30/08/26
- User Whitelist Feature
Added a simple configuration to filter playback by user. The matrix will now only display media watched by the specific accounts you define (e.g., ignoring kids' or guests' accounts).
You have to edit "1. Check CJK & Route" node to add users - Smart CJK (Asian Characters) Fix
The Awtrix matrix cannot render Chinese, Japanese, or Korean characters (displays???). The flow now automatically detects CJK characters in the title. - Cinemeta API Integration for Titles
When Asian characters are detected, the flow automatically extracts the IMDb ID from Jellyfin and silently fetches the clean Western title (English/Polish) using the free Cinemeta API. Regular titles bypass this step for maximum speed. - Title Caching System
Added an internal Node-RED memory cache. External APIs are queried only once per movie/episode. Subsequent 5-second progress bar updates use the cached title to prevent network spam and IP bans.
UPDATE 2. 30/08/26
- Deleting app from loop when no playing active
Just forgot to add empty payload, now it's working.
FyvOggtfrhVC.json
[
{
"id": "1b0cb4375137d04e",
"type": "tab",
"label": "Jellyfin + Awtrix-NG",
"disabled": false,
"info": "",
"env": []
},
{
"id": "3bf1bbfe232b2040",
"type": "inject",
"z": "1b0cb4375137d04e",
"name": "Timestamp 5s",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "5",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 140,
"wires": [
[
"d3dafda4e5b356ae"
]
]
},
{
"id": "d3dafda4e5b356ae",
"type": "http request",
"z": "1b0cb4375137d04e",
"name": "Jellyfin Connection",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://JELLYFIN_IP:8096/Sessions?activeWithinSeconds=120&api_key=YOUR_API_KEY",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 330,
"y": 140,
"wires": [
[
"node_check_cache"
]
]
},
{
"id": "c40f8674a4355dfe",
"type": "mqtt out",
"z": "1b0cb4375137d04e",
"name": "MQTT TOPIC",
"topic": "awtrix-ng/cmd/apps/pushed/Jellyfin",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "a22dcf45cb4d8e5e",
"x": 1120,
"y": 140,
"wires": []
},
{
"id": "node_check_cache",
"type": "function",
"z": "1b0cb4375137d04e",
"name": "1. Check CJK & Route",
"func": "const allowedUsers = [\"Dawid\"];\n\nif (!msg.payload || !Array.isArray(msg.payload) || msg.payload.length === 0) {\n return [null, { payload: \"\" }];\n}\n\nlet session = msg.payload.find(s => s.NowPlayingItem && allowedUsers.includes(s.UserName));\nif (!session) {\n return [null, { payload: \"\" }];\n}\n\nlet item = session.NowPlayingItem;\nlet title = item.Name || \"Brak tytułu\";\n\nmsg.jellyfinSession = session;\nmsg.itemId = item.Id;\n\n// SPRAWDZAMY PAMIĘĆ\nlet cachedTitle = flow.get(\"title_\" + item.Id);\nif (cachedTitle && cachedTitle !== \"Kino azjatyckie\") {\n msg.cleanTitle = cachedTitle;\n return [null, msg]; \n}\n\n// Zasięg Unicode CJK\nconst cjkRegex = /[\\u2E80-\\u9FFF\\uAC00-\\uD7AF\\u3040-\\u30FF\\uFF00-\\uFFEF]/;\nif (cjkRegex.test(title) && item.ExternalUrls) {\n let imdb = item.ExternalUrls.find(u => u.Name === \"IMDb\");\n if (imdb && imdb.Url) {\n // Wyciągamy ID filmu z linku (np. tt33311069)\n let matchId = imdb.Url.match(/(tt\\d+)/);\n if (matchId && matchId[1]) {\n let imdbId = matchId[1];\n let type = (item.Type === \"Episode\" || item.Type === \"Series\") ? \"series\" : \"movie\";\n \n // Uderzamy do całkowicie otwartego API, które ignoruje blokady!\n msg.url = `https://v3-cinemeta.strem.io/meta/${type}/${imdbId}.json`;\n return [msg, null]; \n }\n }\n}\n\n// Brak CJK\nflow.set(\"title_\" + item.Id, title);\nmsg.cleanTitle = title;\nreturn [null, msg];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 560,
"y": 140,
"wires": [
[
"node_api_request"
],
[
"node_format_awtrix"
]
]
},
{
"id": "node_api_request",
"type": "http request",
"z": "1b0cb4375137d04e",
"name": "2. Cinemeta API",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 840,
"y": 60,
"wires": [
[
"node_parse_json"
]
]
},
{
"id": "node_parse_json",
"type": "function",
"z": "1b0cb4375137d04e",
"name": "3. Parse JSON & Save",
"func": "let title = \"Kino azjatyckie\";\n\n// Ponieważ wybraliśmy opcję \"a parsed JSON object\" w HTTP Request,\n// msg.payload jest już pięknym obiektem, bez żadnego szukania tagów!\nif (msg.payload && msg.payload.meta && msg.payload.meta.name) {\n title = msg.payload.meta.name;\n} else {\n node.warn(\"API Cinemeta nie zwróciło poprawnej nazwy.\");\n}\n\n// Zapis do cache (pamięci)\nif (title !== \"Kino azjatyckie\") {\n flow.set(\"title_\" + msg.itemId, title);\n}\n\nmsg.cleanTitle = title;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 860,
"y": 140,
"wires": [
[
"node_format_awtrix"
]
]
},
{
"id": "node_format_awtrix",
"type": "function",
"z": "1b0cb4375137d04e",
"name": "4. Format Awtrix",
"func": "// Sprawdzamy, czy użytkownik coś odtwarza\nlet isPlaying = msg.jellyfinSession && msg.jellyfinSession.NowPlayingItem;\n\n// Odczytujemy stan z pamięci\nlet wasActive = flow.get(\"awtrix_app_active\") || false;\n\nif (!isPlaying) {\n // Jeśli nic nie gra, ale aplikacja była aktywna -> WYŚLIJ PUSTY OBIEKT {} TYLKO RAZ\n if (wasActive) {\n flow.set(\"awtrix_app_active\", false);\n msg.payload = {}; // Jedyny skuteczny sposób na wywalenie apki z pętli Awtrixa!\n return msg;\n }\n // Jeśli już wysłaliśmy pusty obiekt, dalej jest cisza (brak spamu)\n return null;\n}\n\n// Jeśli coś gra, odświeżamy flagę\nflow.set(\"awtrix_app_active\", true);\n\nlet session = msg.jellyfinSession;\nlet item = session.NowPlayingItem;\nlet title = msg.cleanTitle;\n\n// Logika sezonów i odcinków\nif (item.SeriesName && item.ParentIndexNumber && item.IndexNumber) {\n let s = String(item.ParentIndexNumber).padStart(2, '0');\n let e = String(item.IndexNumber).padStart(2, '0');\n title = `${title} S${s}-E${e}`;\n} else if (item.ProductionYear && !title.includes(String(item.ProductionYear))) {\n title = `${title} (${item.ProductionYear})`;\n}\n\nlet isPaused = session.PlayState && session.PlayState.IsPaused;\nlet pct = 0;\n\nif (item.RunTimeTicks > 0 && session.PlayState && session.PlayState.PositionTicks > 0) {\n pct = Math.round((session.PlayState.PositionTicks / item.RunTimeTicks) * 100);\n if (pct > 100) pct = 100;\n if (pct < 0) pct = 0;\n}\n\nmsg.payload = {\n text: title,\n icon: isPaused ? \"75372\" : \"75392\",\n textColor: \"#FFFFFF\",\n scroll: { speed: 200 },\n progress: pct,\n progressColor: \"#BB0FFF\",\n progressTrackColor: \"#333333\",\n repeat: 2,\n iconMode: \"fixed\",\n textCase: \"asTyped\"\n};\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 880,
"y": 220,
"wires": [
[
"c40f8674a4355dfe"
]
]
},
{
"id": "a22dcf45cb4d8e5e",
"type": "mqtt-broker",
"name": "Mosquitto",
"broker": "192.168.0.103",
"port": 1883,
"clientid": "",
"autoConnect": true,
"usetls": false,
"protocolVersion": 4,
"keepalive": 60,
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthRetain": "false",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closeRetain": "false",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willRetain": "false",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
}
]
This flow uses the following icons. Add them to your AWTRIX along with the flow.
8×8 px
8×8 px
Sign in to download these icons or send them to your AWTRIX.
Discussion 0
Ask a question, suggest a change or share how you use it.
No comments yet. Start the conversation.
More flows for Node-RED or Miscellaneous
Something wrong with this flow? Report it.
Have an idea?
Sign in to ask questions and join the conversation.