Смотри мне нужно переписать вот этот код на dart.

```
import base64
import json
import re
import urllib.parse

from typing import Tuple, List, Dict, Any

NoneType = type(None)
T_PACKED = Tuple[str, int, int, List[str], NoneType, Dict[str, str]]


# consts from playerjs
_O_Y = "xx???x=xx?x??="  # maybe dynamic
_ABC = "ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz"
_SALT_ABC_STRING = f"{_ABC}0123456789+/="

#_RE_O_U1 = re.compile(r"""u:\\['"](#1[^>]+==)\\['"]""")  # for extract ciphered data from player js

_RE_O_U1 = re.compile(r"u:\s*\\\s*['\"]([^=]+=[\\]+)\s*['\"]")
"""js script encoded"""

_BASE36_STRING = "0123456789abcdefghijklmnopqrstuvwxyz"

# search anchors
_PARAMS_START = "return p}('"
_PARAMS_PACKED_OFFSET = len(_PARAMS_START) - 1

__all__ = ["extract_playlist"]


def parse_params_to_unpack(packed: str) -> T_PACKED:
    index = packed.find(_PARAMS_START)  # start js code paks

    packed = packed[index + _PARAMS_PACKED_OFFSET : -1]
    p_re = re.search(r"'(.*[^\\])',", packed)
    try:
        p = p_re[1]  # type: ignore
        packed = packed[p_re.span(1)[1] + 2 :]  # type: ignore
        a_re = re.search(r"(\d+),", packed)
        a = int(a_re[1])  # type: ignore
        packed = packed[a_re.span(1)[1] + 1 :]  # type: ignore
        c_re = re.search(r"(\d+),", packed)
        c = int(c_re[1])  # type: ignore
        packed = packed[c_re.span(1)[1] + 1 :]  # type: ignore
        k_re = re.search(r"'(.*)[^\\]'\.split", packed)
        k = k_re[1].split("|")  # type: ignore
    except AttributeError as e:
        raise ValueError("Could not extract params from packed playerjs") from e
    _, d = None, {}  # type: ignore
    return p, a, c, k, _, d


def unpack_playerjs(p: str, a: int, c: int, k: List[str], _: NoneType, d: Dict[str, str]) -> str:
    """Playerjs unpacker"""

    def e(c: int):
        return ("" if c < a else e(c // a)) + (chr(c + 29) if (c := c % a) > 35 else _BASE36_STRING[c])

    while c:
        c -= 1
        d[e(c)] = k[c] or e(c)
    p = re.sub(r"\b\w+\b", lambda e: d.get(e.group(), e.group()), p)

    return p


def get_crypt_codes(playerjs_packed_script):
    packed_params = parse_params_to_unpack(playerjs_packed_script)
    playerjs_script = unpack_playerjs(*packed_params)
    return _RE_O_U1.search(playerjs_script)[1]


# region playerjs crypto
def decode(x):
    if x[:2] == "#1":
        return Salt().d(pepper(x[2:], -1))
    elif x[:2] == "#0":
        return Salt().d(x[2:])
    else:
        return x


class Salt:
    def __init__(self, key_str=_SALT_ABC_STRING):
        self._keyStr = key_str

    def d(self, e):
        t = ""
        f = 0
        e = "".join([c for c in e if c in self._keyStr])
        while f < len(e):
            s = self._keyStr.index(e[f])
            f += 1
            o = self._keyStr.index(e[f])
            f += 1
            u = self._keyStr.index(e[f])
            f += 1
            a = self._keyStr.index(e[f])
            f += 1
            n = (s << 2) | (o >> 4)
            r = ((o & 15) << 4) | (u >> 2)
            i = ((u & 3) << 6) | a
            t += chr(n)
            if u != 64:
                t += chr(r)
            if a != 64:
                t += chr(i)
        return self._ud(t)

    @staticmethod
    def _ud(e):
        t = ""
        n = 0
        while n < len(e):
            r = ord(e[n])
            if r < 128:
                t += chr(r)
                n += 1
            elif 191 < r < 224:
                c2 = ord(e[n + 1])
                t += chr(((r & 31) << 6) | (c2 & 63))
                n += 2
            else:
                c2 = ord(e[n + 1])
                c3 = ord(e[n + 2])
                t += chr(((r & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
                n += 3
        return t


def pepper(s, n):
    s = s.replace("+", "#")
    s = s.replace("#", "+")
    a = sugar(_O_Y) * n
    if n < 0:
        a += len(_ABC) / 2
    r = _ABC[int(a * 2) :] + _ABC[: int(a * 2)]
    return re.sub(r"[A-Za-z]", lambda c: r[_ABC.index(c.group())], s)


def sugar(x):
    x = x.split(chr(61))
    result = ""
    c1 = chr(120)
    for item in x:
        encoded = "".join(chr(49) if char == c1 else chr(48) for char in item)
        chr_val = int(encoded, 2) if encoded else 0
        result += chr(chr_val)
    return int(result[:-1])


# endregion playerjs crypto


def b64e_url_params(s):
    return base64.b64encode(urllib.parse.quote(s).encode()).decode()


def b64d_url_params(s):
    return urllib.parse.unquote(base64.b64decode(s).decode())


def extract_playlist(player_js_packed_response: str, player_encoded: str) -> Dict[str, Any]:
    """extract and decode playlist from dreamcast player

    :param player_js_packed_response: raw dreamcast playerjs response
    :param player_encoded: encoded (key? url?) in anime page from $(function() {new Playerjs("(...)")...
    :return: decoded playlist
    """
    o_u = get_crypt_codes(player_js_packed_response)
    v = json.loads(decode(o_u))
    v["file3_separator"] = "//"
    a = player_encoded[2:]
    # bk0 ... bk4 keys
    for key in (f"bk{i}" for i in range(4, -1, -1)):
        if (result := v.get(key)) and result not in ("undefined", "", None):
            a = a.replace(v["file3_separator"] + b64e_url_params(result), "")
    # unsafe, maybe throw error
    return json.loads(b64d_url_params(a))

import re
from typing import TypedDict, Union, Optional
from contextlib import suppress


from parsel import Selector, SelectorList

T_AnimePage = TypedDict(
    "T_AnimePage",
    {
        "title": str,
        "description": Optional[str],
        "thumbnail": str,
        "player_js_encoded": str,
        "player_js_url": str,
    },
)


class AnimePage:
    """

        Usage example:

        GET https://dreamerscast.com/home/release/323-tensei-kizoku-kantei-skill-de-nariagaru-2

        Encoding (24.12.24 actual step-by-step)

        decoding and extract playlist:

        - GET <player_js_url>
        - unpack, extract encoded symbols
        - by <player_js_encoded> and <player_js_url> values decrypt it (implement logic from original source code)


    {
        "title": "String",
        "description": "String",
        "thumbnail": "String",
        "player_js_encoded": "String",
        "player_js_url": "String"
    }"""

    def __init__(self, document: Union[str, Selector, SelectorList]) -> None:
        self._document = Selector(document) if isinstance(document, str) else document

    def _parse_title(self, v: Union[Selector, SelectorList]) -> str:
        v0 = v.css("h3")
        return "".join(v0.css("::text").getall())

    def _parse_description(self, v: Union[Selector, SelectorList]) -> Optional[str]:
        v0 = v
        with suppress(Exception):
            v1 = v0.css(".postDesc")
            return "".join(v1.css("::text").getall())
        return None

    def _parse_thumbnail(self, v: Union[Selector, SelectorList]) -> str:
        v0 = v.css(".details_poster img")
        v1 = v0.attrib["src"]
        return f"https:{v1}"

    def _parse_player_js_encoded(self, v: Union[Selector, SelectorList]) -> str:
        v0 = v.get()
        return re.search(r'new Playerjs\("(.*?)"\)', v0)[1]

    def _parse_player_js_url(self, v: Union[Selector, SelectorList]) -> str:
        v0 = v.get()
        v1 = re.search('<script[^>]+src="(/js/playerjs.*?)"', v0)[1]
        return f"https://dreamerscast.com{v1}"

    def parse(self) -> T_AnimePage:
        return {
            "title": self._parse_title(self._document),
            "description": self._parse_description(self._document),
            "thumbnail": self._parse_thumbnail(self._document),
            "player_js_encoded": self._parse_player_js_encoded(self._document),
            "player_js_url": self._parse_player_js_url(self._document),
        }

if __name__ == "__main__":
    import httpx
    import subprocess
    import argparse

    def main(anime_url: str) -> dict:
        anime_resp = httpx.get(anime_url)
        anime_page = AnimePage(anime_resp.text).parse()
        player_encoded = anime_page["player_js_encoded"]
        player_js_url = anime_page["player_js_url"]

        player_js_packed_response = httpx.get(player_js_url).text
        return extract_playlist(player_js_packed_response, player_encoded)

    parser = argparse.ArgumentParser(prog="DreamFucker")

    parser.add_argument('-u', '--url')

    args = parser.parse_args()

    data_dict = main(args.url)

    try:
        import yt_dlp
    except ImportError:
        print("ОШИБКА: Библиотека yt-dlp не найдена.")
        print("Пожалуйста, установите её: pip install yt-dlp")
        exit(1)

    import os # Для работы с путями

    # --- Функция-обработчик прогресса (hook) ---
    # Будет вызываться yt-dlp во время скачивания
    def progress_hook(d):
        # Убрали thread_name, так как потока нет
        if d['status'] == 'downloading':
            total_bytes_str = d.get('_total_bytes_str', 'N/A')
            downloaded_bytes_str = d.get('_downloaded_bytes_str', 'N/A')
            speed_str = d.get('_speed_str', 'N/A').strip()
            eta_str = d.get('_eta_str', 'N/A').strip()
            percent_str = d.get('_percent_str', 'N/A').strip()
            filename = d.get('filename', '???')

            # Выводим прогресс в одну строку
            print(f'\rСкачивание {os.path.basename(filename)}: {percent_str} из {total_bytes_str} ({downloaded_bytes_str}) со скоростью {speed_str}, ETA {eta_str}      ', end='')

        elif d['status'] == 'finished':
            filename = d.get('filename', '???')
            total_bytes_str = d.get('_total_bytes_str', d.get('total_bytes_str', 'N/A'))
            # Печатаем завершение и переходим на новую строку
            print(f'\rЗавершено скачивание: {os.path.basename(filename)} ({total_bytes_str})                              ')

        elif d['status'] == 'error':
            filename = d.get('filename', '???')
            print(f'\rОшибка скачивания файла {os.path.basename(filename)}                                                 ')

    # --- Функция для скачивания с использованием библиотеки yt-dlp (без потоков) ---
    def download_with_library_sequential(title, media_url):
        """Скачивает видео с использованием yt_dlp последовательно."""
        print(f"\n--- Начинается обработка: {title} ---") # Добавили \n для разделения вывода
        print(f"URL: {media_url}")

        # Настройки для yt-dlp
        ydl_opts = {
            'outtmpl': f'{title}.%(ext)s',
            'progress_hooks': [progress_hook],
            'noplaylist': True,
            'quiet': True,
            'no_warnings': True,
            'retries': 5,
            'fragment_retries': 5,
            'concurrent_fragment_downloads': 5,
            # 'verbose': True, # Для отладки
        }

        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                error_code = ydl.download([media_url])
                if error_code != 0:
                    print(f"Скачивание '{title}' завершилось с кодом ошибки: {error_code}")
                # Успешное завершение уже печатается в progress_hook

        except yt_dlp.utils.DownloadError as e:
            print(f"\nОШИБКА yt-dlp при скачивании '{title}': {e}")
        except Exception as e:
            print(f"\nНеожиданная ОШИБКА Python при обработке '{title}': {type(e).__name__} - {e}")

    # --- Основная логика (последовательное выполнение) ---
    if 'file' in data_dict and isinstance(data_dict.get('file'), list):
        file_list = data_dict['file']
        if not file_list:
            print("Список 'file' пуст. Нечего скачивать.")
        else:
            total_files = len(file_list)
            print(f"Найдено {total_files} элементов для скачивания.")

            for i, item in enumerate(file_list):
                print(f"\nОбработка файла {i+1} из {total_files}...") # Индикация текущего файла
                title = item.get('title', f'unknown_title_{item.get("id", f"no_id_{i}")}')
                file_urls_string = item.get('file', '')

                if not title or not file_urls_string:
                    print(f"Пропуск элемента #{i+1}: отсутствует 'title' или 'file'. Данные: {item}")
                    continue

                urls = file_urls_string.split(' or ')
                if not urls:
                    print(f"Не удалось извлечь URL для '{title}' из строки: {file_urls_string}")
                    continue

                media_url = urls[0].strip()
                if not media_url.startswith(('http://', 'https://')):
                     print(f"Неверный формат URL '{media_url}' для '{title}'. Попытка второго URL...")
                     if len(urls) > 1:
                         media_url = urls[1].strip()
                         if not media_url.startswith(('http://', 'https://')):
                             print(f"Второй URL '{media_url}' также невалиден для '{title}'. Пропуск.")
                             continue
                     else:
                         print(f"Второй URL отсутствует для '{title}'. Пропуск.")
                         continue

                # --- Прямой вызов функции скачивания ---
                download_with_library_sequential(title, media_url)
                # Пауза не обязательна, но может быть полезна для чтения вывода
                # time.sleep(0.5)

            print("\nВсе операции скачивания завершены.")

    else:
        print("Ключ 'file' отсутствует в данных или не является списком.")
```