Integrate Netflix Now Playing sensor in to Media control card?

I am pretty well versed in media players, but this is definitley interesting. I have been working on intergrating accurate sensors that work with Music Assistant. This now makes me wonder how Node-Red would handle that process.

I also have been building several versions of a custom media card

Thanks for the info!

My goal was to get all the info into a single entity (with arguments) and Custom universal media player for Home Assistant was very helpful in this.

Imo i can do what ever i want with this entity since it would have all the data i want in a single entity

/edit:
I do notice it doesn’t refresh Netflix data to soon but that should not be to difficult.

We are on the same page. I just created a way to incorporate media devices into one card to control media systems when the Universal Media Player options failed.

I have found the Music Assistant is a whole different animal when attempting to consolidate into a Universal Media Player.

I did found out a failure, now spotify and youtube info is not presented correctly (not at all).
Will try to fix it asap

Google rotates cookies and they expire periodically.

What device are you streaming from? I went a different route by using Android Debug Bridge. I was able to simplify the Playing sensor by having it update based on the media player’s action with Netflix vs a time interval.

Today ¡ August 6, 2026

2:01:19 AM Netflix Now Playing->Passengers

2:01:09 AM Netflix Now Playing->No Title

2:00:19 AM Netflix Now Playing->The Fast and the Furious

1:59:59 AM Netflix Now Playing->No Title

1:59:39 AM Netflix Now Playing->The Fast and the Furious

I think it’s possible to eliminate the need for Media player template as well

I was currently testing from a google chromecast 4k and that has issues with debug bridge (play state randomly disconnects)
The template might ineed be dropped but for me it was necessary since i would other wise lose controls (play,pause,vol, back next track etc) when using media player card and different streaming services/apps

I believe i also have some time interval and playstate triggers but still interested in your solution :wink:

I did inject small time delays to let the activity update via Netflix. I am still testinf so those may need increased.

Example Sensor :down_arrow:

[
    {
        "id": "nf_tab_simple",
        "type": "tab",
        "label": "Netflix Now Playing",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "nf_inject_manual",
        "type": "inject",
        "z": "nf_tab_simple",
        "name": "Manual refresh",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "manual",
        "payloadType": "str",
        "x": 150,
        "y": 80,
        "wires": [
            [
                "nf_cachebust"
            ]
        ]
    },
    {
        "id": "nf_events",
        "type": "server-state-changed",
        "z": "nf_tab_simple",
        "name": "Android TV Shield",
        "server": "nf_ha_server",
        "version": 6,
        "outputs": 1,
        "exposeAsEntityConfig": "",
        "entities": {
            "entity": [
                "media_player.android_tv_shield"
            ],
            "substring": [],
            "regex": []
        },
        "outputInitially": true,
        "stateType": "str",
        "ifState": "",
        "ifStateType": "str",
        "ifStateOperator": "is",
        "outputOnlyOnStateChange": false,
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "ignorePrevStateNull": true,
        "ignorePrevStateUnknown": true,
        "ignorePrevStateUnavailable": true,
        "ignoreCurrentStateUnknown": true,
        "ignoreCurrentStateUnavailable": true,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "eventData"
            },
            {
                "property": "topic",
                "propertyType": "msg",
                "value": "",
                "valueType": "triggerId"
            }
        ],
        "x": 160,
        "y": 220,
        "wires": [
            [
                "nf_route"
            ]
        ]
    },
    {
        "id": "nf_route",
        "type": "function",
        "z": "nf_tab_simple",
        "name": "Playing? / Pause?",
        "func": "const ns = msg.data && msg.data.new_state;\nconst os = msg.data && msg.data.old_state;\nif (!ns) return [null, null];\n\nfunction isNetflixPlaying(st) {\n    if (!st || st.state !== 'playing') return false;\n    const app = (st.attributes && st.attributes.app_name) || '';\n    const id = (st.attributes && st.attributes.app_id) || '';\n    return app === 'Netflix' || id === 'com.netflix.ninja';\n}\n\nconst nowNetflix = isNetflixPlaying(ns);\nconst wasNetflix =\n    flow.get('wasNetflixPlaying') === true ||\n    isNetflixPlaying(os);\n\n// Idle / not Netflix: reset Wait 5s so Re-read + Still Netflix never run\nif (!nowNetflix) {\n    flow.set('awaitingPlayConfirm', false);\n    flow.set('wasNetflixPlaying', false);\n    flow.set('scrapeGen', (flow.get('scrapeGen') || 0) + 1);\n    const cancel = { reset: true, topic: msg.topic || '' };\n    if (wasNetflix || !os) {\n        node.status({\n            fill: 'grey',\n            shape: 'ring',\n            text: wasNetflix ? 'Idle - reset wait + No Title' : 'Startup - reset + No Title'\n        });\n        return [cancel, { payload: 'No Title' }];\n    }\n    node.status({\n        fill: 'grey',\n        shape: 'ring',\n        text: 'Idle - reset wait (' + (ns.state || '?') + ')'\n    });\n    return [cancel, null];\n}\n\n// Netflix playing → arm wait; Re-read only after 5s if still armed\nflow.set('awaitingPlayConfirm', true);\nnode.status({ fill: 'blue', shape: 'dot', text: 'Netflix playing - wait 5s' });\nreturn [msg, null];",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 400,
        "y": 220,
        "wires": [
            [
                "nf_debounce"
            ],
            [
                "nf_sensor_now"
            ]
        ]
    },
    {
        "id": "nf_debounce",
        "type": "trigger",
        "z": "nf_tab_simple",
        "name": "Wait 5s (play only)",
        "op1": "",
        "op2": "",
        "op1type": "nul",
        "op2type": "pay",
        "duration": "5",
        "extend": true,
        "overrideDelay": false,
        "units": "s",
        "reset": "",
        "bytopic": "all",
        "topic": "topic",
        "outputs": 1,
        "x": 650,
        "y": 160,
        "wires": [
            [
                "nf_wait_armed"
            ]
        ]
    },
    {
        "id": "nf_wait_armed",
        "type": "function",
        "z": "nf_tab_simple",
        "name": "Wait still armed?",
        "func": "if (flow.get('awaitingPlayConfirm') !== true) {\n    node.status({ fill: 'grey', shape: 'ring', text: 'Stopped (idle/reset)' });\n    return null;\n}\nnode.status({ fill: 'blue', shape: 'dot', text: 'Armed - re-read' });\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 880,
        "y": 100,
        "wires": [
            [
                "nf_current"
            ]
        ]
    },
    {
        "id": "nf_current",
        "type": "api-current-state",
        "z": "nf_tab_simple",
        "name": "Re-read Shield",
        "server": "nf_ha_server",
        "version": 3,
        "outputs": 1,
        "halt_if": "",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "entity_id": "media_player.android_tv_shield",
        "blockInputOverrides": true,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "entity"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_type": "str",
        "x": 1100,
        "y": 160,
        "wires": [
            [
                "nf_gate"
            ]
        ]
    },
    {
        "id": "nf_gate",
        "type": "function",
        "z": "nf_tab_simple",
        "name": "Still Netflix playing?",
        "func": "// Idle/reset already cleared awaitingPlayConfirm — stop here\nif (flow.get('awaitingPlayConfirm') !== true) {\n    node.status({ fill: 'grey', shape: 'ring', text: 'Stopped (idle/reset)' });\n    return [null, null];\n}\n\nconst entity = msg.data;\nif (!entity || entity.state === undefined) {\n    flow.set('awaitingPlayConfirm', false);\n    node.status({ fill: 'red', shape: 'ring', text: 'No entity data' });\n    return [null, null];\n}\n\nconst app = (entity.attributes && entity.attributes.app_name) || '';\nconst id = (entity.attributes && entity.attributes.app_id) || '';\nconst playing = entity.state === 'playing' && (app === 'Netflix' || id === 'com.netflix.ninja');\nconst wasPlaying = flow.get('wasNetflixPlaying') === true;\n\n// Wait finished but no longer Netflix — stop scrape path (route already cleared title)\nif (!playing) {\n    flow.set('awaitingPlayConfirm', false);\n    flow.set('wasNetflixPlaying', false);\n    flow.set('scrapeGen', (flow.get('scrapeGen') || 0) + 1);\n    node.status({ fill: 'grey', shape: 'ring', text: 'Stopped (not Netflix)' });\n    return [null, null];\n}\n\nflow.set('awaitingPlayConfirm', false);\n\nif (wasPlaying) {\n    node.status({ fill: 'blue', shape: 'ring', text: 'Already playing' });\n    return [null, null];\n}\n\nflow.set('wasNetflixPlaying', true);\nconst gen = (flow.get('scrapeGen') || 0) + 1;\nflow.set('scrapeGen', gen);\nmsg.scrapeGen = gen;\nnode.status({ fill: 'green', shape: 'dot', text: 'Netflix playing - scrape' });\nreturn [msg, null];",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1340,
        "y": 160,
        "wires": [
            [
                "nf_cachebust"
            ],
            [
                "nf_sensor_now"
            ]
        ]
    },
    {
        "id": "nf_cachebust",
        "type": "function",
        "z": "nf_tab_simple",
        "name": "Cache bust URL",
        "func": "msg.url = 'https://www.netflix.com/viewingactivity?_=' + Date.now();\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 880,
        "y": 80,
        "wires": [
            [
                "nf_http"
            ]
        ]
    },
    {
        "id": "nf_http",
        "type": "http request",
        "z": "nf_tab_simple",
        "name": "Netflix Viewing Activity",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [
            {
                "keyType": "other",
                "keyValue": "cookie",
                "valueType": "other",
                "valueValue": "your-netflix-cookie"
            },
            {
                "keyType": "other",
                "keyValue": "User-Agent",
                "valueType": "other",
                "valueValue": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
            },
            {
                "keyType": "other",
                "keyValue": "Cache-Control",
                "valueType": "other",
                "valueValue": "no-cache"
            },
            {
                "keyType": "other",
                "keyValue": "Accept-Language",
                "valueType": "other",
                "valueValue": "en-US,en;q=0.9"
            }
        ],
        "x": 1120,
        "y": 80,
        "wires": [
            [
                "nf_parse"
            ]
        ]
    },
    {
        "id": "nf_parse",
        "type": "function",
        "z": "nf_tab_simple",
        "name": "Parse viewedItems",
        "func": "const gen = msg.scrapeGen;\nif (gen != null && gen !== flow.get('scrapeGen')) {\n    node.status({ fill: 'grey', shape: 'ring', text: 'Stale scrape ignored' });\n    return null;\n}\nif (flow.get('wasNetflixPlaying') !== true) {\n    node.status({ fill: 'grey', shape: 'ring', text: 'Left Netflix - ignore scrape' });\n    return null;\n}\n\nconst html = typeof msg.payload === 'string' ? msg.payload : String(msg.payload || '');\n\nif (!html || html.length < 100) {\n    msg.payload = 'No Title';\n    node.status({ fill: 'red', shape: 'ring', text: 'Empty response' });\n    return msg;\n}\n\nif (/\\/login/i.test(msg.responseUrl || '') || /name=[\"']login/i.test(html)) {\n    msg.payload = 'Cookie expired';\n    node.status({ fill: 'red', shape: 'ring', text: 'Cookie expired' });\n    return msg;\n}\n\nfunction extractViewedItems(page) {\n    const marker = '\"viewedItems\":';\n    const start = page.indexOf(marker);\n    if (start < 0) return null;\n    let i = start + marker.length;\n    while (page[i] && /\\s/.test(page[i])) i++;\n    if (page[i] !== '[') return null;\n\n    let depth = 0;\n    let end = i;\n    for (; end < page.length; end++) {\n        const c = page[end];\n        if (c === '[') depth++;\n        else if (c === ']') {\n            depth--;\n            if (depth === 0) {\n                end++;\n                break;\n            }\n        }\n    }\n\n    const raw = page.slice(i, end);\n    const jsonText = raw.replace(/\\\\x([0-9A-Fa-f]{2})/g, (_, h) =>\n        String.fromCharCode(parseInt(h, 16))\n    );\n    return JSON.parse(jsonText);\n}\n\nfunction formatTitle(item) {\n    if (item.seriesTitle && item.episodeTitle) {\n        const season = item.seasonDescriptor ? item.seasonDescriptor + ': ' : '';\n        return (item.seriesTitle + ': ' + season + item.episodeTitle).replace(/\\s+/g, ' ').trim();\n    }\n    if (item.seriesTitle && item.title) {\n        return (item.seriesTitle + ': ' + item.title).replace(/\\s+/g, ' ').trim();\n    }\n    return item.title || item.episodeTitle || 'No Title';\n}\n\nlet items;\ntry {\n    items = extractViewedItems(html);\n} catch (err) {\n    node.error(err);\n    msg.payload = 'No Title';\n    node.status({ fill: 'red', shape: 'ring', text: 'Parse error' });\n    return msg;\n}\n\nif (!items || !items.length) {\n    msg.payload = 'No Title';\n    node.status({ fill: 'yellow', shape: 'ring', text: 'No viewedItems' });\n    return msg;\n}\n\nmsg.payload = formatTitle(items[0]);\nnode.status({ fill: 'green', shape: 'dot', text: msg.payload });\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1370,
        "y": 80,
        "wires": [
            [
                "nf_sensor_now",
                "nf_debug"
            ]
        ]
    },
    {
        "id": "nf_sensor_now",
        "type": "ha-sensor",
        "z": "nf_tab_simple",
        "name": "Netflix Now Playing",
        "entityConfig": "nf_entity_now",
        "version": 0,
        "state": "payload",
        "stateType": "msg",
        "attributes": [],
        "inputOverride": "allow",
        "outputProperties": [],
        "x": 1630,
        "y": 220,
        "wires": [
            []
        ]
    },
    {
        "id": "nf_debug",
        "type": "debug",
        "z": "nf_tab_simple",
        "name": "title",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1590,
        "y": 40,
        "wires": []
    },
    {
        "id": "nf_entity_now",
        "type": "ha-entity-config",
        "server": "nf_ha_server",
        "deviceConfig": "",
        "name": "Netflix Now Playing",
        "version": 6,
        "entityType": "sensor",
        "haConfig": [
            {
                "property": "name",
                "value": "Netflix Now Playing"
            },
            {
                "property": "icon",
                "value": "mdi:netflix"
            },
            {
                "property": "entity_picture",
                "value": ""
            },
            {
                "property": "entity_category",
                "value": ""
            },
            {
                "property": "device_class",
                "value": ""
            },
            {
                "property": "unit_of_measurement",
                "value": ""
            },
            {
                "property": "state_class",
                "value": ""
            }
        ],
        "resend": true,
        "debugEnabled": false
    },
    {
        "id": "nf_ha_server",
        "type": "server",
        "name": "Home Assistant",
        "version": 6,
        "addon": true,
        "rejectUnauthorizedCerts": true,
        "ha_boolean": [
            "y",
            "yes",
            "true",
            "on",
            "home",
            "open"
        ],
        "connectionDelay": true,
        "cacheJson": true,
        "heartbeat": false,
        "heartbeatInterval": 30,
        "areaSelector": "friendlyName",
        "deviceSelector": "friendlyName",
        "entitySelector": "friendlyName",
        "statusSeparator": "at: ",
        "statusYear": "hidden",
        "statusMonth": "short",
        "statusDay": "numeric",
        "statusHourCycle": "h23",
        "statusTimeFormat": "h:m",
        "enableGlobalContextStore": true
    }
]

Date format from your previous posted example needed updated based on how Netflix reads it. US is MM-DD-YY, EU is DD-MM-YY from my understanding.

This method works the best by disabling the AutoPlay settings here

I just mentioned this because HA templates have come a long way. Let’s tackle that after we secure a solid solution to media info (like Titles) for services that do not report natively to HA.

Edit: I built two local integrations:

  1. Android TV Remote Local - combines the best of Android Debug Bridge and Android TV Remote
  2. Media player template Local - adds the ability to template the icon (entity_picture), use an entity state in the value_template: and keep media_image_url_template: as the background attribute.

The Node-Red Sensor was updated to accommodate these changes.