Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/xhamster.py (Python)

Code before:

        if cipher_type == 'xor':
            return bytes(
                a ^ b for a, b in
                zip(ciphertext.encode(), itertools.cycle(self._XOR_KEY))).decode()

        if cipher_type == 'rot13':
            return codecs.decode(ciphertext, cipher_type)

        self.report_warning(f'Skipping format "{format_id}": unsupported cipher type "{cipher_type}"')
        return None

    def _deobfuscate_url(self, hex_string: str) -> str:
        """
        Deobfuscates a URL from a hex string, replicating the logic
        from the original JavaScript function.
        """

        def to_signed_32(n: int) -> int:
            """Converts a number to a 32-bit signed integer using bitwise math."""
            n &= 0xFFFFFFFF
            if n & 0x80000000:
                return n - 0x100000000
            return n

        def create_prng(algo_id: int, seed: int):
            s = to_signed_32(seed)

            def algo1():
                nonlocal s
                s = to_signed_32(s * 1664525 + 1013904223)
                return s & 0xFF

            def algo2():
                nonlocal s
                s = to_signed_32(s ^ (s << 13))
                s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 17))
                s = to_signed_32(s ^ (s << 5))
                return s & 0xFF

            def algo3():
                nonlocal s
                s = to_signed_32(s + 0x9e3779b9)
                e = s
                e = to_signed_32(e ^ ((e & 0xFFFFFFFF) >> 16))
                e = to_signed_32(e * to_signed_32(0x85ebca77))
                e = to_signed_32(e ^ ((e & 0xFFFFFFFF) >> 13))
                e = to_signed_32(e * to_signed_32(0xc2b2ae3d))
                e = to_signed_32(e ^ ((e & 0xFFFFFFFF) >> 16))
                return e & 0xFF


Review comment (suggestion):
Perhaps should actually be a generator, say a `class` with a `next()` method to replace `get_next_byte`...:
<details><summary>..., s/t like this</summary>

```py
...
        class prng:
            def __init__(self, algo_id: int, seed: int)
                try:
                    self._the_algo = getattr(self, f'_algo{algo_id}')
                except AttributeError:
                    raise ValueError(f'Unknown algorithm ID: {algo_id}')
                self._s = to_signed_32(seed)

            def _algo1(self, s):
                s = self._s = to_signed_32(s * 1664525 + 1013904223)
                return s

            def _algo2(self, s):
                s = to_signed_32(s ^ (s << 13))
                s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 17))
                s = self._s = to_signed_32(s ^ (s << 5))
                return s

            def _algo3(self, s):
                s = self._s = to_signed_32(s + 0x9e3779b9)
                s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 16))
                s = to_signed_32(s * to_signed_32(0x85ebca77))
                s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 13))
                s = to_signed_32(s * to_signed_32(0xc2b2ae3d))
                s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 16))
                return s

            def next(self):
                return self._the_algo(self._s) & 0xFF

        try:
            byte_data = bytes.fromhex(hex_string)
            byte_data[4]
        except (ValueError, IndexError) as e:
            raise ValueError(f'Invalid hex string provided: {e}')

        algo_id = byte_data[0]
        seed = int.from_bytes(byte_data[1:5], byteorder='little', signed=True)

        byte_gen = prng(algo_id, seed)

        decrypted_bytes = bytearray(
            byte_data[i] ^ byte_gen.next() for i in range(5, len(byte_data))
        )
...
```

</details>

Code after:
        'url': 'https://xhamster2.com/videos/femaleagent-shy-beauty-takes-the-bait-1509445',
        'only_matching': True,
    }, {
        'url': 'https://xhamster11.com/videos/femaleagent-shy-beauty-takes-the-bait-1509445',
        'only_matching': True,
    }, {
        'url': 'https://xhamster26.com/videos/femaleagent-shy-beauty-takes-the-bait-1509445',
        'only_matching': True,
    }, {
        'url': 'http://xhamster.com/movies/1509445/femaleagent_shy_beauty_takes_the_bait.html',
        'only_matching': True,
    }, {
        'url': 'http://xhamster.com/movies/2221348/britney_spears_sexy_booty.html?hd',
        'only_matching': True,
    }, {
        'url': 'http://de.xhamster.com/videos/skinny-girl-fucks-herself-hard-in-the-forest-xhnBJZx',
        'only_matching': True,
    }, {
        'url': 'https://xhday.com/videos/strapless-threesome-xhh7yVf',
        'only_matching': True,
    }, {
        'url': 'https://xhvid.com/videos/lk-mm-xhc6wn6',
        'only_matching': True,
    }, {
        'url': 'https://xhamster20.desi/videos/my-verification-video-scottishmistress23-11937369',
        'only_matching': True,
    }]

    _XOR_KEY = b'xh7999'

    def _decipher_format_url(self, format_url, format_id):
        if all(char in string.hexdigits for char in format_url):
            byte_data = bytes.fromhex(format_url)
            seed = int.from_bytes(byte_data[1:5], byteorder='little', signed=True)
            byte_gen = _ByteGenerator(byte_data[0], seed)
            return bytearray(byte ^ next(byte_gen) for byte in byte_data[5:]).decode('latin-1')

        cipher_type, _, ciphertext = try_call(
            lambda: base64.b64decode(format_url).decode().partition('_')) or [None] * 3

        if not cipher_type or not ciphertext:
            self.report_warning(f'Skipping format "{format_id}": failed to decipher URL')
            return None

        if cipher_type == 'xor':
            return bytes(
                a ^ b for a, b in
                zip(ciphertext.encode(), itertools.cycle(self._XOR_KEY))).decode()

        if cipher_type == 'rot13':
            return codecs.decode(ciphertext, cipher_type)