StreaMonitor icon indicating copy to clipboard operation
StreaMonitor copied to clipboard

Today I started reporting the error again: "Error on Download"

Open lolo1p19 opened this issue 2 months ago • 26 comments

Is this the case for everyone? Or is it just me?

lolo1p19 avatar Oct 27 '25 13:10 lolo1p19

I looked at the output and it seems that there was an error in obtaining pkey three

lolo1p19 avatar Oct 27 '25 13:10 lolo1p19

i didn't know when it's was fixed!

Garvthakral000 avatar Oct 27 '25 14:10 Garvthakral000

It's true that the error occurred when getting the pkey. I referred to another author's method to get the pkey and solved it, but I don't know how to send the code. The easiest way is to copy the pkey parameter on the m3u8 to stripchat.py and replace the corresponding parameter.

lolo1p19 avatar Oct 27 '25 16:10 lolo1p19

Please explain more or provide code i didn't understand . please sir

Garvthakral000 avatar Oct 27 '25 17:10 Garvthakral000

I’ve done some quick debugging on the livestream web page and found that the MP4 URLs in the .m3u8 playlists are no longer encrypted as before.

We can now obtain an unencrypted playlist by appending the query parameters playlistType=standard&psch=v1&pkey={pkey} to the original PlaylistVariants URL:

    def getPlaylistVariants(self, url):
