Pluto for Channels - KineticMan docker

If anyone interested, I put a quick fork out that might solve the multi-stream issue.

When Pluto TV introduced mandatory account login, all the existing solutions (jgomez177, nuken, maddox) authenticate correctly — but they all share a single hardcoded clientID. Pluto TV uses this to identify your "device", and enforces one active stream per device. So every time you tune a second channel, it kills the first one.

The Fix

Instead of one clientID, the app now maintains a pool of virtual device sessions (default: 10). Each session has its own randomly-generated clientID and its own auth token. When you tune a channel, it grabs the next slot from the pool — so each concurrent stream looks like a completely separate device to Pluto TV.

In practice: 10 simultaneous streams, no more dropped connections.

Installation

Docker:

docker run -d --restart unless-stopped \
  -p 7777:7777 \
  -e PLUTO_USERNAME='[email protected]' \
  -e PLUTO_PASSWORD='yourpassword' \
  --name pluto-for-channels \
  kineticman/pluto-for-channels:main

docker-compose:

yaml

services:
  pluto-for-channels:
    image: kineticman/pluto-for-channels:main
    container_name: pluto-for-channels
    restart: unless-stopped
    ports:
      - "7777:7777"
    environment:
      PLUTO_USERNAME: [email protected]
      PLUTO_PASSWORD: yourpassword
      PLUTO_CODE: local,us_east,us_west,ca,uk,fr,de

Then hit http://your-ip:7777 for the status page with your M3U and EPG links.

Tuning Concurrent Streams

Default is 10 simultaneous streams. If you need more or less, change one line in pluto.py and rebuild:

python

STREAM_POOL_SIZE = 10
5 Likes

Nice work @KineticMan! I'm streaming 3 Pluto channels right now using a single account, and a single CDVR Custom Channels source.

@bnhf Project One-Click to be updated soon? :wink:

Yes.

2 Likes

Now we can enjoy our Judge Judy, Price is Right, and Jersey Shore channels!

lol I actually do love Pluto -- some great movies on there and you can't beat the price.

bnhf/olivetin:latest (aka bnhf/olivetin:2026.02.27) pushed a few minutes ago with an updated Pluto-for-Channels Action. This now uses the @KineticMan fork, which includes single-account, single-M3U support for the latest changes to Pluto. Pluto account required. Supports up to 10 streams:

screenshot-htpc6-2026-02-27-14-20-53

Use the Project One-Click Delete Action to remove any versions you currently have installed -- then run this Action.

1 Like

@KineticMan, I don't know why, but all the Pluto stations are coming back with DRM tags:

image

Currently, all the ones I tested play fine in Channels and VLC, so it doesn't appear to be an issue (yet):

2026/02/28 09:27:57.848009 [M3U] stream timestamps: pluto-00s-replay: start_at=2026-02-28T09:27:56-05:00 end_at=2026-02-28T09:28:16-05:00 live_delay=15s
2026/02/28 09:27:57.855865 [TNR] Opened connection to M3U-PlutoTest for ch9016 00s Replay
2026/02/28 09:27:57.856033 [HLS] Starting live stream for channel 9016 from 172.22.0.1 (bitrate=3321kbps)
2026/02/28 09:28:07.674384 [HLS] Probed live stream in 9.791446753s: h264 1280x720 progressive 2201153bps
2026/02/28 09:28:09.388832 [HLS] Session ch9016-dANY-ip172.22.0.1 started in 11.524780619s
2026/02/28 09:30:42.309906 [HLS] Stopping transcoder session ch9016-dANY-ip172.22.0.1 (out=2m54.378333s finished=false first_seq=1 last_seq=35)
2026/02/28 09:30:42.399321 [TNR] Closed connection to M3U-PlutoTest for ch9016 00s Replay

However, I am concerned that something in there is triggering it. Here's the bits of code that might be setting it off:

                # DRM detection (headers)
                drm_detected = False
                drm_headers = ["x-drm", "drm-type", "x-playready", "x-widevine", "x-fairplay", "license", "x-license-url"]
                for h in drm_headers:
                    if h in response.headers:
                        stream_metadata.append({"field": "DRM Detected", "value": True})
                        stream_metadata.append({"field": "DRM Type", "value": response.headers[h]})
                        drm_detected = True
                        break

---

                    # DRM in manifest
                    if "widevine" in manifest.lower():
                        stream_metadata.append({"field": "DRM Detected (HLS)", "value": True})
                        stream_metadata.append({"field": "DRM Type (HLS)", "value": "Widevine"})
                        drm_detected = True
                        manifest_drm_type = "Widevine"
                    elif "playready" in manifest.lower():
                        stream_metadata.append({"field": "DRM Detected (HLS)", "value": True})
                        stream_metadata.append({"field": "DRM Type (HLS)", "value": "PlayReady"})
                        drm_detected = True
                        manifest_drm_type = "PlayReady"
                    elif "fairplay" in manifest.lower():
                        stream_metadata.append({"field": "DRM Detected (HLS)", "value": True})
                        stream_metadata.append({"field": "DRM Type (HLS)", "value": "FairPlay"})
                        drm_detected = True
                        manifest_drm_type = "FairPlay"

---

                        # MPEG-TS DRM detection (heuristic)
                        if ts_info.get("drm_detected"):
                            stream_metadata.append({"field": "DRM Detected (MPEG-TS)", "value": True})
                            stream_metadata.append({"field": "DRM Type (MPEG-TS)", "value": ts_info.get("drm_type", "Conditional Access/ECM/EMM/Private Data")})
                            drm_detected = True

---

                # If DRM detected, update status
                if drm_detected or manifest_drm_type:
                    status = "DRM"

Based upon this, it appears to be in the Manifest. I don't have time to look into further right now, but can get to it around the end of this coming week. In the meantime, I'm turning off station status checking in PLM for Pluto just to not cause the stations to be disabled.

:warning: :warning: :warning: Either way, this could be a heads up for everyone that DRM might be coming for all Pluto stations (not just select ones) no matter what. :warning: :warning: :warning:

I wouldn’t panic yet- the original code (which I didn’t touch) always sent a manifest that says “widevine capable” because we told Pluto we support it during authentication. Your DRM detector is simply reading the capability declaration I suspect.

I could experiment removing that tag in the boot parm to say we don’t support DRM?


'drmCapabilities': '',

New bnhf/olivetin:latest (aka bnhf/olivetin:2026.02.28) pushed this morning with reworked Project One-Click support across the board for the latest Pluto-for-Channels. This supports full removal of any previous One-Click installed versions, and installation of the @KineticMan fork.

For those of you continuing to use a hammer-and-nails approach to installing CDVR-related extensions, think of Project One-Click as your compressor-and-air-nailer solution. It's a powerful tool, with excellent ease-of-use -- once you've done the initial setup. No more bent nails or smashed fingers. :slight_smile:

Does "original code" mean since you added the login, or back to the @joagomez one? Because the latter has never had this happen, with only stations like CNN Headlines returning DRM because they actually were. This is my first time doing a complete run on your fork.

Might be worth a look to limit false positives. Otherwise, I'd have to to add a situation to check for if it was found in drmCapabilities field and ignore it.

Sorry to be clear, I didn’t add that DRM piece. I’d need to look when it was added - maybe when he added authentication.

At any rate, it’s a low risk to experiment changing that field. I’ll test and report back.

OK I removed that line in pluto.py for the DRM header to send back "" instead of Widevine. Is this the expected output from the Examine page? I don't see anything with DRM listed.