[
    {
        "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": ""
    }
]