Repository: streamlink/streamlink

Issue:
plugins.pluto: No playable streams found on this URL
### Checklist

- [X] This is a plugin issue and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

Unable to open any streams on Pluto

### Debug log

```text
streamlink --loglevel=debug "https://pluto.tv/en/live-tv/5a4d3a00ad95e4718ae8d8db" best
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.4
[cli][debug] Streamlink: 6.0.1
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.18.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.10.3
[cli][debug]  typing-extensions: 4.7.1
[cli][debug]  urllib3: 2.0.4
[cli][debug]  websocket-client: 1.6.1
[cli][debug] Arguments:
[cli][debug]  url=https://pluto.tv/en/live-tv/5a4d3a00ad95e4718ae8d8db
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --ffmpeg-ffmpeg=C:\Program Files\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin pluto for URL https://pluto.tv/en/live-tv/5a4d3a00ad95e4718ae8d8db
[plugins.pluto][debug] slug=5a4d3a00ad95e4718ae8d8db
[plugins.pluto][debug] app_version=7.4.2-50c72df610dfe1b89e4c5110547c8da17584191d
error: No playable streams found on this URL: https://pluto.tv/en/live-tv/5a4d3a00ad95e4718ae8d8db
```

plugins.pluto: Ad filtering not properly working for (some) streams
### Checklist