-        url = "https://edge-hls.{host}/hls/{id}{vr}/master/{id}{vr}{auto}.m3u8".format(
+        url = "https://edge-hls.{host}/hls/{id}{vr}/master/{id}{vr}{auto}.m3u8?psch=v1&pkey={pkey}&playlistType=standard".format(
                host='doppiocdn.' + random.choice(['org', 'com', 'net']),
                id=self.lastInfo["streamName"],
                vr='_vr' if self.vr else '',
                auto='_auto' if not self.vr else '',
+               pkey=self._pkey,
            )

The pkey value can be extracted from the Native.js file, which is referenced in the main bundle:

@@ -121,1 +121,9 @@ def getInitialData(cls):
+        native_js_name = re.findall('require[(]"./(Native.*?[.]js)"[)]', cls._main_js_data)[0]
+
+        r = requests.get(f"{mmp_base}/{native_js_name}", headers=cls.headers)
+        if r.status_code != 200:
+            raise Exception("Failed to fetch native.js")
+        cls._native_js_data = r.content.decode('utf-8')
+        cls._pkey = re.findall('pkey ?: ?"(.*?)"', cls._native_js_data)[0]
+        print(f'pkey={cls._pkey}')

Since the .m3u8 files are now unencrypted, we can skip the decryption step by setting the m3u_processor to None:

-        self.getVideo = lambda _, url, filename: getVideoNativeHLS(self, url, filename, liveplatform.m3u_decoder)
+        self.getVideo = lambda _, url, filename: getVideoNativeHLS(self, url, filename, None)

This workaround is confirmed working as of 2025-10-28T02:05:04+08:00 but should be treated as fragile.

It’s unclear whether this shift to unencrypted playlists is temporary (e.g., during A/B testing or infrastructure migration) or a permanent relaxation of anti-scraping measures.

I’m new to this project and don’t have historical context on how often the platform changes its APIs.

@lossless1024

Rukkhadevata avatar Oct 27 '25 18:10 Rukkhadevata

I’ve done some quick debugging on the livestream web page and found that the MP4 URLs in the .m3u8 playlists are no longer encrypted as before.

We can now obtain an unencrypted playlist by appending the query parameters playlistType=standard&psch=v1&pkey={pkey} to the original PlaylistVariants URL:

def getPlaylistVariants(self, url):
  •    url = "https://edge-hls.{host}/hls/{id}{vr}/master/{id}{vr}{auto}.m3u8".format(
    
  •    url = "https://edge-hls.{host}/hls/{id}{vr}/master/{id}{vr}{auto}.m3u8?psch=v1&pkey={pkey}&playlistType=standard".format(
              host='doppiocdn.' + random.choice(['org', 'com', 'net']),
              id=self.lastInfo["streamName"],
              vr='_vr' if self.vr else '',
              auto='_auto' if not self.vr else '',
    
  •           pkey=self._pkey,
          )
    

The pkey value can be extracted from the Native.js file, which is referenced in the main bundle:

@@ -121,1 +121,9 @@ def getInitialData(cls):

  •    native_js_name = re.findall('require[(]"./(Native.*?[.]js)"[)]', cls._main_js_data)[0]
    
  •    r = requests.get(f"{mmp_base}/{native_js_name}", headers=cls.headers)
    
  •    if r.status_code != 200:
    
  •        raise Exception("Failed to fetch native.js")
    
  •    cls._native_js_data = r.content.decode('utf-8')
    
  •    cls._pkey = re.findall('pkey ?: ?"(.*?)"', cls._native_js_data)[0]
    
  •    print(f'pkey={cls._pkey}')
    

Since the .m3u8 files are now unencrypted, we can skip the decryption step by setting the m3u_processor to None:

  •    self.getVideo = lambda _, url, filename: getVideoNativeHLS(self, url, filename, liveplatform.m3u_decoder)
    
  •    self.getVideo = lambda _, url, filename: getVideoNativeHLS(self, url, filename, None)
    

This workaround is confirmed working as of 2025-10-28T02:05:04+08:00 but should be treated as fragile.

It’s unclear whether this shift to unencrypted playlists is temporary (e.g., during A/B testing or infrastructure migration) or a permanent relaxation of anti-scraping measures.

I’m new to this project and don’t have historical context on how often the platform changes its APIs.

@lossless1024

From what I can tell they are still encrypted. I changed the lines you indicated and still see error on download. Can anyone else confirm?

mikeymatrix70 avatar Oct 27 '25 18:10 mikeymatrix70

the above solution by @Rukkhadevata generates many small (less than few MB) .mp4 files as the output,

dwa99 avatar Oct 27 '25 20:10 dwa99

the above solution by @Rukkhadevata generates many small (less than few MB) .mp4 files as the output,

His code has no issues. There’s just one more place you need to remember to change. In the getPlaylistVariants(self, url) function in stripchat.py., on the second-to-last line:

return [variant | {'url': f'{variant["url"]}{"&" if "?" in variant["url"] else "?"}psch={psch}'}]

—you just need to remove psch={psch}.

gao4433 avatar Oct 27 '25 20:10 gao4433

Can someone post a working stripchat.py file? I apologize but after making the changes now the downloader crashes on start.

mikeymatrix70 avatar Oct 27 '25 21:10 mikeymatrix70

Can someone post a working stripchat.py file? I apologize but after making the changes now the downloader crashes on start.

Please post the error message so I can see if I can help resolve this issue for you.

Suzumiya258 avatar Oct 27 '25 21:10 Suzumiya258

The error doesn't stay on screen. Here is my stripchat.py file and what about stripchatvr.py?

https://1drv.ms/u/c/10a6a739b0c7e81e/EeF7ZQQU9HhCqEtWqVUoGgcB4aN9n0bl-dXpeDKEqCHf5g?e=AWQcFr

mikeymatrix70 avatar Oct 27 '25 21:10 mikeymatrix70

The error doesn't stay on screen. Here is my stripchat.py file and what about stripchatvr.py?

https://1drv.ms/u/c/10a6a739b0c7e81e/EeF7ZQQU9HhCqEtWqVUoGgcB4aN9n0bl-dXpeDKEqCHf5g?e=AWQcFr

@mikeymatrix70 You forgot to add the "get pkey" section mentioned by @Rukkhadevata in the getInitialData() function. Try adding the following code("get pkey" section) to the end of the getInitialData() function and run it again.

def getInitialData(cls):
  ...
  # get pkey
  print(cls._main_js_data)
  native_js_name = re.findall('require[(]"./(Native.*?[.]js)"[)]', cls._main_js_data)[0]
  
  r = requests.get(f"{mmp_base}/{native_js_name}", headers=cls.headers)
  if r.status_code != 200:
      raise Exception("Failed to fetch native.js")
  cls._native_js_data = r.content.decode('utf-8')
  cls._pkey = re.findall('pkey ?: ?"(.*?)"', cls._native_js_data)[0]
  print(f'pkey={cls._pkey}')

Suzumiya258 avatar Oct 27 '25 21:10 Suzumiya258

His code has no issues. There’s just one more place you need to remember to change. In the getPlaylistVariants(self, url) function in stripchat.py., on the second-to-last line:

return [variant | {'url': f'{variant["url"]}{"&" if "?" in variant["url"] else "?"}psch={psch}'}]

—you just need to remove psch={psch}.

thanks for a hint @gao4433, I removed &pkey={pkey} and now works! Credits to @Rukkhadevata,

dwa99 avatar Oct 27 '25 21:10 dwa99

Will the code be updated to account for this or will this be something we will have to manually change for now? Thanks

CaptainPornHelper avatar Oct 27 '25 22:10 CaptainPornHelper

The issue right now is for whatever reason theres two PSCH's in the master and the first one doesn't work, thats the one we are grabbing. Just make it try all of them and save the one that works for that session until restart or if it fails again.

rez-nov avatar Oct 27 '25 23:10 rez-nov

The issue right now is for whatever reason theres two PSCH's in the master and the first one doesn't work, thats the one we are grabbing. Just make it try all of them and save the one that works for that session until restart or if it fails again.

A temporarily feasible solution comes from @Rukkhadevata. You just need to replace the code in StreaMonitor\streamonitor\sites\stripchat.py.

import itertools
import random
import re
import time
import requests
import base64
import hashlib

from streamonitor.bot import Bot
from streamonitor.downloaders.hls import getVideoNativeHLS
from streamonitor.enums import Status


class StripChat(Bot):
    site = 'StripChat'
    siteslug = 'SC'

    _static_data = None
    _main_js_data = None
    _doppio_js_data = None
    _mouflon_keys: dict = None
    _cached_keys: dict[str, bytes] = None

    def __init__(self, username):
        if StripChat._static_data is None:
            StripChat._static_data = {}
            try:
                self.getInitialData()
            except Exception as e:
                StripChat._static_data = None
                raise e
        while StripChat._static_data == {}:
            time.sleep(1)
        super().__init__(username)
        self.vr = False
        self.getVideo = lambda _, url, filename: getVideoNativeHLS(self, url, filename, None)

    @classmethod
    def getInitialData(cls):
        r = requests.get('https://hu.stripchat.com/api/front/v3/config/static', headers=cls.headers)
        if r.status_code != 200:
            raise Exception("Failed to fetch static data from StripChat")
        StripChat._static_data = r.json().get('static')

        mmp_origin = StripChat._static_data['features']['MMPExternalSourceOrigin']
        mmp_version = StripChat._static_data['featuresV2']['playerModuleExternalLoading']['mmpVersion']
        mmp_base = f"{mmp_origin}/v{mmp_version}"

        r = requests.get(f"{mmp_base}/main.js", headers=cls.headers)
        if r.status_code != 200:
            raise Exception("Failed to fetch main.js from StripChat")
        StripChat._main_js_data = r.content.decode('utf-8')

        native_js_name = re.findall('require[(]"./(Native.*?[.]js)"[)]', StripChat._main_js_data)[0]
        r = requests.get(f"{mmp_base}/{native_js_name}", headers=cls.headers)
        if r.status_code != 200:
            raise Exception("Failed to fetch native.js")
        cls._native_js_data = r.content.decode('utf-8')

        # 5. 从 Native.js 提取 pkey
        cls._pkey = re.findall('pkey ?: ?"(.*?)"', cls._native_js_data)[0]




        doppio_js_name = re.findall('require[(]"./(Doppio.*?[.]js)"[)]', StripChat._main_js_data)[0]

        r = requests.get(f"{mmp_base}/{doppio_js_name}", headers=cls.headers)
        if r.status_code != 200:
            raise Exception("Failed to fetch doppio.js from StripChat")
        StripChat._doppio_js_data = r.content.decode('utf-8')

    @classmethod
    def m3u_decoder(cls, content):
        _mouflon_file_attr = "#EXT-X-MOUFLON:FILE:"
        _mouflon_filename = 'media.mp4'

        def _decode(encrypted_b64: str, key: str) -> str:
            if cls._cached_keys is None:
                cls._cached_keys = {}
            hash_bytes = cls._cached_keys[key] if key in cls._cached_keys \
                else cls._cached_keys.setdefault(key, hashlib.sha256(key.encode("utf-8")).digest())
            encrypted_data = base64.b64decode(encrypted_b64 + "==")
            return bytes(a ^ b for (a, b) in zip(encrypted_data, itertools.cycle(hash_bytes))).decode("utf-8")

        _, pkey = StripChat._getMouflonFromM3U(content)

        decoded = ''
        lines = content.splitlines()
        last_decoded_file = None
        for line in lines:
            if line.startswith(_mouflon_file_attr):
                last_decoded_file = _decode(line[len(_mouflon_file_attr):], cls.getMouflonDecKey(pkey))
            elif line.endswith(_mouflon_filename) and last_decoded_file:
                decoded += (line.replace(_mouflon_filename, last_decoded_file)) + '\n'
                last_decoded_file = None
            else:
                decoded += line + '\n'
        return decoded

    @classmethod
    def getMouflonDecKey(cls, pkey):
        if cls._mouflon_keys is None:
            cls._mouflon_keys = {}
        return cls._mouflon_keys[pkey] if pkey in cls._mouflon_keys \
            else cls._mouflon_keys.setdefault(pkey, re.findall(f'"{pkey}:(.*?)"', cls._doppio_js_data)[0])

    @staticmethod
    def _getMouflonFromM3U(m3u8_doc):
        if '#EXT-X-MOUFLON:' in m3u8_doc:
            _mouflon_start = m3u8_doc.find('#EXT-X-MOUFLON:')
            if _mouflon_start > 0:
                _mouflon = m3u8_doc[_mouflon_start:m3u8_doc.find('\n', _mouflon_start)].strip().split(':')
                psch = _mouflon[2]
                pkey = _mouflon[3]
                return psch, pkey
        return None, None

    def getWebsiteURL(self):
        return "https://stripchat.com/" + self.username

    def getVideoUrl(self):
        return self.getWantedResolutionPlaylist(None)


    def getPlaylistVariants(self, url):
        url = "https://edge-hls.{host}/hls/{id}{vr}/master/{id}{vr}{auto}.m3u8?psch=v1&pkey={pkey}&playlistType=standard".format(
                host='doppiocdn.' + random.choice(['org', 'com', 'net']),
                id=self.lastInfo["streamName"],
                vr='_vr' if self.vr else '',
                auto='_auto' if not self.vr else '',
                pkey=self._pkey,
         )
        result = requests.get(url, headers=self.headers, cookies=self.cookies)
        m3u8_doc = result.content.decode("utf-8")
        psch, pkey = StripChat._getMouflonFromM3U(m3u8_doc)
        variants = super().getPlaylistVariants(m3u_data=m3u8_doc)
        return [variant | {'url': f'{variant["url"]}{"&" if "?" in variant["url"] else "?"}psch={psch}'}
                for variant in variants]

    @staticmethod
    def uniq(length=16):
        chars = ''.join(chr(i) for i in range(ord('a'), ord('z')+1))
        chars += ''.join(chr(i) for i in range(ord('0'), ord('9')+1))
        return ''.join(random.choice(chars) for _ in range(length))

    def getStatus(self):
        r = requests.get(
            f'https://stripchat.com/api/front/v2/models/username/{self.username}/cam?uniq={StripChat.uniq()}',
            headers=self.headers
        )

        try:
            data = r.json()
        except requests.exceptions.JSONDecodeError:
            self.log('Failed to parse JSON response')
            return Status.UNKNOWN

        if 'cam' not in data:
            if 'error' in data:
                error = data['error']
                if error == 'Not Found':
                    return Status.NOTEXIST
                self.logger.warn(f'Status returned error: {error}')
            return Status.UNKNOWN

        self.lastInfo = {'model': data['user']['user']}
        if isinstance(data['cam'], dict):
            self.lastInfo |= data['cam']

        status = self.lastInfo['model'].get('status')
        if status == "public" and self.lastInfo["isCamAvailable"] and self.lastInfo["isCamActive"]:
            return Status.PUBLIC
        if status in ["private", "groupShow", "p2p", "virtualPrivate", "p2pVoice"]:
            return Status.PRIVATE
        if status in ["off", "idle"]:
            return Status.OFFLINE
        if self.lastInfo['model'].get('isDeleted') is True:
            return Status.NOTEXIST
        if data['user'].get('isGeoBanned') is True:
            return Status.RESTRICTED
        self.logger.warn(f'Got unknown status: {status}')
        return Status.UNKNOWN

gao4433 avatar Oct 28 '25 03:10 gao4433

I'm trying to understand how decryption works.

So, from following m3u8 chunk I'm extracting Zokee2OhPh9kugh4 as key and

0KPWVjC9TC6Vl6vhv3WfI6cgFPPD6Pfokf2LWOKkywqn0tgBQOUge9aXreS5NPZ/oCUZmq6toKK6jM9vwtE

as encrypted base64 data.

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MOUFLON:PSCH:v1:Zokee2OhPh9kugh4
#EXT-X-TARGETDURATION:2
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.500
#EXT-X-PART-INF:PART-TARGET=0.500
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA-SEQUENCE:2568
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-MAP:URI="https://media-hls.doppiocdn.net/b-hls-12/139856891/139856891_720p_h264_init_keHMFhUEKlXl9e7B.mp4"
#EXT-X-MOUFLON:FILE:0KPWVjC9TC6Vl6vhv3WfI6cgFPPD6Pfokf2LWOKkywqn0tgBQOUge9aXreS5NPZ/oCUZmq6toKK6jM9vwtE
#EXT-X-PART:DURATION=0.500,URI="https://media-hls.doppiocdn.net/b-hls-12/139856891/media.mp4",INDEPENDENT=YES
#EXT-X-MOUFLON:FILE:0KPWVjC9TC6Vl6vhv3WfI6cgFPPD6Pfokf2LWOKkywqn0tgBQOUge9aXreS5NPZ/oCUZmq6toKK6jc9vwtE
#EXT-X-PART:DURATION=0.500,URI="https://media-hls.doppiocdn.net/b-hls-12/139856891/media.mp4"
#EXT-X-MOUFLON:FILE:0KPWVjC9TC6Vl6vhv3WfI6cgFPPD6Pfokf2LWOKkywqn0tgBQOUge9aXreS5NPZ/oCUZmq6toKK6js9vwtE
#EXT-X-PART:DURATION=0.500,URI="https://media-hls.doppiocdn.net/b-hls-12/139856891/media.mp4"
#EXT-X-MOUFLON:FILE:0KPWVjC9TC6Vl6vhv3WfI6cgFPPD6Pfokf2LWOKkywqn0tgBQOUge9aXreS5NPZ/oCUZmq6toKK6j89vwtE
#EXT-X-PART:DURATION=0.500,URI="https://media-hls.doppiocdn.net/b-hls-12/139856891/media.mp4"
#EXT-X-PROGRAM-DATE-TIME:2025-10-28T09:56:36.903+0000
#EXTINF:2.000

After that I'm trying to decrypt it:

import itertools
import base64
import hashlib

def decode(encrypted_b64: str, key: str) -> str:
    hash_bytes = hashlib.sha256(key.encode("utf-8")).digest()
    encrypted_data = base64.b64decode(encrypted_b64 + "==")
    return bytes(a ^ b for (a, b) in zip(encrypted_data, itertools.cycle(hash_bytes))).decode("utf-8")

if __name__ == '__main__':
    encrypted_b64 = "0KPWVjC9TC6Vl6vhv3WfI6cgFPPD6Pfokf2LWOKkywqn0tgBQOUge9aXreS5NPZ/oCUZmq6toKK6jM9vwtE"
    key = "Zokee2OhPh9kugh4"
    print(decode(encrypted_b64, key))

But I'm getting invalid utf8.

What I'm doing wrong?

felix-tesla avatar Oct 28 '25 10:10 felix-tesla

I don't know how much help it will be since others have posted solutions. This seems to have worked for me:

stripchat.py

    @staticmethod
    def _getMouflonFromM3U(m3u8_doc):
        if '#EXT-X-MOUFLON:' in m3u8_doc:
            # we have the mouflon extention in the m3u8
            filtered_iterator = filter(
                lambda line: "#EXT-X-MOUFLON:PSCH" in line,
                m3u8_doc.split('\n')
            )
            # there seems to be cases where theres 2 of these keys, may need to figure out
            # for now, let's just take the last one
            lines = list(filtered_iterator)
            if len(lines) > 0:
                last_line = lines[-1]
                # #EXT-X-MOUFLON:PSCH:v1:{key}
                [_, _, psch, pkey] = last_line.strip().split(':')
                return psch, pkey
            
            _mouflon_start = m3u8_doc.find('#EXT-X-MOUFLON:')
            if _mouflon_start > 0:
                _mouflon = m3u8_doc[_mouflon_start:m3u8_doc.find('\n', _mouflon_start)].strip().split(':')
                psch = _mouflon[2]
                pkey = _mouflon[3]
                return psch, pkey
            
        return None, None

jasoncot avatar Oct 28 '25 16:10 jasoncot

Oh, now I understand, no actual decryption is needed now, only passing pkey while requesting the playlist. Thank you all.

felix-tesla avatar Oct 28 '25 16:10 felix-tesla

key = "Zokee2OhPh9kugh4"

Replace it with Quean4cai9boJa5a which is the real key mapped from Zokee2OhPh9kugh4.

Zokee2OhPh9kugh4 is called pkey (the query param name when requesting the media playlist), or keyId (term used in Doppio.js when parsing the #EXT-X-MOUFLON tag).

The decode function needs key which is mapped from keyId. You can find related code at def getMouflonDecKey(cls, pkey)

Rukkhadevata avatar Oct 28 '25 17:10 Rukkhadevata

File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connectionpool.py", line 1093, in _validate_conn conn.connect() File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connection.py", line 790, in connect sock_and_verified = _ssl_wrap_socket_and_match_hostname( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connection.py", line 969, in ssl_wrap_socket_and_match_hostname ssl_sock = ssl_wrap_socket( ^^^^^^^^^^^^^^^^ File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\util\ssl.py", line 480, in ssl_wrap_socket ssl_sock = ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\util\ssl.py", line 524, in _ssl_wrap_socket_impl return ssl_context.wrap_socket(sock, server_hostname=server_hostname) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\ssl.py", line 455, in wrap_socket return self.sslsocket_class._create( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\ssl.py", line 1041, in _create self.do_handshake() File "C:\Users\AppData\Local\Programs\Python\Python312\Lib\ssl.py", line 1319, in do_handshake self._sslobj.do_handshake() ssl.SSLEOFError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1010)

During handling of the above exception, another exception occurred:

Anyone explain this error ??

Garvthakral000 avatar Oct 29 '25 05:10 Garvthakral000

Pkey seems to be a fixed "Zokee2OhPh9kugh4". If so, can the program skip the step of obtaining pkey and directly use "Zokee2OhPh9kugh4" as the value.When the program no longer retrieves pkey, the error output will be greatly reduced.

lolo1p19 avatar Oct 29 '25 16:10 lolo1p19

For python 3.8.10

The issue right now is for whatever reason theres two PSCH's in the master and the first one doesn't work, thats the one we are grabbing. Just make it try all of them and save the one that works for that session until restart or if it fails again.

A temporarily feasible solution comes from @Rukkhadevata. You just need to replace the code in StreaMonitor\streamonitor\sites\stripchat.py.


+from typing import Dict
class StripChat(Bot):
    site = 'StripChat'
    siteslug = 'SC'

    _static_data = None
    _main_js_data = None
    _doppio_js_data = None
    _mouflon_keys: dict = None
-   _cached_keys: dict[str, bytes] = None
+   _cached_keys: Dict[str, bytes] = None
def getPlaylistVariants(self, url):
        url = "https://edge-hls.{host}/hls/{id}{vr}/master/{id}{vr}{auto}.m3u8?psch=v1&pkey={pkey}&playlistType=standard".format(
                host='doppiocdn.' + random.choice(['org', 'com', 'net']),
                id=self.lastInfo["streamName"],
                vr='_vr' if self.vr else '',
                auto='_auto' if not self.vr else '',
                pkey=self._pkey,
         )
        result = requests.get(url, headers=self.headers, cookies=self.cookies)
        m3u8_doc = result.content.decode("utf-8")
        psch, pkey = StripChat._getMouflonFromM3U(m3u8_doc)
        variants = super().getPlaylistVariants(m3u_data=m3u8_doc)
-       return [variant | {'url': f'{variant["url"]}{"&" if "?" in variant["url"] else "?"}psch={psch}'} for variant in variants]
+       return [{**variant, 'url': f'{variant["url"]}{"&" if "?" in variant["url"] else "?"}psch={psch}'} for variant in variants]

eldepor avatar Oct 30 '25 07:10 eldepor

I pushed a fix to this issue

lossless1024 avatar Nov 02 '25 00:11 lossless1024

The "Error on Download" message is still appearing.

lolo1p19 avatar Nov 07 '25 02:11 lolo1p19

download with ffmpeg works again on stripchat

LituanusVulgaris avatar Nov 11 '25 11:11 LituanusVulgaris

download with ffmpeg works again on stripchat

Really? SC is no longer encrypted? I'll try it later.

lolo1p19 avatar Nov 23 '25 22:11 lolo1p19

C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master> python Downloader.py Traceback (most recent call last): File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\Downloader.py", line 43, in main() File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\Downloader.py", line 25, in main streamers = config.loadStreamers() ^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\config.py", line 47, in loadStreamers streamer_bot = bot_class.fromConfig(streamer) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\bot.py", line 345, in fromConfig instance = cls(username=data['username']) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\sites\stripchat.py", line 31, in init raise e File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\sites\stripchat.py", line 28, in init self.getInitialData() File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\sites\stripchat.py", line 54, in getInitialData native_js_name = re.findall('require[(]"./(Native.*?[.]js)"[)]', StripChat._main_js_data)[0] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^ IndexError: list index out of range

what's the issue now ?

codebygarv avatar Nov 25 '25 04:11 codebygarv

C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master> python Downloader.py Traceback (most recent call last): File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\Downloader.py", line 43, in main() File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\Downloader.py", line 25, in main streamers = config.loadStreamers() ^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\config.py", line 47, in loadStreamers streamer_bot = bot_class.fromConfig(streamer) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\bot.py", line 345, in fromConfig instance = cls(username=data['username']) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\sites\stripchat.py", line 31, in init raise e File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\sites\stripchat.py", line 28, in init self.getInitialData() File "C:\Users\Downloads\StreaMonitor-master\StreaMonitor-master\streamonitor\sites\stripchat.py", line 54, in getInitialData native_js_name = re.findall('require[(]"./(Native.*?[.]js)"[)]', StripChat._main_js_data)[0] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^ IndexError: list index out of range

what's the issue now ?

It seems like SC changed, once again, the way the stream data is loaded. I think this issue https://github.com/lossless1024/StreaMonitor/issues/261 is the same.

CaptainPornHelper avatar Nov 27 '25 02:11 CaptainPornHelper

I added a key cache, so you can populate stripchat_mouflon_keys.json with known decryption keys. The format is {"pkey": "decryption_key"}. Like in #260

lossless1024 avatar Nov 29 '25 14:11 lossless1024