- [X] This is a [plugin issue](https://streamlink.github.io/plugins.html) and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

streamlink 6.5.0

### Description

~$ streamlink --no-config --version
streamlink 6.5.0

For at least several weeks, Pluto has not been properly filtering out ads during the ad breaks. However, the issue manifests itself differently, depending on the Pluto channel, the time or show being presented on that channel, and the particular player (VLC and MPV) being used. Depending on the channel or time or show, the ads might be filtered like used to be normal. On other channels, most or all the ads are unfiltered. Sometimes, during an ad break, the show is paused, but the audio of the advertisements play, and the stream breaks. If the user starts watching a pluto channel during an ad break, I have seen the ads run smoothly and the actual program get filtered out as though it was the ad. 

The variable, changing nature of the issue makes it hard for me to select a good sample of debug log output for diagnosing the issue. A channel which is experiencing the problem today might start filtering out ads properly later in the day during a different show. Or maybe the issue exists during one day but manifests itself a little differently on the next day. I am pasting a sample debug log from a couple of days ago, from a stream which, at the time at least, was not filtering out ads but otherwise running fairly smoothly. Also keep in mind that issue #5475 is still relevant, which requires the user to know the old urls of each stream in order to use streamlink. Please let me know if this debug log is not sufficient for some reason.

Step 1: enter "streamlink https://pluto.tv/en/live-tv/star-trek-1 best --loglevel=debug --auto-version-check=True --player=mpv --player-no-close=True" (I use a config file to enter most of these options, and I use one-click launchers on my desktop Linux to enter the command into the terminal for each stream I am interested in).
Step 2: watch the stream and wait for an ad break
Step 3: observe results and hope that the stream does not break
Step 4: close out of the stream after the ad break is over and the normal content resumes
Step 5: copy and paste the debug log from the event into a text editor.

### Debug log

```text
[cli][debug] OS:         Linux-5.15.0-91-generic-x86_64-with-glibc2.35
[cli][debug] Python:     3.10.12
[cli][debug] OpenSSL:    OpenSSL 3.0.2 15 Mar 2022
[cli][debug] Streamlink: 6.5.0
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.8.0
[cli][debug]  pycountry: 20.7.3
[cli][debug]  pycryptodome: 3.19.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.8.0
[cli][debug]  urllib3: 2.1.0
[cli][debug]  websocket-client: 1.2.3
[cli][debug] Arguments:
[cli][debug]  url=https://pluto.tv/en/live-tv/star-trek-1
[cli][debug]  --loglevel=debug
[cli][debug]  --auto-version-check=True
[cli][debug]  --player=mpv
[cli][debug]  --player-no-close=True
[cli][debug]  --default-stream=['best']
[cli][info] Found matching plugin pluto for URL https://pluto.tv/en/live-tv/star-trek-1
[plugins.pluto][debug] slug=star-trek-1
[plugins.pluto][debug] app_version=7.8.3-0cae52359d82558db2247a6ec8c0aca12b24f675
[plugins.pluto][debug] path=/stitch/hls/channel/5efbd39f8c4ce900075d7698/master.m3u8
[utils.l10n][debug] Language code: en_US
[cli][info] Available streams: 570k (worst), 1000k, 1500k, 2100k, 3100k (best)
[cli][info] Opening stream: 3100k (hls-pluto)
[cli][info] Starting player: mpv
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] First Sequence: 45; Last Sequence: 49
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 47; End Sequence: None
[stream.hls][debug] Adding segment 47 to queue
[stream.hls][debug] Adding segment 48 to queue
[stream.hls][debug] Adding segment 49 to queue
[stream.hls][debug] Writing segment 47 to output
[stream.hls][debug] Segment 47 complete
[cli.output][debug] Opening subprocess: ['/usr/bin/mpv', '--force-media-title=https://pluto.tv/en/live-tv/star-trek-1', '-']
[stream.hls][debug] Writing segment 48 to output
[stream.hls][debug] Segment 48 complete
[cli][debug] Writing stream to output
[stream.hls][debug] Writing segment 49 to output
[stream.hls][debug] Segment 49 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 50 to queue
[stream.hls][debug] Writing segment 50 to output
[stream.hls][debug] Segment 50 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 51 to queue
[stream.hls][debug] Writing segment 51 to output
[stream.hls][debug] Segment 51 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 52 to queue
[stream.hls][debug] Writing segment 52 to output
[stream.hls][debug] Segment 52 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 53 to queue
[stream.hls][debug] Writing segment 53 to output
[stream.hls][debug] Segment 53 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 54 to queue
[stream.hls][debug] Writing segment 54 to output
[stream.hls][debug] Segment 54 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 55 to queue
[stream.hls][debug] Writing segment 55 to output
[stream.hls][debug] Segment 55 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 56 to queue
[stream.hls][debug] Writing segment 56 to output
[stream.hls][debug] Segment 56 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 57 to queue
[stream.hls][debug] Writing segment 57 to output
[stream.hls][debug] Segment 57 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 58 to queue
[stream.hls][debug] Writing segment 58 to output
[stream.hls][debug] Segment 58 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 59 to queue
[stream.hls][debug] Writing segment 59 to output
[stream.hls][debug] Segment 59 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 60 to queue
[stream.hls][debug] Writing segment 60 to output
[stream.hls][debug] Segment 60 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 61 to queue
[stream.hls][debug] Writing segment 61 to output
[stream.hls][debug] Segment 61 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 62 to queue
[stream.hls][debug] Writing segment 62 to output
[stream.hls][debug] Segment 62 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 63 to queue
[stream.hls][debug] Adding segment 64 to queue
[stream.hls][debug] Writing segment 63 to output
[stream.hls][debug] Segment 63 complete
[stream.hls][debug] Writing segment 64 to output
[stream.hls][debug] Segment 64 complete
[stream.hls][warning] Encountered a stream discontinuity. This is unsupported and will result in incoherent output data.
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 65 to queue
[stream.hls][debug] Writing segment 65 to output
[stream.hls][debug] Segment 65 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 66 to queue
[stream.hls][debug] Writing segment 66 to output
[stream.hls][debug] Segment 66 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 67 to queue
[stream.hls][debug] Writing segment 67 to output
[stream.hls][debug] Segment 67 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 68 to queue
[stream.hls][debug] Writing segment 68 to output
[stream.hls][debug] Segment 68 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 69 to queue
[stream.hls][debug] Writing segment 69 to output
[stream.hls][debug] Segment 69 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 70 to queue
[stream.hls][debug] Writing segment 70 to output
[stream.hls][debug] Segment 70 complete
[stream.hls][warning] Encountered a stream discontinuity. This is unsupported and will result in incoherent output data.
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 71 to queue
[stream.hls][debug] Writing segment 71 to output
[stream.hls][debug] Segment 71 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 72 to queue
[stream.hls][debug] Writing segment 72 to output
[stream.hls][debug] Segment 72 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 73 to queue
[stream.hls][debug] Discarding segment 73
[stream.hls][info] Filtering out segments and pausing stream output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 74 to queue
[stream.hls][debug] Discarding segment 74
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 75 to queue
[stream.hls][debug] Discarding segment 75
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 76 to queue
[stream.hls][debug] Writing segment 76 to output
[stream.hls][debug] Segment 76 complete
[stream.hls][warning] Encountered a stream discontinuity. This is unsupported and will result in incoherent output data.
[stream.hls][info] Resuming stream output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 77 to queue
[stream.hls][debug] Writing segment 77 to output
[stream.hls][debug] Segment 77 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 78 to queue
[stream.hls][debug] Writing segment 78 to output
[stream.hls][debug] Segment 78 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 79 to queue
[stream.hls][debug] Writing segment 79 to output
[stream.hls][debug] Segment 79 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 80 to queue
[stream.hls][debug] Writing segment 80 to output
[stream.hls][debug] Segment 80 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 81 to queue
[stream.hls][debug] Writing segment 81 to output
[stream.hls][debug] Segment 81 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 82 to queue
[stream.hls][debug] Discarding segment 82
[stream.hls][info] Filtering out segments and pausing stream output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 83 to queue
[stream.hls][debug] Discarding segment 83
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 84 to queue
[stream.hls][debug] Discarding segment 84
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 85 to queue
[stream.hls][debug] Discarding segment 85
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 86 to queue
[stream.hls][debug] Discarding segment 86
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 87 to queue
[stream.hls][debug] Discarding segment 87
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 88 to queue
[stream.hls][debug] Discarding segment 88
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 89 to queue
[stream.hls][debug] Discarding segment 89
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 90 to queue
[stream.hls][debug] Discarding segment 90
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 91 to queue
[stream.hls][debug] Writing segment 91 to output
[stream.hls][debug] Segment 91 complete
[stream.hls][warning] Encountered a stream discontinuity. This is unsupported and will result in incoherent output data.
[stream.hls][info] Resuming stream output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 92 to queue
[stream.hls][debug] Writing segment 92 to output
[stream.hls][debug] Segment 92 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 93 to queue
[stream.hls][debug] Writing segment 93 to output
[stream.hls][debug] Segment 93 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 94 to queue
[stream.hls][debug] Writing segment 94 to output
[stream.hls][debug] Segment 94 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Segments in this playlist are encrypted
[stream.hls][debug] Adding segment 95 to queue
[cli][info] Player closed
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[cli][info] Stream ended
[cli][info] Closing currently open stream…
```

plugins.pluto: error Unable to validate response text: ValidationError(Callable):   iselement(None) is not true
### Checklist

- [X] This is a [plugin issue](https://streamlink.github.io/plugins.html) and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

streamlink 6.7.2

### Description

~$ streamlink --no-config --version
streamlink 6.7.2

Most pluto streams do not start and have this error instead. See https://gist.github.com/DeltaEpsilon19498/c99c333a136307139c80270090ca9e2b for an example output.

However, a few streams still work correctly. See https://gist.github.com/DeltaEpsilon19498/e7939edfb826b55c8da6402d9051916c for example output of a stream that does still work.

The error happens regardless of if a VPN is used or is not used.

### Debug log

```text
streamlink https://pluto.tv/en/live-tv/star-trek-1 --verbose-player --hls-live-edge=20 --stream-segment-threads=5
Output is at https://gist.github.com/DeltaEpsilon19498/c99c333a136307139c80270090ca9e2b 

streamlink https://pluto.tv/en/live-tv/the-godzilla-channel
Output is at https://gist.github.com/DeltaEpsilon19498/e7939edfb826b55c8da6402d9051916c

The configuration file looks like this-
loglevel=debug
verbose-player
auto-version-check=True
player-no-close
default-stream=best
```



Discussion:
This has been happening to me this entire year. I have been getting around it by using the old pluto tv urls with streamlink, which still work for now. For example, https://pluto.tv/en/live-tv/5a4d3a00ad95e4718ae8d8db corresponds to https://pluto.tv/en/live-tv/pluto-tv-comedy . If you use https://pluto.tv/en/live-tv/pluto-tv-comedy instead, the plugin does work.

However, this issue has lasted for a long time, and I am afraid that the old links like pluto-tv-comedy could be deprecated at some point in favor of 5a4d3a00ad95e4718ae8d8db . At that point, streamlink would not work at all. Therefore, I support fixing this plugin to accommodate these newer channel links. If I knew how to code, I would do it myself.
I have this issue for the last 3 days, using the URL with channel name.

Strealink does not even arrive in the error that didn't find no playable streams for me. It get stuck in app_version line.

**Debug Log:**

```
streamlink --loglevel=debug https://www.pluto.tv/en/live-tv/smithsonian-channel
[cli][debug] OS:         Linux-5.15.0-78-generic-x86_64-with-glibc2.29
[cli][debug] Python:     3.8.10
[cli][debug] Streamlink: 6.0.1
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.18.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.10.3
[cli][debug]  typing-extensions: 4.7.1
[cli][debug]  urllib3: 2.0.4
[cli][debug]  websocket-client: 1.6.1
[cli][debug] Arguments:
[cli][debug]  url=https://www.pluto.tv/en/live-tv/smithsonian-channel
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin pluto for URL https://www.pluto.tv/en/live-tv/smithsonian-channel
[plugins.pluto][debug] slug=smithsonian-channel
[plugins.pluto][debug] app_version=7.4.2-50c72df610dfe1b89e4c5110547c8da17584191d
```
> I have this issue for the last 3 days, using the URL with channel name.
> 
> Strealink does not even arrive in the error that didn't find no playable streams for me. It get stuck in app_version line.
> 
> **Debug Log:**
> 
> ```
> streamlink --loglevel=debug https://www.pluto.tv/en/live-tv/smithsonian-channel
> [cli][debug] OS:         Linux-5.15.0-78-generic-x86_64-with-glibc2.29
> [cli][debug] Python:     3.8.10
> [cli][debug] Streamlink: 6.0.1
> [cli][debug] Dependencies:
> [cli][debug]  certifi: 2023.7.22
> [cli][debug]  isodate: 0.6.1
> [cli][debug]  lxml: 4.9.3
> [cli][debug]  pycountry: 22.3.5
> [cli][debug]  pycryptodome: 3.18.0
> [cli][debug]  PySocks: 1.7.1
> [cli][debug]  requests: 2.31.0
> [cli][debug]  trio: 0.22.2
> [cli][debug]  trio-websocket: 0.10.3
> [cli][debug]  typing-extensions: 4.7.1
> [cli][debug]  urllib3: 2.0.4
> [cli][debug]  websocket-client: 1.6.1
> [cli][debug] Arguments:
> [cli][debug]  url=https://www.pluto.tv/en/live-tv/smithsonian-channel
> [cli][debug]  --loglevel=debug
> [cli][info] Found matching plugin pluto for URL https://www.pluto.tv/en/live-tv/smithsonian-channel
> [plugins.pluto][debug] slug=smithsonian-channel
> [plugins.pluto][debug] app_version=7.4.2-50c72df610dfe1b89e4c5110547c8da17584191d
> ```

try "streamlink https://pluto.tv/en/live-tv/smithsonian-channel-selects" instead 
The following are channelslugs available currently if you have a United States IP address. For example, to watch pluto-tv-drama, enter "streamlink https://www.pluto.tv/en/live-tv/pluto-tv-drama best" into your command line interface.
``` "channelSlugs": [
                "pluto-tv-spotlight",
                "pluto-tv-icons",
                "pluto-tv-action",
                "pluto-tv-reaction",
                "pluto-tv-comedy",
                "pluto-tv-drama",
                "pluto-tv-fantastic",
                "pluto-tv-romance",
                "pluto-tv-crime-movies",
                "pluto-tv-thrillers",
                "pluto-tv-horror",
                "pluto-tv-terror",
                "black-cinema",
                "moviesphere-ptv1",
                "pluto-tv-staff-picks",
                "pluto-tv-documentaries",
                "00s-replay",
                "90s-throwback",
                "80s-rewind",
                "70s-cinema",
                "paramount-movie-channel",
                "pluto-tv-westerns-ptv1",
                "classic-movies-channel",
                "pluto-tv-cult-films",
                "flicks-of-fury"
                "sitcom-legends",
                "tv-land-sitcoms",
                "more-tv-sitcoms",
                "black-throwbacks",
                "funny-af",
                "comedy-central-pluto-tv",
                "comedy-central-animation",
                "pluto-tv-pranks",
                "tosh-1",
                "wild-n-out",
                "laughoutloud",
                "stand-up-tv",
                "slightly-off-ifc",
                "british-comedy",
                "mst3k",
                "rifftrax",
                "always-funny-videos",
                "failarmy"
                "classic-tv-comedy",
                "classic-tv-families",
                "black-classics",
                "i-love-lucy-ptv1",
                "the-andy-griffith-show",
                "the-addams-family",
                "beverly-hillbillies-ptv1",
                "happy-days-ptv1",
                "threes-company",
                "family-ties",
                "90210",
                "baywatch",
                "the-love-boat",
                "dynasty",
                "classic-tv-drama-ptv1",
                "classic-tv-crime-drama",
                "matlock",
                "perry-mason",
                "mission-impossible",
                "western-tv",
                "gunsmoke",
                "rawhide",
                "wanted-dead-or-alive",
                "the-rifleman",
                "classic-tv-variety",
                "the-bob-ross-channel",
                "shout-tv"
                "star-trek-1",
                "more-star-trek",
                "stargate",
                "doctor-who-classic",
                "pluto-tv-sci-fi",
                "the-asylum",
                "the-godzilla-channel",
                "tokushoutsu"
                "csi",
                "csi-miami-ptv2",
                "csi-ny",
                "criminal-minds",
                "blue-bloods-ptv1",
                "pluto-tv-crime-drama",
                "tv-land-drama",
                "more-tv-drama",
                "action-drama",
                "the-walking-dead-universe",
                "midsomer-murders",
                "britbox-mysteries",
                "british-drama",
                "pluto-tv-suspense",
                "pluto-tv-drama-life",
                "heartland",
                "degrassi"
                "pluto-tv-true-crime",
                "more-true-crime",
                "dateline-247",
                "48-hours",
                "unsolved-mysteries",
                "the-new-detectives",
                "cold-case-files",
                "forensic-files",
                "court-tv-1",
                "crime-360"
                "cops",
                "car-chase",
                "dog-the-bounty-hunter",
                "rescue-911",
                "spike",
                "pluto-tv-reality",
                "bar-rescue",
                "pluto-tv-lives",
                "xtreme-outdoor-presented-by-history",
                "spike-outdoors",
                "pluto-tv-backcountry",
                "real-disaster",
                "pluto-tv-paranormal",
                "ghost-hunters",
                "cars",
                "top-gear-ptv1",
                "dating-disasters",
                "love-and-hip-hop",
                "vh1-hip-hop-family",
                "vh1-i-love-reality",
                "jersey-shore",
                "teen-mom",
                "mtv-dating",
                "pluto-tv-celebrity",
                "pluto-tv-weddings",
                "say-yes-to-the-dress",
                "all-reality-by-we-tv",
                "pluto-tv-competition",
                "black-ink-crew",
                "ink-master",
                "the-challenge",
                "dallas-cowboys-cheerleaders",
                "survivor-ptv1",
                "the-amazing-race",
                "fear-factor"
                "paramount-ptv1",
                "showtime-selects",
                "bet-pluto-tv",
                "bet-her",
                "ion",
                "movie-favorites-by-lifetime",
                "hallmark-movies-and-more",
                "pluto-tv-christmas-ptv2",
                "bounce-xl",
                "shades-of-black",
                "thegrio",
                "aspiretv-life",
                "mtv-pluto-tv",
                "cmt-pluto-tv",
                "logo-pluto-tv",
                "outtv-proud",
                "k-content-by-cj-enm",
                "mixible",
                "live-music-replay"
                "the-price-is-right",
                "the-price-is-right-the-barker-era",
                "lets-make-a-deal",
                "family-feud-classics",
                "hollywood-squares",
                "supermarket-sweep",
                "deal-or-no-deal",
                "are-you-smarter-than-a-5th-grader",
                "buzzr",
                "pluto-tv-game-shows",
                "more-tv-game-shows",
                "game-show-central"
                "best-of-the-drew-barrymore-show",
                "best-of-dr-phil",
                "nosey",
                "confess-by-nosey",
                "judge-nosey",
                "the-judge-judy-channel",
                "hot-bench",
                "pluto-tv-soaps",
                "qvc-1",
                "hsn"
                "pluto-tv-news",
                "cbs-news",
                "cbs-news-texas",
                "cbs-news-new-york-2",
                "cbs-news-los-angeles-2",
                "cbs-news-chicago-ptv1",
                "abc-news-live",
                "cnn-headlines",
                "nbc-news-now",
                "today-1",
                "cbs-news-spotlight",
                "weathernation-atlanta",
                "sky-news",
                "bloomberg-tv",
                "cheddar-1",
                "scripps-news",
                "newsmax2",
                "blaze-live",
                "americas-voice",
                "oan-plus",
                "the-first",
                "tyt-network"
                "cbs-sports-hq",
                "fox-sports",
                "nfl-channel",
                "mlb",
                "pga-tour",
                "golazo-network",
                "pbr-ridepass",
                "pluto-tv-sports",
                "fight",
                "bellator-mma",
                "pfl",
                "impact-wrestling-channel",
                "glory-kickboxing",
                "bein-sports-xtra",
                "mavtv-select",
                "waypoint-tv",
                "world-poker-tour",
                "pokergo"
                "home-for-the-holidays-ptv1",
                "this-old-house",
                "pluto-tv-home-ptv2",
                "tiny-house-nation",
                "pbs-antiques-roadshow",
                "antiques-roadshow-uk",
                "antiques-road-trip",
                "bbc-home-and-garden",
                "lively-place",
                "the-design-network"
                "americas-test-kitchen",
                "food-tv",
                "sweet-escapes",
                "gordon-ramsays-hells-kitchen",
                "jamie-oliver-channel",
                "iron-chef-ptv2",
                "bbc-food",
                "julia-child"
                "smithsonian-channel-selects",
                "pluto-tv-history",
                "pluto-tv-military",
                "powernation-ptv1",
                "pluto-tv-science",
                "xive-tv",
                "pluto-tv-travel",
                "pluto-tv-animals",
                "pbs-nature",
                "the-pet-collective",
                "dogs-247",
                "cats-247",
                "faith-tv",
                "tbn",
                "naturescape"
                "nick-jr-pluto-tv-ptv1",
                "zoom-zoom",
                "dora-tv",
                "little-stars-universe",
                "little-baby-bum",
                "bebecito-bum-y-sus-amigos",
                "baby-shark-tv",
                "nick-pluto-tv",
                "nick-en-espanol",
                "90s-kids-tv",
                "totally-teen",
                "totally-turtles",
                "transformerstv",
                "team-spirit",
                "lego-kids-tv",
                "kids-movie-club",
                "rainbow-squad",
                "kartoon-channel",
                "pocketwatch",
                "ryan-and-friends",
                "find-out-why",
                "forever-kids",
                "garfield-and-friends",
                "grande-ish",
                "no-parents-allowed"
                "pluto-tv-gamer",
                "ign",
                "pluto-tv-pixel-world",
                "sailor-moon",
                "anime-all-day",
                "one-piece",
                "naruto",
                "yu-gi-oh-ptv1",
                "lupin-the-3rd"
                "vevo-70s",
                "vevo-80s",
                "vevo-90s",
                "vevo-2k",
                "cmt-equal-play",
                "xite-classic-country",
                "vevo-country",
                "yo-mtv",
                "vevo-true-school-hip-hop",
                "vevo-randb",
                "vevo-retro-rock",
                "xite-rock",
                "vevo-pop",
                "mtv-spankin-new",
                "mtv-biggest-pop",
                "mtv-flow-latino-ptv1",
                "xite-gospel",
                "classica",
                "vevo-latino",
                "vevo-reggaeton-and-trap"
                "cine-premiere",
                "septimo-arte",
                "cine-clasico",
                "pelis-y-popcorn",
                "cine-adrenalina",
                "cine-terror",
                "cine-jaja",
                "cine-xoxo",
                "telemundo-telenovelas-clasicas-ptv1",
                "novelas-con-la-abuela",
                "amantes-del-romance",
                "corazon-turco",
                "badass-novelas",
                "entre-nosotras",
                "series-con-n",
                "csi-en-espanol",
                "csi-miami-en-espanol",
                "csi-ny-en-espanol",
                "crimenes-imperfectos",
                "misterios-sin-resolver-1",
                "investiga-ptv1",
                "emergencia-911",
                "mi-obsesion-favorita",
                "acapulco-shore",
                "mtv-en-espanol",
                "sony-canal-escape-perfecto",
                "spike-extremo",
                "the-walking-dead-en-espanol",
                "amc-en-espanol",
                "estrella-tv",
                "casos-de-la-dra-polo",
                "nosey-escandalos-ptv3",
                "comedy-central-en-espanol",
                "hells-kitchen-en-espanol",
                "foodies",
                "mundo-paranormal",
                "mundo-viajero",
                "mundo-geek",
                "estrella-news",
                "euronews-ptv1",
                "telefe-noticias-ptv1",
                "bein-sports-xtra-en-espanol",
                "lucha-libre-aaa",
                "vevo-iconos-latinos",
                "vevo-regional-mexicano-ptv1"
                "wsb-atlanta",
                "cbs-news-baltimore",
                "cbs-news-bay-area",
                "cbs-news-boston",
                "wsoc-charlotte",
                "cbs-news-chicago",
                "cbs-news-colorado",
                "cbs-news-detroit-ptv1",
                "cbs-news-los-angeles",
                "cbs-news-miami",
                "cbs-news-minnesota",
                "cbs-news-new-york",
                "news-12-new-york",
                "wftv-orlando",
                "cbs-news-philadelphia-ptv1",
                "cbs-news-pittsburgh",
                "cbs-news-sacramento",
                "cbs-news-texas-ptv1"
```
Please check #5910 and see if there are any channels which aren't working
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#pull-request-feedback

This is the entire filtering logic of the Pluto plugin, which, as you can see, is a simple regex for specific HLS segment URLs when are then skipped in Streamlink's output:
https://github.com/streamlink/streamlink/blame/6.5.0/src/streamlink/plugins/pluto.py#L25-L29

If some ad segments don't get filtered out anymore for a couple of weeks now, then this means that the site has added new/different ad segments, and the regex needs to be adjusted. However, if there's additional metadata in the HLS playlist that could be used to filter out ads (similar to what the Twitch plugin is doing), then this would be the better choice, because the filtering logic would not depend on segment URLs anymore. Since I don't use this site/plugin, I can't tell you if that's the case though.

See the [`--loglevel=all`](https://streamlink.github.io/cli.html#cmdoption-loglevel) log level, which will output the entire HLS playlist content on each refresh, so you're able to see the segment URLs and other metadata. Setting [`--logfile=PATH`](https://streamlink.github.io/cli.html#cmdoption-logfile) is usually a good idea when increasing the log verbosity.

The site returns `204` on the first of those two URLs if a user agent header other than `curl.*` is set, resulting in a valid, but empty response. The validation schema then fails because it expects an HTML response.

```
$ curl -v --http1.1 -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0' 'https://pluto.tv/en/live-tv/star-trek-1'
* Host pluto.tv:443 was resolved.
* IPv6: (none)
* IPv4: 54.236.159.251, 3.222.43.146, 18.233.194.99, 18.207.72.174, 3.218.177.29, 44.206.187.150, 3.220.32.178, 3.219.131.163
*   Trying 54.236.159.251:443...
* Connected to pluto.tv (54.236.159.251) port 443
* ALPN: curl offers http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / secp256r1 / rsaEncryption
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=pluto.tv
*  start date: Nov 10 00:00:00 2023 GMT
*  expire date: Dec  9 23:59:59 2024 GMT
*  subjectAltName: host "pluto.tv" matched cert's "pluto.tv"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/1.x
> GET /en/live-tv/star-trek-1 HTTP/1.1
> Host: pluto.tv
> Accept: */*
> User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
> 
< HTTP/1.1 204 No Content
< Date: Wed, 27 Mar 2024 11:59:21 GMT
< Connection: keep-alive
< access-control-allow-origin: *
< access-control-expose-headers: WWW-Authenticate,Server-Authorization
< cache-control: no-cache
< x-envoy-upstream-service-time: 1
< server: istio-envoy
< 
* Connection #0 to host pluto.tv left intact
```

```
$ curl -v --http1.1 'https://pluto.tv/en/live-tv/star-trek-1'
* Host pluto.tv:443 was resolved.
* IPv6: (none)
* IPv4: 52.70.120.201, 52.205.28.49, 54.91.128.106, 3.219.199.245, 34.204.251.30, 3.222.81.197, 34.236.135.105, 34.196.253.110
*   Trying 52.70.120.201:443...
* Connected to pluto.tv (52.70.120.201) port 443
* ALPN: curl offers http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / secp256r1 / rsaEncryption
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=pluto.tv
*  start date: Nov 10 00:00:00 2023 GMT
*  expire date: Dec  9 23:59:59 2024 GMT
*  subjectAltName: host "pluto.tv" matched cert's "pluto.tv"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/1.x
> GET /en/live-tv/star-trek-1 HTTP/1.1
> Host: pluto.tv
> User-Agent: curl/8.6.0
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Date: Wed, 27 Mar 2024 11:59:52 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Connection: keep-alive
< location: /en/live-tv/5efbd39f8c4ce900075d7698
< access-control-allow-origin: *
< access-control-expose-headers: WWW-Authenticate,Server-Authorization
< cache-control: no-cache
< x-envoy-upstream-service-time: 2
< server: istio-envoy
< 
* Connection #0 to host pluto.tv left intact
```
Thank you for your help.

If I add --http-header "User-Agent=curl/8.6.0" to either of the two links, I get another error. AttributeError: 'NoneType' object has no attribute 'get'

The gist for the first link with the user agent header is here- https://gist.github.com/DeltaEpsilon19498/f1798cc4c99a2cf6b9fdf1c69da99778

The gist for the second link with the user agent header is here- https://gist.github.com/DeltaEpsilon19498/ac960ae9bbc20d6a4acfb1456e31e3ec


> AttributeError: 'NoneType' object has no attribute 'get'

I'm aware of this follow-up issue. I've already started rewriting the plugin this noon but didn't finish it yet.

The issue is that the redirecteded URL (when the `curl` UA header is set on the input URL) includes a different video ID compared to what the API returns when requesting data of that specific video ID, and the current plugin implementation doesn't expect that. On their website, there's a second URL update with the correct video ID, but it's super annoying to figure out because of dozens of "heartbeat" HTTP requests being made **per second** :man_facepalming:, flooding the dev tools and breaking request/response filtering in Chromium. I believe it's just a simple `window.history.{push,replace}State()` anyway, so it actually doesn't matter. However, when rewriting the plugin, I will have to check their API responses and general website content structure first in order to not break any other content on there. Since I'm not familiar with the site, I'll have to double check, which takes time. I'll have another look at this tomorrow or so...
> https://pluto.tv/en/live-tv/star-trek-1
> https://pluto.tv/en/live-tv/the-godzilla-channel

Are these non-ID / channel-name URLs even relevant anymore? The entire site only contains links with explicit stream/VOD IDs instead of channel-names, movie-names, show-names, etc.

As said, when requesting the non-ID URLs and the UA header is from a regular web browser, then the server response is 204 without any content. Setting the UA to something like `curl` makes the server return a `304` with a redirection, but the redirected URL is not the requested content and simply just the content from their front page, regadless the input URL. The same thing is also true for their "start" API endpoint where the plugin gets its data from. When requesting content using a channel-name or VOD-name, then they simply return data from their front page.

My initial plan was to let the plugin follow the redirection and simply use the returned data, but this is surely not what the user expects, namely different content.

For VODs, there seems to be an API endpoint to request data via "slugs", e.g.
`https://service-vod.clusters.pluto.tv/v4/vod/slugs?slugs=south-park-de`
which then returns the same data as when requesting the VOD ID.
That API endpoint does require an authentication token though, which requires an initial API request to be made on their "start" API endpoint. This could be used as a fallback for VODs, but not for live streams. The question still remains though whether the non-ID URLs are still relevant. There's a reason why they return an empty 204 response when requesting them via your web browser.

So, where did you get the stream URLs you've posted earlier from?
My apologies for taking a while to respond. I only use github through tor for privacy reasons, and I neglected to check github during the weekend. I will try using vpn to check your pull request.

I am using the old urls as a workaround for issue 5475 (https://github.com/streamlink/streamlink/issues/5475). See my comment at https://github.com/streamlink/streamlink/issues/5475#issuecomment-1663260204 and https://github.com/streamlink/streamlink/issues/5475#issuecomment-1894323089 . I do not remember exactly how I got them, but I probably went to a bunch of channels and looked for channel slugs in the developer console, then tested them.

So for the last year and a half or two years or so, the new pluto link for the star trek channel, for example, would not work with streamlink. But the old link, https://pluto.tv/en/live-tv/star-trek-1 , would work with streamlink. See my comments on issue 5475 with channel slugs that worked when I made the list. 

Ideally, the channel-name urls would not be used anymore. I have just been using them as a workaround for issue 5475, since otherwise streamlink would not work at all for pluto tv. If the pull request fixes issue 5475, that is a bonus.



Patch:
diff --git a/src/streamlink/plugins/pluto.py b/src/streamlink/plugins/pluto.py
index 2f0feeb1b0b..ab7fb29ca42 100644
--- a/src/streamlink/plugins/pluto.py
+++ b/src/streamlink/plugins/pluto.py
@@ -13,8 +13,9 @@
 from urllib.parse import parse_qsl, urljoin
 from uuid import uuid4
 
+from streamlink.exceptions import PluginError
 from streamlink.plugin import Plugin, pluginmatcher
-from streamlink.plugin.api import validate
+from streamlink.plugin.api import useragents, validate
 from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWriter
 from streamlink.utils.url import update_qsd
 
@@ -23,7 +24,7 @@
 
 
 class PlutoHLSStreamWriter(HLSStreamWriter):
-    ad_re = re.compile(r"_ad/creative/|dai\.google\.com|Pluto_TV_OandO/.*(Bumper|plutotv_filler)")
+    ad_re = re.compile(r"_ad/creative/|creative/\d+_ad/|dai\.google\.com|Pluto_TV_OandO/.*(Bumper|plutotv_filler)")
 
     def should_filter_segment(self, segment):
         return self.ad_re.search(segment.uri) is not None or super().should_filter_segment(segment)
@@ -38,152 +39,192 @@ class PlutoHLSStream(HLSStream):
     __reader__ = PlutoHLSStreamReader
 
 
-@pluginmatcher(re.compile(r"""
-    https?://(?:www\.)?pluto\.tv/(?:\w{2}/)?(?:
-        live-tv/(?P<slug_live>[^/]+)
-        |
-        on-demand/series/(?P<slug_series>[^/]+)(?:/season/\d+)?/episode/(?P<slug_episode>[^/]+)
-        |
-        on-demand/movies/(?P<slug_movies>[^/]+)
-    )/?$
-""", re.VERBOSE))
+@pluginmatcher(
+    name="live",
+    pattern=re.compile(
+        r"https?://(?:www\.)?pluto\.tv/(?:\w{2}/)?live-tv/(?P<id>[^/]+)/?$",
+    ),
+)
+@pluginmatcher(
+    name="series",
+    pattern=re.compile(
+        r"https?://(?:www\.)?pluto\.tv/(?:\w{2}/)?on-demand/series/(?P<id_s>[^/]+)(?:/season/\d+)?/episode/(?P<id_e>[^/]+)/?$",
+    ),
+)
+@pluginmatcher(
+    name="movies",
+    pattern=re.compile(
+        r"https?://(?:www\.)?pluto\.tv/(?:\w{2}/)?on-demand/movies/(?P<id>[^/]+)/?$",
+    ),
+)
 class Pluto(Plugin):
-    def _get_api_data(self, kind, slug, slugfilter=None):
-        log.debug(f"slug={slug}")
-        app_version = self.session.http.get(self.url, schema=validate.Schema(
-            validate.parse_html(),
-            validate.xml_xpath_string(".//head/meta[@name='appVersion']/@content"),
-            validate.any(None, str),
-        ))
-        if not app_version:
-            return
-
-        log.debug(f"app_version={app_version}")
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.session.http.headers.update({"User-Agent": useragents.FIREFOX})
+        self._app_version = None
+        self._device_version = re.search(r"Firefox/(\d+(?:\.\d+)*)", useragents.FIREFOX)[1]
+        self._client_id = str(uuid4())
+
+    @property
+    def app_version(self):
+        if self._app_version:
+            return self._app_version
+
+        self._app_version = self.session.http.get(
+            self.url,
+            schema=validate.Schema(
+                validate.parse_html(),
+                validate.xml_xpath_string(".//head/meta[@name='appVersion']/@content"),
+                validate.any(None, str),
+            ),
+        )
+        if not self._app_version:
+            raise PluginError("Could not find pluto app version")
+
+        log.debug(f"{self._app_version=}")
+
+        return self._app_version
+
+    def _get_api_data(self, request):
+        log.debug(f"_get_api_data: {request=}")
+
+        schema_paths = validate.any(
+            validate.all(
+                {
+                    "paths": [
+                        validate.all(
+                            {
+                                "type": str,
+                                "path": str,
+                            },
+                            validate.union_get("type", "path"),
+                        ),
+                    ],
+                },
+                validate.get("paths"),
+            ),
+            validate.all(
+                {
+                    "path": str,
+                },
+                validate.transform(lambda obj: [("hls", obj["path"])]),
+            ),
+        )
+        schema_live = [{
+            "name": str,
+            "id": str,
+            "slug": str,
+            "stitched": schema_paths,
+        }]
+        schema_vod = [{
+            "name": str,
+            "id": str,
+            "slug": str,
+            "genre": str,
+            "stitched": validate.any(schema_paths, {}),
+            validate.optional("seasons"): [{
+                "episodes": [{
+                    "name": str,
+                    "_id": str,
+                    "slug": str,
+                    "stitched": schema_paths,
+                }],
+            }],
+        }]
 
         return self.session.http.get(
             "https://boot.pluto.tv/v4/start",
             params={
                 "appName": "web",
-                "appVersion": app_version,
-                "deviceVersion": "94.0.0",
+                "appVersion": self.app_version,
+                "deviceVersion": self._device_version,
                 "deviceModel": "web",
                 "deviceMake": "firefox",
                 "deviceType": "web",
-                "clientID": str(uuid4()),
-                "clientModelNumber": "1.0",
-                kind: slug,
+                "clientID": self._client_id,
+                "clientModelNumber": "1.0.0",
+                **request,
             },
             schema=validate.Schema(
-                validate.parse_json(), {
+                validate.parse_json(),
+                {
                     "servers": {
                         "stitcher": validate.url(),
                     },
-                    validate.optional("EPG"): [{
-                        "name": str,
-                        "id": str,
-                        "slug": str,
-                        "stitched": {
-                            "path": str,
-                        },
-                    }],
-                    validate.optional("VOD"): [{
-                        "name": str,
-                        "id": str,
-                        "slug": str,
-                        "genre": str,
-                        "stitched": {
-                            "path": str,
-                        },
-                        validate.optional("seasons"): [{
-                            "episodes": validate.all(
-                                [{
-                                    "name": str,
-                                    "_id": str,
-                                    "slug": str,
-                                    "stitched": {
-                                        "path": str,
-                                    },
-                                }],
-                                validate.filter(lambda k: slugfilter and k["slug"] == slugfilter),
-                            ),
-                        }],
-                    }],
-                    "sessionToken": str,
                     "stitcherParams": str,
+                    "sessionToken": str,
+                    validate.optional("EPG"): schema_live,
+                    validate.optional("VOD"): schema_vod,
                 },
             ),
         )
 
-    def _get_playlist(self, host, path, params, token):
-        qsd = dict(parse_qsl(params))
-        qsd["jwt"] = token
+    def _get_streams_live(self):
+        data = self._get_api_data({"channelSlug": self.match["id"]})
+        epg = data.get("EPG", [])
+        media = next((e for e in epg if e["id"] == self.match["id"]), None)
+        if not media:
+            return
+
+        self.id = media["id"]
+        self.title = media["name"]
+
+        return data, media["stitched"]
 
-        url = urljoin(host, path)
-        url = update_qsd(url, qsd)
+    def _get_streams_series(self):
+        data = self._get_api_data({"seriesIDs": self.match["id_s"]})
+        vod = data.get("VOD", [])
+        media = next((v for v in vod if v["id"] == self.match["id_s"]), None)
+        if not media:
+            return
+        seasons = media.get("seasons", [])
+        episode = next((e for s in seasons for e in s["episodes"] if e["_id"] == self.match["id_e"]), None)
+        if not episode:
+            return
 
-        return PlutoHLSStream.parse_variant_playlist(self.session, url)
+        self.id = episode["_id"]
+        self.author = media["name"]
+        self.category = media["genre"]
+        self.title = episode["name"]
 
-    @staticmethod
-    def _get_media_data(data, key, slug):
-        media = data.get(key)
-        if media and media[0]["slug"] == slug:
-            return media[0]
+        return data, episode["stitched"]
+
+    def _get_streams_movies(self):
+        data = self._get_api_data({"seriesIDs": self.match["id"]})
+        vod = data.get("VOD", [])
+        media = next((v for v in vod if v["id"] == self.match["id"]), None)
+        if not media:
+            return
+
+        self.id = media["id"]
+        self.category = media["genre"]
+        self.title = media["name"]
+
+        return data, media["stitched"]
 
     def _get_streams(self):
-        m = self.match.groupdict()
-        if m["slug_live"]:
-            data = self._get_api_data("channelSlug", m["slug_live"])
-            media = self._get_media_data(data, "EPG", m["slug_live"])
-            if not media:
-                return
-
-            self.id = media["id"]
-            self.title = media["name"]
-            path = media["stitched"]["path"]
-
-        elif m["slug_series"] and m["slug_episode"]:
-            data = self._get_api_data("episodeSlugs", m["slug_series"], slugfilter=m["slug_episode"])
-            media = self._get_media_data(data, "VOD", m["slug_series"])
-            if not media or "seasons" not in media:
-                return
-
-            for season in media["seasons"]:
-                if season["episodes"]:
-                    episode = season["episodes"][0]
-                    if episode["slug"] == m["slug_episode"]:
-                        break
-            else:
-                return
-
-            self.author = media["name"]
-            self.category = media["genre"]
-            self.id = episode["_id"]
-            self.title = episode["name"]
-            path = episode["stitched"]["path"]
-
-        elif m["slug_movies"]:
-            data = self._get_api_data("episodeSlugs", m["slug_movies"])
-            media = self._get_media_data(data, "VOD", m["slug_movies"])
-            if not media:
-                return
-
-            self.category = media["genre"]
-            self.id = media["id"]
-            self.title = media["name"]
-            path = media["stitched"]["path"]
-
-        else:
+        res = None
+        if self.matches["live"]:
+            res = self._get_streams_live()
+        elif self.matches["series"]:
+            res = self._get_streams_series()
+        elif self.matches["movies"]:
+            res = self._get_streams_movies()
+
+        if not res:
             return
 
-        log.trace(f"data={data!r}")
-        log.debug(f"path={path}")
+        data, paths = res
+        for mediatype, path in paths:
+            if mediatype != "hls":
+                continue
 
-        return self._get_playlist(
-            data["servers"]["stitcher"],
-            path,
-            data["stitcherParams"],
-            data["sessionToken"],
-        )
+            params = dict(parse_qsl(data["stitcherParams"]))
+            params["jwt"] = data["sessionToken"]
+            url = urljoin(data["servers"]["stitcher"], path)
+            url = update_qsd(url, params)
+
+            return PlutoHLSStream.parse_variant_playlist(self.session, url)
 
 
 __plugin__ = Pluto


Test patch:
diff --git a/tests/plugins/test_pluto.py b/tests/plugins/test_pluto.py
index ab6305c56e0..f8a340a3ba7 100644
--- a/tests/plugins/test_pluto.py
+++ b/tests/plugins/test_pluto.py
@@ -5,56 +5,28 @@
 class TestPluginCanHandleUrlPluto(PluginCanHandleUrl):
     __plugin__ = Pluto
 
-    should_match = [
-        "http://www.pluto.tv/live-tv/channel-lineup",
-        "http://pluto.tv/live-tv/channel",
-        "http://pluto.tv/live-tv/channel/",
-        "https://pluto.tv/live-tv/red-bull-tv-2",
-        "https://pluto.tv/live-tv/4k-tv",
-        "http://www.pluto.tv/on-demand/series/leverage/season/1/episode/the-nigerian-job-2009-1-1",
-        "http://pluto.tv/on-demand/series/fear-factor-usa-(lf)/season/5/episode/underwater-safe-bob-car-ramp-2004-5-3",
-        "https://www.pluto.tv/on-demand/movies/dr.-no-1963-1-1",
-        "http://pluto.tv/on-demand/movies/the-last-dragon-(1985)-1-1",
-        "http://www.pluto.tv/lc/live-tv/channel-lineup",
-        "http://pluto.tv/lc/live-tv/channel",
-        "http://pluto.tv/lc/live-tv/channel/",
-        "https://pluto.tv/lc/live-tv/red-bull-tv-2",
-        "https://pluto.tv/lc/live-tv/4k-tv",
-        "http://www.pluto.tv/lc/on-demand/series/leverage/season/1/episode/the-nigerian-job-2009-1-1",
-        "http://pluto.tv/lc/on-demand/series/fear-factor-usa-(lf)/season/5/episode/underwater-safe-bob-car-ramp-2004-5-3",
-        "https://www.pluto.tv/lc/on-demand/movies/dr.-no-1963-1-1",
-        "https://www.pluto.tv/lc/on-demand/movies/dr.-no-1963-1-1/",
-        "http://pluto.tv/lc/on-demand/movies/the-last-dragon-(1985)-1-1",
-        "http://pluto.tv/lc/on-demand/movies/the-last-dragon-(1985)-1-1/",
-        "https://pluto.tv/en/on-demand/series/great-british-menu-ptv1/episode/north-west-fish-2009-5-7-ptv1",
-        "https://pluto.tv/en/on-demand/series/great-british-menu-ptv1/episode/north-west-fish-2009-5-7-ptv1/",
-        "https://www.pluto.tv/en/on-demand/series/great-british-menu-ptv1/episode/north-west-fish-2009-5-7-ptv1",
-        "https://www.pluto.tv/en/on-demand/series/great-british-menu-ptv1/episode/north-west-fish-2009-5-7-ptv1/",
+    should_match_groups = [
+        (
+            ("live", "https://pluto.tv/en/live-tv/61409f8d6feb30000766b675"),
+            {"id": "61409f8d6feb30000766b675"},
+        ),
+
+        (
+            ("series", "https://pluto.tv/en/on-demand/series/5e00cd538e67b0dcb2cf3bcd/season/1/episode/60dee91cfc802600134b886d"),
+            {"id_s": "5e00cd538e67b0dcb2cf3bcd", "id_e": "60dee91cfc802600134b886d"},
+        ),
+
+        (
+            ("movies", "https://pluto.tv/en/on-demand/movies/600545d1813b2d001b686fa9"),
+            {"id": "600545d1813b2d001b686fa9"},
+        ),
     ]
 
     should_not_match = [
-        "https://fake.pluto.tv/live-tv/hello",
-        "http://www.pluto.tv/live-tv/channel-lineup/extra",
-        "https://www.pluto.tv/live-tv",
         "https://pluto.tv/live-tv",
-        "https://www.pluto.com/live-tv/swag",
-        "http://pluto.tv/movies/dr.-no-1963-1-1",
-        "http://pluto.tv/on-demand/movies/dr.-no-1/963-1-1",
-        "http://pluto.tv/on-demand/series/dr.-no-1963-1-1",
-        "http://pluto.tv/on-demand/movies/leverage/season/1/episode/the-nigerian-job-2009-1-1",
-        "http://pluto.tv/on-demand/fear-factor-usa-(lf)/season/5/episode/underwater-safe-bob-car-ramp-2004-5-3",
-        "https://fake.pluto.tv/lc/live-tv/hello",
-        "http://www.pluto.tv/lc/live-tv/channel-lineup/extra",
-        "https://www.pluto.tv/lc/live-tv",
-        "https://pluto.tv/lc/live-tv",
-        "https://www.pluto.com/lc/live-tv/swag",
-        "http://pluto.tv/lc/movies/dr.-no-1963-1-1",
-        "http://pluto.tv/lc/on-demand/movies/dr.-no-1/963-1-1",
-        "http://pluto.tv/lc/on-demand/series/dr.-no-1963-1-1",
-        "http://pluto.tv/lc/on-demand/movies/leverage/season/1/episode/the-nigerian-job-2009-1-1",
-        "http://pluto.tv/lc/on-demand/fear-factor-usa-(lf)/season/5/episode/underwater-safe-bob-car-ramp-2004-5-3",
-        "https://pluto.tv/en/on-demand/series/great-british-menu-ptv1/episode/north-west-fish-2009-5-7-ptv1/extra",
-        "https://pluto.tv/en/on-demand/series/great-british-menu-ptv1/season/5/episode/north-west-fish-2009-5-7-ptv1/extra",
-        "https://www.pluto.tv/en/on-demand/series/great-british-menu-ptv1/episode/north-west-fish-2009-5-7-ptv1/extra",
-        "https://www.pluto.tv/en/on-demand/series/great-british-menu-ptv1/season/5/episode/north-west-fish-2009-5-7-ptv1/extra",
+        "https://pluto.tv/en/live-tv/61409f8d6feb30000766b675/details",
+
+        "https://pluto.tv/en/on-demand/series/5e00cd538e67b0dcb2cf3bcd/details/season/1",
+
+        "https://pluto.tv/en/on-demand/movies/600545d1813b2d001b686fa9/details",
     ]